点分治概述

感觉分治是一种很奥妙的算法。
学一万年都学不会的那种。
点分治自然属于分治算法的一种,似乎会了点分治就分治入门了?

目前为止点分治板题解决2类问题:
1.树上满足条件的路径条数计数(带修)。
2.树上路径有某种权值取max(带修)。

点分治(或者分治)的思想是什么呢?
枚举树上每一个点,处理经过它的路径。这里的处理可以看做由它出发的路径两两合并。
为了满足复杂度并且不WA,之前计算过的路径不重复计算。就是说当前的路径不经过之前处理过的点。

乍一看如果不计算处理的复杂度,复杂度是 O ( n 2 ) O(n^2) O(n2)的。

但是,神奇的事情发生了,某种神秘顺序处理点可以使复杂度将为 n l o g nlog nlog。考虑序列上的分治算法,每次取中点。不会傻乎乎地从前往后对不对?那在树上自然也不会傻乎乎地从叶子上开始。树上的中点是什么?

重心。

于是我们有了一个优秀的想法:
1.每次找一棵树的重心,处理重心。
2.删掉重心,分成很多棵树,递归这个过程。

这样我们就会点分治了。

点分治上统计答案的小技巧

有一道非常经典的题:统计树上路径长度恰好为k的路径条数。
有一个很自然的想法,
对于每一个分治中心开一个桶,记录一下从中心出发的路径长度为下标的路径数。然后遍历一遍如果当前到达点的长度为 p p p,那么长度 k − p k-p kp的路径都可以和它组成长度为 k k k的路径,于是我们把答案加上下标存的数。
但是这样会有一个问题:只有不同子树才能组成路径。如果像上面的做法会计算不合法的答案。
其实这个问题很好解决,对一棵子树处理分为查询和加入。我们对于每个子树dfs两次,第一次查询,第二次加入,这样查询的时候不会求出不合法的。由于一条路径有开头结尾,这样我们只在一个端点统计答案。感觉特别优美。

来看一道题:3697: 采药人的路径
要区分题目中的中点和分治中心,这两个一点关系都没有。不能强行让分治中心当中点做。这样就爆炸了。

正确的做法是什么呢?
把边权为0的变成-1。要求 d i s ( v , u ) = 0 dis(v,u)=0 dis(v,u)=0并且 m i d mid mid u u u的路径上有一个点 y y y d i s ( v , y ) = 0 dis(v,y)=0 dis(v,y)=0的路径。
考虑分治中心 m i d mid mid d i s ( m i d , y ) = d i s ( m i d , u ) dis(mid,y)=dis(mid,u) dis(mid,y)=dis(mid,u)
正解渐渐浮出水面:
dfs的时候记录一下每种长度有没有出现过,开两个桶,b1记录祖先没有出现过的个数,b2记录祖先出现过的个数。统计答案的时候分开讨论出现和没出现过的加入答案就行。

