【模板】点分治 Centroid Decomposition

posted on 2022-07-20 18:59:16 | under 模板 | source

0x00 模板(P3806)

给定 \(n,k\) 和一棵树,计算

\[\sum\limits_{i,j\leq n}[{\tt dist}(i,j)=k] \]

即树上距离为 \(k\) 的点对数量。

点分治,令 \({\tt solve}(x)\) 表示我们要计算 \(u\) 所在连通块的答案。选一个根 \(u\)(它应该是重心),把点对分成两类:

  • 路径经过 \(u\) 的:在 \({\tt calc}(u)\) 中计算,表示计算经过 \(u\) 的点对数量。
  • 路径不经过 \(u\) 的:那么这两个点在 \(u\) 的某个儿子的子树里,删掉 \(u\),继续递归 \({\tt solve}(v)\) 计算。

将这个过程递归下去,就一定能解决所有点对,因为每个根都会被枚举。

选定的根 \(u\) 应该是这棵树的重心,考虑重心的定义:删掉一个点后剩余的最大连通块最小(指点数),那么这点称作树的重心。重心最多有两个,最少有一个。

node getroot(int u,int fa,int T){
	node res=node(1e9,0); 
	siz[u]=1,smx[u]=0;
	for(int i=g.head[u];i;i=g.nxt[i]){
		int v=g[i].v;
		if(v==fa||cut[v]) continue;
		res=min(res,getroot(v,u,T));
		siz[u]+=siz[v];
		smx[u]=max(smx[u],siz[v]);
	}
	smx[u]=max(smx[u],T-siz[u]);
	return min(res,node(smx[u],u));
} 

注意这个 \(\min\),这有一组数据供自测。

点击查看代码

测测你的重心!这组数据的重心是 100。

100
100 27 752
100 54 1941
99 39 1650
100 12 709
100 65 933
97 95 9594
100 19 1256
100 82 2603
100 8 507
100 60 5919
100 68 4321
100 1 97
100 26 1274
100 39 1903
100 53 340
100 28 2552
100 67 1949
100 41 291
100 25 2464
100 80 5219
100 42 3661
100 48 90
100 57 260
100 49 1970
100 18 588
91 87 3091
100 22 203
100 2 16
100 66 2889
100 71 2674
100 61 1231
100 62 5862
100 5 380
93 53 8145
100 85 7967
100 70 3318
100 24 231
100 74 205
100 32 2914
100 21 723
100 84 3657
100 89 2500
100 10 799
100 33 717
100 14 512
100 59 3516
100 63 3365
100 17 149
100 30 2049
100 44 2899
100 29 346
100 37 2159
100 20 1923
100 51 4573
100 55 4623
92 25 3272
100 75 1876
100 56 5367
100 6 430
100 88 7689
100 36 635
100 83 2866
100 46 4076
100 72 2105
100 40 2615
100 15 227
98 61 5283
100 73 4584
100 77 3674
94 58 3126
100 3 69
100 4 195
100 45 204
100 52 4389
100 9 346
100 35 3015
100 43 668
100 16 1454
100 79 1142
100 11 912
100 76 7115
100 64 2994
100 31 1031
100 58 1738
100 47 3783
100 78 4847
100 13 1129
100 50 1829
100 34 260
90 20 7448
100 86 404
95 46 7296
100 23 1292
100 38 630
100 81 1974
96 85 4742
100 87 2529
100 7 598
100 69 6213
1116777
3103
3308517
3810813
4076
346
1133103
204
3342908
1687129
7713
4296
4008534
204
2899
3873
5067
2562
1327982
3651
1869939
1751585
2092712
2931862
752
340
2662730
356
2881
430
7960
5288
3272
2387387
3657
715383
2500
4447
3661
2735624
653248
205
204
611930
1345
1355673
5423
5043
3674
2615

把递归的每一层的重心连起来连成一棵树,这棵树叫作点分树。从点分树上的一个点往上跳,点分树的子树大小翻倍,所以点分树只有 \(O(\log n)\) 层,总时间复杂度为 \(T(n)=2T(\frac{n}{2})+O({\tt calc})\)

//写法一
LL solve(int u,int k){
	cut[u]=1;
	LL res=calc(u,k);
	for(int i=g.head[u];i;i=g.nxt[i]){
		int v=g[i].v;
		if(cut[v]) continue;
		res+=solve(getroot(v,0,siz[v]).second,k);
	}
	return res;
}
//写法二:容斥
LL solve(int u){
	cut[u]=1;
	LL res=calc(u,0);
	for(int i=g.head[u];i;i=g.nxt[i]){
		int v=g[i].v;
		if(cut[v]) continue;
		res-=calc(v,g[i].w);
		res+=solve(getroot(v,0,siz[v]).second);
	}
	return res;
}
solve(getroot(1,0,n).second,k);

