首页 > 技术文章 > CF708C Centroids

luckyblock 2020-10-18 21:36 原文

知识点:换根 DP,up and down

原题面:Codeforces

Original_Humankind

上古时期的古代人类的涂鸦。

第一次正式地接触换根 DP = =
写这么个破题写了好长时间。

另外,up and down 似乎是 cdx 时代的叫法/jk

简述

给定一棵大小为 \(n\) 树。
可以进行一种修改:删去任意一条边,再加入一条边,需保证修改后后还是一棵树。
对于每一个节点,判断进行不多于一次修改后,该点能否成为重心(若以某个点为根,每个子树的大小都不大于\(\frac{n}{2}\),则称该点为重心)。
\(1\le n\le 4\times 10^5\),时限 4s。

分析

钦定 1 为根,求得所有子树的 \(size\) 和 重儿子 \(son\)
考虑一个本不是重心的点 \(u\),以它为根时必存在一\(size > \frac{n}{2}\) 的儿子。
在 dfs 的过程中考虑该子树的位置:
若该它在 \(u\) 以下,则一定为重儿子 \(son_u\)
否则它由 \(u\) 以上树的剩余部分构成,大小为 \(n - size_u\)

显然,欲使 \(u\) 成为重心,必须从该大小 \(>\frac{n}{2}\) 的子树取出一块。
贪心的想,最优的修改,是取出一块 \(size \le \frac{n}{2}\) 的且 \(size\) 最大的,直接接到 \(u\) 上。
若原子树 \(size\) 减去取出部分的 \(size\) \(\le \frac{n}{2}\)\(u\) 就成为了重心。


需要同时维护子树和祖先的信息,考虑换根 DP。
第一次 dfs 处理子树信息,第二次 dfs 处理祖先信息进行换根。

先考虑子树信息。
\(f_{u}\) 表示 \(u\)儿子 的子树中的,\(size \le \frac{n}{2}\) 的子树中的,最大的 \(size\)
\(g_v\)\(v\) 的贡献,则显然有:

\[\begin{aligned} &g_v = \begin{cases} size_{v} &size_{v}\le \frac{n}{2}\\ {f_v} &\text{otherwise}\\ \end{cases}\\ &f_{u} = \max_{fa_v=u} \{g_v\} \end{aligned}\]

对于祖先的信息,考虑在第二次 dfs 中维护。
\(val\) 表示 \(u\) 以上部分中,\(size \le \frac{n}{2}\) 的子树中的,最大的 \(size\)
考虑向下深入,根由 \(u\) 变为 \(v\) 时,对 \(val\) 的影响。

对于\(v\) 上面部分,若 \(n - size_u\le \frac{n}{2}\),则其贡献为 \(n - size_u\),否则贡献为 \(val\)
对于 \(v\) 的兄弟 \(d\),显然其贡献为 \(f_d\)
贡献取 \(\max\) 即为新的 \(val\)

在第二次 dfs 换根时,进行答案的判定,则有:

\[ans_u = \begin{cases} 1 &(n-size_{u}\le \frac{n}{2}) \land (size_{son_u}\le \frac{n}{2})\\ [n-size_{u}-val \le \frac{n}{2}] &n-size_{u}\le \frac{n}{2}\\ [size_{son_u} - f_{son_u}\le \frac{n}{2}] &size_{son_u}\le \frac{n}{2}\\ \end{cases}\]


考虑一些奇怪的实现。

在更新 \(val\) 时,提到了这样一句:「对于 \(v\) 的兄弟 \(d\),显然其贡献为 \(f_d\)」。
若直接枚举兄弟进行更新,复杂度显然无法承受。

考虑预处理,对于一个节点 \(u\) 的所有儿子 \(v\),考虑将所有 \(f_{v_i}\) 写成一个序列。
所求即为抠去 将转移到的 \(v\) 后序列的最大值,可以通过维护序列前缀 和 后缀最大值实现。
详见代码。

采用这样的实现后,总复杂度是常熟略大的 \(O(n)\)


以上是个人的傻逼写法,看了题解之后学习了。
真的有必要维护前缀最大值,和后缀最大值吗?
显然没有必要。

若将转移到的 \(f_v\) 不是最大的,则用于更新的 \(f_{v_i}\) 一定是最大的。
否则则用于更新的 \(f_{v_i}\),一定是次大的。
仅维护最大值和次大值即可,总复杂度不变,但常数和空间复杂度都大大减小。

代码

自己 YY 的粗鄙的实现。