值得一提的是分治算法的数组很多是不能用memset清空的。不然辛辛苦苦写完了又变成 n 2 n^2 n2了。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define N 100000
    using namespace std;
    struct lxy{
    	int to,len,next;
    }eg[200005];
    
    int head[100005],size[100005],n,a1,a2,a3,dw,p,cnt;
    bool vis[100005];
    int f[200005];
    int b1[200005],b2[200005];
    long long ans;
    
    void add(int op,int ed,int len){
    	eg[++cnt].next=head[op];
    	eg[cnt].to=ed;
    	eg[cnt].len=len;
    	head[op]=cnt;
    }
    
    void findw(int u,int num){
    	int y=0;
    	size[u]=1;vis[u]=1;
    	for(int i=head[u];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	  	findw(eg[i].to,num);
    	  	size[u]+=size[eg[i].to];
    	  	y=max(y,size[eg[i].to]);
    	  }
    	y=max(y,num-size[u]);
    	if(y<p) dw=u,p=y;
    	vis[u]=0;
    }
    
    void dfs(int u,int dis){
    	vis[u]=1;
    	f[dis+N]++;
    	if(dis==0&&f[dis+N]>2) ans++;
    	if(f[dis+N]==1) ans+=b2[N-dis];
    	else ans+=b2[N-dis]+b1[N-dis];
    	for(int i=head[u];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	  	dfs(eg[i].to,dis+eg[i].len);
    	  }
    	f[dis+N]--;
    	vis[u]=0;
    }
    
    void adfs(int u,int dis){
    	vis[u]=1;
    	f[dis+N]++;
    	if(f[dis+N]==1) b1[dis+N]++;
    	else b2[dis+N]++;
    	for(int i=head[u];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	  	adfs(eg[i].to,dis+eg[i].len);
    	  }
    	f[dis+N]--;
    	vis[u]=0;
    }
    
    void clr(int u,int dis){
    	vis[u]=1;size[u]=1;
    	b1[dis+N]=0;b2[dis+N]=0;
    	for(int i=head[u];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	  	clr(eg[i].to,dis+eg[i].len);
    	  	size[u]+=size[eg[i].to];
    	  }
    	vis[u]=0;
    }
    
    void div(int u,int num){
    	if(num==1) return;
    	dw=0,p=0x7f7f7f7f;findw(u,num);
    	int wei=dw;vis[wei]=1;
    	for(int i=head[wei];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	    dfs(eg[i].to,eg[i].len);
    	    adfs(eg[i].to,eg[i].len);
    	  }
    	clr(wei,0);
    	vis[wei]=1;
    	for(int i=head[wei];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0)
    	    div(eg[i].to,size[eg[i].to]);
    }
    
    int main()
    {
    	memset(head,-1,sizeof(head));
    	scanf("%d",&n);
    	f[N]=1;
    	for(int i=1;i<n;i++){
    	  scanf("%d%d%d",&a1,&a2,&a3);
    	  if(a3==0) a3--;
    	  add(a1,a2,a3),add(a2,a1,a3);
        }
    	div(1,n);
    	printf("%lld",ans);
    }
从点分治到点分树

有了上面点分治的结构,我们发现动态的问题也可以解决。
因为如果把分治中心 f a fa fa指向上一层的分治中心成为一个树形结构。这个树高是 l o g log log。所以和一个点有关的中心其实只有 l o g log log个。如果单层修改复杂度可以接受,那么问题也可以得到解决。
但是统计答案需要思考。

[ZJOI2007]Hide 捉迷藏

