分治算法之点分治

点分治

算法学习

前言

解决什么样的问题: 树上静态路径计数,往往题目与树上路径长度有关。

原理: 以任意一个点为根,树上的路径被分为两种:过根节点的/不过根节点的。点分支在树上进行分治,每次找到重心,将一棵树变成若干棵子树。统计第一类路径,并递归到子树进行点分治,这样第二类路径就包括在了子树的第一类路径和第二类路径里。

写法:

  • 维护 d i s dis dis 表示已经存储的若干棵子树的路径, t m p tmp tmp 表示 v v v 子树的路径。
  • 通过 d i s dis dis t m p tmp tmp 的组合,即可求出所有跨根路径。

时间复杂度: 基础时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

完整模板

#include<bits/stdc++.h>
using namespace std;
const int N=2e4+100;
vector<int>G[N];
long long n,dis[N],ans=0,del[N];

//1.找重心rt
int siz[N],maxz[N],rt;
void getRt(int u,int fa){
    siz[u]=1;
    for(int v:G[u]){
        if(v==fa||del[v])continue;
        getRt(v,u);
        siz[u]+=siz[v];
        maxz[u]=max(maxz[u],siz[v]);
    }
    maxz[u]=max(maxz[u],n-siz[u]);
    if(maxz[u]<maxz[rt])rt=u;
}
//2.统计以u为根的第一类路径
//2.1求以u为根的子树距离
vector<int>tmp;
void getDis(int u,int fa,int d){
    tmp.push_back(d);
    for(int v:G[u]){
        if(v==fa||del[v])continue;
        getDis(v,u,d+1);
    }
}
//2.2两两子树合并计算
void calc(int u){
	memset(dis,0,sizeof(dis));
	for(int v:G[u]){
		if(del[v])continue;
		tmp.clear();
		getDis(v,u,1);
		//temp与dis组合答案
        for(int d:tmp)dis[d]++;
	} 
}
void dfz(int u){
	calc(u);
	//calc(u,1);
	del[u]=1; 
	for(int v:G[u]){
		if(del[v]==0){
			rt=0;
			//calc(u,-1);
			getRt(v,0);//找到子树的重心 
			dfz(rt);//计算子树 
		}
	}
}
int main(){
	int u,v;
	cin>>n;
	for(int i=1;i<n;i++){
		cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
	}
	maxz[0]=1e9;
	getRt(1,0);
	dfz(rt);
	cout<<ans;
}

例题练习

例题1:P2634 统计路径长度为 3 3 3 的整数倍的路径

题目描述: 给定一棵树,要求统计路径长度为 3 3 3 的整数倍的路径数量。

问题分析: a n s = ∑ s i g n × ( d i s [ 0 ] + d i s [ 0 ] + d i s [ 1 ] × d i s [ 2 ] × 2 ) ans=\sum sign\times (dis[0]+dis[0]+dis[1]\times dis[2]\times 2) ans=sign×(dis[0]+dis[0]+dis[1]×dis[2]×2) 。加上 u u u 节点的答案,减去 $\sum $ v v v 的答案,即可得到跨子树的答案。

void getdis(int u,int fa,int sumdis){
	dis[sumdis%3]++;
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa||del[v])continue;
		getdis(v,u,sumdis+e[i].w);
	}
}
void calc(int u,int sign,int fir){
	dis[1]=dis[2]=dis[0]=0;
	getdis(u,0,fir%3);
	if(sign==1)ans+=dis[1]*dis[2]*2+dis[0]*dis[0];
	else ans-=dis[1]*dis[2]*2+dis[0]*dis[0];
}
void dfz(int u){
	calc(u,1,0);//计算子树
	del[u]=1; 
	for(int v:G[u]){
		if(del[v]==0){
			rt=0;
			calc(v,-1,e[i].w);//删去v子树
			getRt(v,0);//找到子树的重心 
			dfz(rt);//计算子树 
		}
	}
}

例题2:P3806 统计是否有长度为 k 的路径(k<=1e7)

题目描述: 给定一棵树。 m m m 次询问,每次询问求是否有长度为 k i k_i ki 的路径。

问题分析: 将问题离线到 q [ m ] q[m] q[m] 里。组合路径时: a n s [ i ] = d i s [ q [ i ] − t m p [ j ] ] ans[i]=dis[q[i]-tmp[j]] ans[i]=dis[q[i]tmp[j]]

vector<int>tmp;
void getdis(int u,int fa,int d){
	if(d>1e7)return;
    tmp.push_back(d);
	for(int v:G[u]){
		if(v==fa||del[v])continue;
		getdis(v,u,d+e[i].w);
	}
}
vector<int>cl;
void calc(int u){
    for(int v:cl)dis[v]=0;
    dis[0]=1;
    cl.clear();
	
	for(int v:G[u]){
		if(del[v])continue;
		tmp.clear();
		getdis(v,u,e[i].w);
		//temp与dis组合答案
		for(int d:tmp){
			for(int k=1;k<=m;k++){
				if(q[k]-d>=0)ans[k]|=dis[q[k]-d];
			}
		}
        for(int d:tmp){
            dis[d]=1;
            cl.push_back(d);
        }
	} 
}
void dfz(int u){
	calc(u);
	//calc(u,1);
	del[u]=1; 
	for(int v:G[u]){
		if(del[v]==0){
			root=0;
			//calc(u,-1);
			getRt(v,0);//找到子树的重心 
			dfz(rt);//计算子树 
		}
	}
}

例题3:P4178 统计路径长度 <=k 的路径数量(k<=2e4)

题目描述: n n n 个点构成一棵树,边带边权。求路径长度 < = k <=k <=k 的路径数量。

问题分析:

  • 树上启发式合并只有求每个点为根跨子树的路径。其父节点往上的那颗子树没法完整算进去。
  • 这时候就可以用点分支了。
  • 按模板统计每棵子树的 d i s dis dis 即可,统计时:ans+=query(goal-tmp[j])+1 。其中 q u e r y ( x ) query(x) query(x) 表示 d i s dis dis < = x <=x <=x 的数量。
  • 用树状数组维护 d i s dis dis 即可。
vector<int>tmp;
void getdis(int u,int fa,int d){
	if(d>R)return;
    tmp.push_back(d);
	for(auto [v,w]:G[u]){
		if(v==fa||del[v])continue;
		getdis(v,u,d+w);
	}
}
void calc(int u){
	for(int i=1;i<=R;i++)dis[i]=0;
	for(auto [v,w]:G[u]){
		if(del[v])continue;
		tmp.clear();
		getdis(v,u,w);
		//temp与dis组合答案
		for(int d:tmp){
			if(goal-d>=0)ans+=query(goal-d)+1;
		} 
		for(int d:tmp)update(d,1);
	} 
}
void dfz(int u){
	calc(u);
	del[u]=1; 
	for(auto [v,w]:G[u]){
		if(del[v]==0){
			root=0;
			getroot(v,0);//找到子树的重心 
			dfz(root);//计算子树 
		}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值