//知识点:换根 DP 
/*
By:Luckyblock
设 size[u],u 子树的大小。  
son[u],u 的重儿子。  
f[u],u 的儿子中, size \le \dfrac{n}{2} 的子树中,最大的 size。  
pre[u][x],u 的前 x 个儿子中,size \le \dfrac{n}{2} 的子树中,最大的 size。  
suf[u][x],u 的后 x 个儿子中,size \le \dfrac{n}{2} 的子树中,最大的 size。  
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kMaxn = 4e5 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int son[kMaxn], num[kMaxn], rk[kMaxn];
int size[kMaxn], f[kMaxn];
std::vector <int> pre[kMaxn], suf[kMaxn], tmp[kMaxn];
bool ans[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Debug(int u_) {
  for (int i = 1; i <= num[u_]; ++ i) {
    printf("%d ", pre[u_][i]);
  }
  printf("\n");
  for (int i = num[u_]; i >= 1; -- i) {
    printf("%d ", suf[u_][i]);
  }
  printf("\n");
}
void Dfs1(int u_, int fa_) {
  size[u_] = 1ll;
  pre[u_].push_back(0);
  suf[u_].push_back(0);
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue ;

    tmp[u_].push_back(v_);
    rk[v_] = ++ num[u_];

    Dfs1(v_, u_);
    size[u_] += size[v_];
    if (size[v_] > size[son[u_]]) son[u_] = v_;

    int max_v = (size[v_] <= n / 2) ? size[v_] : f[v_];
    pre[u_].push_back(max_v);
    Chkmax(pre[u_][num[u_]], pre[u_][num[u_] - 1]);
  }
  for (int i = num[u_], j = 1; i >= 1; -- i, ++ j) {
    int v_ = tmp[u_][i - 1];
    int max_v = (size[v_] <= n / 2) ? size[v_] : f[v_];
    suf[u_].push_back(max_v);
    Chkmax(suf[u_][j], suf[u_][j - 1]);
  }
  f[u_] = pre[u_][num[u_]];
}
void Dfs2(int u_, int fa_, int val_) {
  if ((size[son[u_]] <= n / 2) && (n - size[u_] <= n / 2)) {
    ans[u_] = true;
  } else {
    if (size[son[u_]] > n / 2) {
      ans[u_] |= (size[son[u_]] - f[son[u_]] <= n / 2);
    } else {
      ans[u_] |= (n - size[u_] - val_ <= n / 2);
    }
  }

  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue ;
    int new_val = val_;
    if (n - size[u_] <= n / 2) Chkmax(new_val, n - size[u_]);
    Chkmax(new_val, pre[u_][rk[v_] - 1]);
    Chkmax(new_val, suf[u_][num[u_] - rk[v_]]);
    Dfs2(v_, u_, new_val);
  }
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_), AddEdge(v_, u_);
  }
  Dfs1(1, 0), Dfs2(1, 0, 0);
  for (int i = 1; i <= n; ++ i) {
    printf("%d ", ans[i] ? 1 : 0);
  }
  return 0;
}

用于对拍的暴力。
对于每个节点,DP 找到其子树中 \(size\le \frac{n}{2}\) 的最大的子树用于修改。
复杂度 \(O(n^2)\)

//知识点:DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector> 
#define LL long long
const int kMaxn = 4e5 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int son[kMaxn], size[kMaxn], f[kMaxn];
bool ans[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Dfs1(int u_, int fa_) {
  size[u_] = 1ll;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue ;
    Dfs1(v_, u_);
    size[u_] += size[v_];
    if (size[v_] > size[son[u_]]) son[u_] = v_;
    
    int max_v = (size[v_] <= n / 2) ? size[v_] : f[v_];
    Chkmax(f[u_], max_v);
  }
}
//=============================================================
int main() {
  freopen("1.in", "r", stdin);
  freopen("force.out", "w", stdout);
  n = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_), AddEdge(v_, u_);
  }
  for (int i = 1; i <= n; ++ i) {
    memset(f, 0, sizeof (f));
    memset(son, 0, sizeof (son));
    memset(size, 0, sizeof (size));
    Dfs1(i, 0);
    if (size[son[i]] <= n / 2) {
      ans[i] = true; 
    } else {
      ans[i] |= size[son[i]] - f[son[i]] <= n / 2;
    }
  }
  for (int i = 1; i <= n; ++ i) {
    printf("%d ", ans[i] ? 1 : 0);
  }
  return 0;
}

推荐阅读