树分治乱学
点分治
\(\mathrm{\color{black}{w}\color{red}{ind\_whisper}}\) 轻松切点分治然后还写了\(\huge{\rightarrow\text{博客}\leftarrow}\) orz.
0. 什么是点分治
神 \(\mathrm{\color{black}{w}\color{red}{ind\_whisper}}\) 说 : 所谓点分治,就是把所有的点分开来治
那么,什么是点分治? 点分治 就是 淀粉汁 也就是为了勾芡而调配的水淀粉,一种乳白色悬浊液,那么,一道色香味俱全的糖醋排骨就做好啦!(大雾).
通过对于整个树形结构的遍历,一次性解决大量对于树链的询问.
对于一棵树,其任意两点之间都有唯一一条简单路径,于是总的路径数就是 \(\frac{n(n - 1)}{2}\) , 是 \(n^2\) 级别的,显然直接枚举每条路径然后维护是很劣的.
那么,在遍历一棵树的时候,把树尽可能均匀分成两部分,那么整体就是 \(\mathrm{O}(n \log n)\) 了.
1.如何点分治
为了更为平均,引入 树的重心.
对于树上的每一个点,算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心.
而重心有一个性质 : 以树的重心为根时,所有子树的大小都 不超过整棵树大小的一半.
求重心也很简单, \(\mathrm{O}(n)\) 的一次 dfs 即可.
那么问题就来了,大量的求重心复杂度会很高吗 ?
实际是不会的.只要总是找到重心,子树就能尽量均分,这样就可以实现找重心总体 \(\mathrm{O}(n \log n)\)
可以发现,分治 + 求重心的过程都是 \(\mathrm{O}(n \log n)\) 的,于是总体复杂度就是 \(\mathrm{O}(n \log n)\).
然后分的部分解决后,考虑如何处理路径问题.
首先,对于目前分治到的一个点 \(u\) (这时候这个点是重心,作为根节来用),可以求出 \(u\) 到其子树内所有的点的路径的信息,然后递归得到 \(u\) 子树内路径的信息,最后回溯到 \(u\) 时,合并这些信息和 \(u\) 父亲的信息,拼接在一起得到跨越分治到的根的信息.
2.模板题
Code :
int head[N],ecnt = -1;
struct Edge {
int nxt,to,w;
}e[N << 1];
inline void AddEdge(int st,int ed,int w) {
e[++ecnt] = (Edge) {head[st],ed,w},head[st] = ecnt;
e[++ecnt] = (Edge) {head[ed],st,w},head[ed] = ecnt;
}
int n,m;
int q[105];
bool res[105],vis[N];
bool buc[10000005];
int dist[N],tmpd[N];
int siz[N],f[N];
int totsiz,rt,cnt;
#define Max(a,b) ((a) > (b) ? (a) : (b))
void calcsiz(int u,int _f) {
siz[u] = 1,f[u] = 0;
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
calcsiz(v,u);
f[u] = Max(f[u],siz[v]);
siz[u] += siz[v];
}
f[u] = Max(f[u],totsiz - siz[u]);
if(f[u] < f[rt]) rt = u;
}
void calcdist(int u,int _f) {
tmpd[++cnt] = dist[u];
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
dist[v] = dist[u] + e[i].w;
calcdist(v,u);
}
}
std::queue<int> tag;
void solve(int u,int _f) {
buc[0] = 1;
tag.push(0);
vis[u] = 1;
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
dist[v] = e[i].w;
calcdist(v,u);
ff(j,1,cnt) ff(k,1,m) if(q[k] >= tmpd[j])
res[k] |= buc[q[k] - tmpd[j]];
ff(j,1,cnt) if(tmpd[j] < 10000000)
tag.push(tmpd[j]),buc[tmpd[j]] = 1;
cnt = 0;
}
while(!tag.empty())
buc[tag.front()] = 0,tag.pop();
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
totsiz = siz[v];
rt = 0;
f[rt] = INF;
calcsiz(v,u);
calcsiz(rt,rt);
solve(rt,u);
}
}
int mian() {
init_IO();
mems(head,-1);
n = read(),m = read();
ff(i,1,n - 1) {
int st = read(),ed = read(),w = read();
AddEdge(st,ed,w);
}
ff(i,1,m) q[i] = read();
rt = 0,f[rt] = INF;
totsiz = n;
calcsiz(1,1);
calcsiz(rt,rt);
solve(rt,rt);
ff(i,1,m) if(res[i])
putC('A'),putC('Y'),putC('E'),enter;
else
putC('N'),putC('A'),putC('Y'),enter;
end_IO();
return 0;
}
P4178 Tree
从查找有没有改为了前缀和,树状数组即可.
但是我的点分治好慢啊.
Code :
int head[N],ecnt = -1;
struct Edge {
int nxt,to,w;
}e[N << 1];
inline void AddEdge(int st,int ed,int w) {
e[++ecnt] = (Edge) {head[st],ed,w},head[st] = ecnt;
e[++ecnt] = (Edge) {head[ed],st,w},head[ed] = ecnt;
}
int n;
int q,ans;
bool vis[N];
int dist[N],tmpd[N];
int siz[N],f[N];
int totsiz,rt,cnt;
#define Max(a,b) ((a) > (b) ? (a) : (b))
void calcsiz(int u,int _f) {
siz[u] = 1,f[u] = 0;
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
calcsiz(v,u);
f[u] = Max(f[u],siz[v]);
siz[u] += siz[v];
}
f[u] = Max(f[u],totsiz - siz[u]);
if(f[u] < f[rt]) rt = u;
}
void calcdist(int u,int _f) {
tmpd[++cnt] = dist[u];
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
dist[v] = dist[u] + e[i].w;
calcdist(v,u);
}
}
#define lb(x) (x & (-x))
int T[20005];
void M(int p,int val) {
while(p <= 20001) {
T[p] += val;
p += lb(p);
}
}
int Q(int p) {
int ret = 0;
while(p) {
ret += T[p];
p -= lb(p);
}
return ret;
}
std::queue<int> tag;
void solve(int u,int _f) {
M(1,1);
tag.push(1);
vis[u] = 1;
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
dist[v] = e[i].w;
calcdist(v,u);
ff(j,1,cnt) if(q >= tmpd[j])
ans += Q(q - tmpd[j] + 1);
ff(j,1,cnt) if(tmpd[j] <= q) {
M(tmpd[j] + 1,1);
tag.push(tmpd[j] + 1);
}
cnt = 0;
}
while(!tag.empty()) {
M(tag.front(),-1);
tag.pop();
}
fe(i,u) {
int v = e[i].to;
if(v == _f || vis[v]) continue;
totsiz = siz[v];
rt = 0;
f[rt] = INF;
calcsiz(v,u);
calcsiz(rt,rt);
solve(rt,u);
}
}
边分治
和点分治相似,同样是先选择边再均分整棵树,但是可以发现,对于一个二叉树,这个过程进行的最平均,不然可能会被卡.
那么考虑新建一些不影响统计过程的结点,然后分配一下其余点使其成为二叉树.
点分树
带修好难啊.
以后再补吧.
总结
-
重链剖分解决指定少量链问题
-
点分治解决全体链性质问题
-
树形 DP 非常泛用,最重要的是如何设计状态
-
dsu on tree 有时候比较难想
-
长链剖分线性复杂度真的强不过需要先设计出 DP 方案