树上的点分治

简单说一下静态点分治,快过年时候一个下午讲过去,现在都忘得差不多了,赶紧捡起来(背个板子)

首先,点分治主要解决树的路径点权相关问题,其思想是分治(233)。

我们将树通过(子)重心进行“分块”,不断地进行统计,时间复杂度nlogn大概?

还是先来例题,不然说不明白= =

#POJ1741 树中点对统计

Description

  给定一棵N(1<=N<=100000)个结点的带权树,每条边都有一个权值(为正整数,小于等于1001)。定义dis(u,v)为u,v两点间的最短路径长度,路径的长度定义为路径上所有边的权和。再给定一个K(1<=K<=10^9),如果对于不同的两个结点u,v,如果满足dist(u,v)<=K,则称(u,v)为合法点对。求合法点对个数。

Input

  输入文件的第一行包含两个整数n和k,接下来n-1行每行都有三个整数u,v,l, 表示结点u和结点v之间有一条长l的边。

Output

  输出文件只有一行为合法点对的个数。

Sample Input

5 4

1 2 3

1 3 1

1 4 2

3 5 1

Sample Output

8

Hint

【数据范围】:
  对于50%的数据,n<=1000,k<=1000;
  对于100%的数据,n<=100000,k<=10^9;

#include<bits/stdc++.h>
using namespace std;
#define Inc(i,L,r) for(register int i=(L);i<=(r);++i)
const int N = 1e5+10;
struct Edge{
	int to[N<<1],next[N<<1],w[N<<1];
	int cnt,h[N];
	inline void add(int x,int y,int z){
		next[++cnt]=h[x];
		to[cnt]=y;
		w[cnt]=z;
		h[x]=cnt;
	}
}e;
int n,k,G,sum,Minsiz,siz[N];
int ans,dis[N];
bool vst[N];
inline void init(){
	scanf("%d%d",&n,&k);
	Inc(i,1,n-1){
		int x,y,w;scanf("%d%d%d",&x,&y,&w);
		e.add(x,y,w),e.add(y,x,w);
	}
	sum=n;
}
inline void FindG(int x,int fa){//找重心 
	int Maxsiz=0;
	siz[x]=1;
	for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
		FindG(e.to[p],x);
		siz[x]+=siz[e.to[p]];
		Maxsiz=max(Maxsiz,siz[e.to[p]]);
	}
	Maxsiz=max(Maxsiz,sum-siz[x]);
	if(Maxsiz<Minsiz)G=x,Minsiz=Maxsiz;
}
inline void FindD(int x,int fa,int dist){//记录 
	dis[++dis[0]]=dist;
	for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
		FindD(e.to[p],x,dist+e.w[p]);
	}
}
inline void Cal(int x,int dist,int cmd){//统计 
	dis[0]=0;
	FindD(x,0,dist);
	sort(dis+1,dis+dis[0]+1);
	int L=1,r=dis[0];
	while(L<r){
		if(dis[L]+dis[r]<=k)ans+=(r-L)*cmd,++L;
		else --r;
	}
}
inline void stat(int x){
	Minsiz=1<<30;
	FindG(x,0);
	vst[G]=1;
	Cal(G,0,1);
	for(int p=e.h[G];p;p=e.next[p])if(!vst[e.to[p]]){
		Cal(e.to[p],e.w[p],-1);//容斥 
		sum=siz[e.to[p]];//注意改sum 
		stat(e.to[p]);
	}
}
int main(){
	init();
	stat(1);
	cout<<ans<<"\n";
	return 0;
}

通过代码我们可以感受出点分治的策略,每次以子重心为根,统计子树对根的贡献,然后将子树的贡献“合并起来”,例如我们要统计如下图所示的子树:

假设现在重心为1,那么我们统计了:

1->2,1->2->3,1->2->4

2->3,2->4

……

实际上我们想要的只是第1行的贡献,即我们只想合并子树与子树之间的贡献,对于子树内部的贡献,则留给下次统计子树时,然后,又因为每种合法答案必然通过某个重心,而我们不会重复统计重心且会将重心枚举完全,因此能够保证答案不重不漏。

但是上述代码却统计了子树内部的信息,即我们算以1为根的子树时,将3->2->4之类的贡献也一并算进去了,但是类似的贡献其实在下一次计算时也会算进去,因此我们要将这种非法答案减去。

再来一道题。

#BZOJ2152 聪聪可可

Description

  聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:
  由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。
  聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。

Input

输入的第1行包含1个正整数n。
后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。

Output

以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。

Sample Input

5

1 2 1

1 3 2

1 4 1

2 5 3

Sample Output

13/25

Hint