构建点分树,每个分治中心开两个set,一个记录每个儿子的最深黑点,另一个记录所有黑点到 f a fa fa的距离。
每个中心的答案就是第一个set前两大的加起来。总答案就是所有中心的最大值。所以对于总答案也开一个set即可。
修改的话沿着 f a fa fa向上改就行。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<set>
    #include<queue>
    using namespace std;
    struct lxy{
    	int to,next;
    }eg[200005];
    
    int n,m,a1,a2,cnt,head[100005],size[100005],p,dw,fa[100005],q;
    bool col[100005],vis[100005];
    int dis[100005][25];
    queue <int> f;
    char s[1];
    multiset <int> ans,son[100005],sel[100005];
    
    void add(int op,int ed){
    	eg[++cnt].next=head[op];
    	eg[cnt].to=ed;
    	head[op]=cnt;
    }
    
    void findw(int u,int b,int num,int las,int tep){
    	f.push(b);
    	int y=0;
    	size[u]=1;vis[u]=1;dis[u][tep]=b;
    	for(int i=head[u];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	  	findw(eg[i].to,b+1,num,las,tep);
    	  	size[u]+=size[eg[i].to];
    	  	y=max(y,size[eg[i].to]);
    	  }
    	y=max(y,num-size[u]);
    	if(y<p) p=y,dw=u;
    	vis[u]=0;
    }
    
    void dfs(int u){
    	vis[u]=1;size[u]=1;
    	for(int i=head[u];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0){
    	  	dfs(eg[i].to);
    	  	size[u]+=size[eg[i].to];
    	  }
    	vis[u]=0;
    }
    
    int div(int u,int num,int las,int tep){
    	if(num==1){
    		fa[u]=las;dis[u][tep]=1;
    		sel[u].insert(-1);son[u].insert(0);
    		return -1;
    	}
    	p=0x7f7f7f7f,dw=0;findw(u,1,num,las,tep);
    	int wei=dw;
    	while(!f.empty()){
    		sel[wei].insert(-f.front());
    		f.pop();
    	}
    	fa[wei]=las;dfs(wei);vis[wei]=1;
    	for(int i=head[wei];i!=-1;i=eg[i].next)
    	  if(vis[eg[i].to]==0)
    	    son[wei].insert(div(eg[i].to,size[eg[i].to],wei,tep+1));
    	son[wei].insert(0);
    	if(son[wei].size()>=2){
    	  int r=*(son[wei].begin());r+=*(++son[wei].begin());
    	  ans.insert(r);
        }
    	return *(sel[wei].begin());
    }
    
    int main()
    {
    	memset(head,-1,sizeof(head));
    	scanf("%d",&n);q=n;
    	for(int i=1;i<n;i++)
    	  scanf("%d%d",&a1,&a2),add(a1,a2),add(a2,a1);
    	div(1,n,0,0);
    	for(int i=1;i<=n;i++){
    	  int j;for(j=1;dis[i][j]!=0;j++);
    	  reverse(dis[i]+1,dis[i]+j);
    	}
        scanf("%d",&m);
    	for(int i=1;i<=m;i++){
    		scanf("%s",s);
    		if(s[0]=='G'){
    			if(q==0) printf("-1\n");
    			else if(q==1) printf("0\n");
    			else printf("%d\n",-*(ans.begin()));
    		}
    		else if(s[0]=='C'){
    			scanf("%d",&a1);
    			if(son[a1].size()>=2){
    				int r=*(son[a1].begin());r+=*(++son[a1].begin());
    	            ans.erase(ans.find(r));
    			}
    			if(col[a1]==0) son[a1].erase(son[a1].find(0));
    			else son[a1].insert(0);
    			if(son[a1].size()>=2){
    				int r=*(son[a1].begin());r+=*(++son[a1].begin());
    	            ans.insert(r);
    			}
    			for(int j=a1,t=1;fa[j]!=0;j=fa[j],t++){
    				int z=fa[j];
    				if(col[a1]==0)
    				  if(sel[j].lower_bound(-dis[a1][t])!=sel[j].begin()){
    					sel[j].erase(sel[j].find(-dis[a1][t]));continue;}
    				if(col[a1]==1&&sel[j].size()!=0)
    				  if(*sel[j].begin()<=-dis[a1][t]){
    				    sel[j].insert(-dis[a1][t]);continue;}
    				if(son[z].size()>=2){
    				  int r=*(son[z].begin());r+=*(++son[z].begin());
    	              ans.erase(ans.find(r));
    				}
    				if(sel[j].size()!=0) son[z].erase(son[z].find(*sel[j].begin()));
    				if(col[a1]==0) sel[j].erase(sel[j].find(-dis[a1][t]));
    				else sel[j].insert(-dis[a1][t]);
    				if(sel[j].size()!=0) son[z].insert(*sel[j].begin());
    				if(son[z].size()>=2){
    					int r=*(son[z].begin());r+=*(++son[z].begin());
    	                ans.insert(r);
    				}
    			}
    			if(col[a1]==0) col[a1]=1,q--;
    			else col[a1]=0,q++; 
    		}
    	}
    }

点分治的变化特别多,感觉很难总结。比如上面这一道题,另一个版本就是查询里某一个点最近的黑点。按照上面的套路也可以做,但是统计答案就不一样了。

与点分治离不开的一个东西就是容斥。因为避免不了同一子树出现错误答案的情况。有一道题询问距离k以内所有点权和,带修。每个点不止开一棵线段树记录每个距离的和,还要开一棵以每个点到fa的距离作下标。这样爬树的时候才能容斥。

再补充一句,有些点分的题似乎树剖或lct也可以做。但是实在是太难写了。
那树剖或lct有什么好处呢? 空间 O ( n ) O(n) O(n)
而点分树的空间一般是 O ( n l o g ) O(nlog) O(nlog)的。

听说SC2017 点分64MB直接卡退役。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值