你注意到这个递归调用很奇怪,怎么能说 \(siz_v\) 是连通块大小呢?膜拜了 这位大佬 后,你不觉得奇怪了,它真的是对的。

那么 \({\tt calc}(u)\) 应该怎么写?

  1. \({\tt getdist}(u)\):从根出发,把每个点到根的距离存到一个数组里。
  2. 两种写法:
    • 写法一:\({\tt getdist}(u)\) 的同时记录这些点来自哪棵子树,计算答案时严格按照这个限制做(即不能有两个点在同一子树中),可以双指针。
    • 写法二:我不管什么子树不子树的,我容斥,全部点对减去在子树内的点对就是 \(u\) 的答案。注意写法,容斥的时候传下去的初始值要一样。
bool cmp(int x,int y){return dis[x]<dis[y];}
void getdist(int u,int fa,int w,int from){
	dis[id[++tot]=u]=w,fom[u]=from;
	for(int i=g.head[u];i;i=g.nxt[i]){
		int v=g[i].v;
		if(v==fa||cut[v]) continue;
		getdist(v,u,w+g[i].w,from);
	}
}
//写法一
LL calc(int u,int){
	dis[id[tot=1]=u]=0,fom[u]=u;
	for(int i=g.head[u];i;i=g.nxt[i]){
		int v=g[i].v;
		if(cut[v]) continue;
		getdist(v,u,g[i].w,v); 
	}
	sort(id+1,id+tot+1,cmp);
	for(int i=1;i<=m;i++){
		int k=q[i];LL res=0;
		if(k<0) continue;
		for(int L=1,R=tot;L<R;){
			if(dis[id[L]]+dis[id[R]]<k) L++;
			else if(dis[id[L]]+dis[id[R]]>k) R--;
			else res+=fom[id[L]]!=fom[id[R]],L++;
			if(res) break;
		}
		if(res) q[i]=-q[i];
	}
	return 19491001;
}
//写法二:O(n^2) 暴力

于是我们解决了这一题,这是 P3806 的代码实现

0x01(P4178)

给定 \(n,k\) 和一棵树,计算

\[\sum\limits_{i,j\leq n}[{\tt dist}(i,j)\leq k] \]

没有什么区别,就是这个 \(\leq\),也可以双指针,注意写法:

template<int N> struct counter{
	int c[N+10],u[N+10][2],tot;
	counter():tot(0){memset(c,0,sizeof c);}
	void insert(int k,int p){u[++tot][0]=k,u[tot][1]=p,c[p]+=k;}
	int& operator[](int i){return c[i];}
	void undo(int){c[u[tot][1]]-=u[tot][0],tot--;}
	void rollback(){for(;tot>0;undo(1));}
};
LL calc(int u,int k){
	dis[id[tot=1]=u]=0,fom[u]=u;
	for(int i=g.head[u];i;i=g.nxt[i]){
		int v=g[i].v;
		if(cut[v]) continue;
		getdist(v,u,g[i].w,v);
	}
	sort(id+1,id+tot+1,cmp);
	for(int i=1;i<=tot;i++) c.insert(1,fom[id[i]]);
	LL res=0;
	for(int L=1,R=tot;L<=tot;L++){
		while(dis[id[L]]+dis[id[R]]>k&&R>=1) c.undo(fom[id[R--]]);
		if(dis[id[L]]+dis[id[R]]<=k) res+=R-c[fom[id[L]]];
        //当前 [1,R] 可以和 L 配对,但要减掉和 L 同子树的点。
	}
	c.rollback();
	return res;
}

0x02(P2634)

给定 \(n\) 和一棵树,计算

\[\sum\limits_{i,j\leq n}[3|{\tt dist}(i,j)] \]

还是一样的,考虑算出 \(c_0,c_1,c_2\) 表示一共有多少条路径长度模 \(3\)\(0,1,2\)。用容斥的写法(双指针没有单调性)。方案数为 \((c_0)^2+2c_1c_2\)

关于这个方案数,有时 \((i,j)\)\((j,i)\) 会算两次,有时 \((i,i)\) 会混进答案(这会有 \(n\) 个),看题决定写法。

posted @ 2022-11-06 19:12  caijianhong  阅读(42)  评论(0编辑  收藏  举报