【样例说明】
13组点对分别是(1,1) (2,2) (2,3) (2,5) (3,2) (3,3) (3,4) (3,5) (4,3) (4,4) (5,2) (5,3) (5,5)。

【数据规模】
  对于30%的数据,n<=1000;
  另有20%的数据,给出的树中每个节点的度不超过2;
  对于100%的数据,n<=20000。

#include<bits/stdc++.h>
using namespace std;
#define Inc(i,L,r) for(register int i=(L);i<=(r);++i)
const int N = 2e4+10;
struct Edge{
	int to[N<<1],next[N<<1],w[N<<1];
	int cnt,h[N];
	inline void add(int x,int y,int z){
		next[++cnt]=h[x];
		to[cnt]=y;
		w[cnt]=z;
		h[x]=cnt;
	}
}e;
int n,G,sum,Minsiz,siz[N];
int ans,c[3],dis[N];
bool vst[N];
inline void init(){
	scanf("%d",&n);
	Inc(i,1,n-1){
		int x,y,w;scanf("%d%d%d",&x,&y,&w);
		e.add(x,y,w),e.add(y,x,w);
	}
	sum=n;
}
inline void FindG(int x,int fa){//找重心 
	int Maxsiz=0;
	siz[x]=1;
	for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
		FindG(e.to[p],x);
		siz[x]+=siz[e.to[p]];
		Maxsiz=max(Maxsiz,siz[e.to[p]]);
	}
	Maxsiz=max(Maxsiz,sum-siz[x]);
	if(Maxsiz<Minsiz)G=x,Minsiz=Maxsiz;
}
inline void FindD(int x,int fa,int dist){
	dis[++dis[0]]=dist;
	for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
		FindD(e.to[p],x,dist+e.w[p]);
	}
}
inline void Cal(int x,int dist,int cmd){
	dis[0]=c[0]=c[1]=c[2]=0;
	FindD(x,0,dist);
	Inc(i,1,dis[0])++c[dis[i]%3];
	ans+=(c[0]*c[0]+c[1]*c[2]*2)*cmd;
}
inline void stat(int x){
	Minsiz=1<<30;
	FindG(x,0);
	vst[G]=1;
	Cal(G,0,1);
	for(int p=e.h[G];p;p=e.next[p])if(!vst[e.to[p]]){
		Cal(e.to[p],e.w[p],-1);
		sum=siz[e.to[p]];
		stat(e.to[p]);
	}
}
inline int gcd(int a,int b){
	if(!b)return a;
	return gcd(b,a%b);
}
int main(){
	init();
	stat(1);
	printf("%d/%d\n",ans/gcd(ans,n*n),n*n/gcd(ans,n*n));
	return 0;
}

发现了吗,点分治都是一个板子,所以这个玄乎的东西只要搞清楚怎么计数就好了(当然,你搞不清楚点分治计数也不一定搞得清楚啊)

最后还是总结一下点分治的套路(模板):

#define Inc(i,L,r) for(register int i=(L);i<=(r);++i)
const int N = 2e4+10;
struct Edge{
	int to[N<<1],next[N<<1],w[N<<1];
	int cnt,h[N];
	inline void add(int x,int y,int z){
		next[++cnt]=h[x];
		to[cnt]=y;
		w[cnt]=z;
		h[x]=cnt;
	}
}e;
int n,G,sum,Minsiz,siz[N];
int ans,dis[N];
bool vst[N];
inline void FindG(int x,int fa){//找重心 
	int Maxsiz=0;
	siz[x]=1;
	for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
		FindG(e.to[p],x);
		siz[x]+=siz[e.to[p]];
		Maxsiz=max(Maxsiz,siz[e.to[p]]);
	}
	Maxsiz=max(Maxsiz,sum-siz[x]);
	if(Maxsiz<Minsiz)G=x,Minsiz=Maxsiz;
}
inline void FindD(int x,int fa,int dist){
	//……记录所需信息 
	/*dis[++dis[0]]=dist;
	for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
		FindD(e.to[p],x,dist+e.w[p]);
	}*/
}
inline void Cal(int x,int dist,int cmd){
	//初始化 
	FindD(x,0,dist);
	//计算贡献 
}
inline void stat(int x){
	Minsiz=1<<30;
	FindG(x,0);
	vst[G]=1;
	Cal(G,0,1);
	for(int p=e.h[G];p;p=e.next[p])if(!vst[e.to[p]]){
		Cal(e.to[p],e.w[p],-1);//减去贡献 
		sum=siz[e.to[p]];
		stat(e.to[p]);
	}
}

另外,必须明确每次点分治解决的严格经过这个点的路径= =不然会出现玄学错误的

猜你喜欢

转载自blog.csdn.net/DancingZ/article/details/81115367