点分治(树分治)详解

       作者: hsez_yyh

       链接:https://blog.csdn.net/yyh_getAC/article/details/126696654

       来源:湖北省黄石二中信息竞赛组
       著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

        点分治,又叫树分治,顾名思义,点分治就是在树上进行分治并统计答案的一种方式。此前我们就有涉及过分治的思想:将原问题分解成若干相同形式、相互独立的子问题,并逐个干掉。而点分治就是基于这种思想,只不过其适用类型被移到了树上。通常的,对于点分治能解决的问题,都是与 树上路径 的统计与询问有关。

        我们先从一道题目入手:P4178 Tree - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

        题目大意就是:给定一棵 n 个节点的树,每条边有边权,求出树上两点距离小于等于 k 的点对数量。

        乍一看,没什么好思路。可以先想一下暴力做法:枚举每个点,然后对于每个点为根跑一边dfs,统计该点到其他所有点的距离,答案/2即可。很显然这是一种可行方案。但是时间代价太高了,需要 O( N^2 ) 的复杂度才能解决。于是我们可以考虑来优化我们的解法。

        仔细思考,我们不难发现一个这样的性质:当我们选择任意一个点作为根rt时,任何一条从 u 到 v 的路径path(u,v),要么穿过rt,即 lca(u,v) = rt ;要么就在 rt 的某个子树 sub(son[rt]) 里面。

基于此性质,我们可以每次选取一个点作为分治中心,令其为 rt 做 dfs,那么问题就可以转化为:

统计 T树 中经过 rt 且长度<=K的路径数量即可。                        根据分治的思想,我们就能推理出点分治的大致步骤:每次选取一个合适点作为分治中心,统计答案,然后对于该分治中心的所有子树,继续进行分治操作。        对于所有已经作为分治中心的点,我们需要将其打上标记,因为一旦某个点作为分治中心进行操作并统计答案后,其再也不会对其他的分治过程产生影响。

        下面我们要来考虑一个很重要的问题:如何选取合适的分治中心。 假设我们随机选择分治中心,那么我们考虑极端情况,当一棵树在我们的随机选择下退化成了一条链,而我们最开始的分治中心为链的一个端点,那么悲剧的一幕发生了:你即将递归 N(节点数) 次,对于每次的操作,又是O(N)或O(N*logN) 的复杂度,很显然,又一次悲剧的卡成了 O(N^2) 以上的复杂度。 好吧,不要庆幸,总有那些毒瘤出题人给你卡没掉。显然O(N) 层的递归是我们不想要的,我们需要的是无论什么数据,都能保证严格O(logN) 的方法,怎么样才能保证整个递归操作都是严格的 logN 层呢?欸,这里就要请出我们上古时期的一个知识点:树的重心 (不会的建议回炉重造) 。由于树的重心能保证以其为根节点,任意一个子树的大小都 \leq N/2 ,所以,我们只需要每次在递归时寻找当前树 的重心,就能保证我们的递归最多进行 logN 层。然而我们每次找重心也只需要把当前数树遍历一遍,完全ok的,也就是常数大了一倍,但是可以接受,毕竟我们把总复杂度强行降到了严格的 O(N\times log N)或O(N\timeslog^2 N) 级别了。 而这里每层递归的复杂度是 O(N) 还是 O(N\times log N) , 取决于统计答案的复杂度。

        下面给出伪代码:

void solve(int u) // 统计 sub(u) 中的合法答案 
{
	rt=dfs_rt();//找重心 
	ans=get_res()//统计答案
	for( j∈son[u] ) //继续递归 
	{
		if(vis[j]) continue ;// 已经分治过的点不能再次访问 
		ans+=solve(j);
	} 
	return ans;
}

         这里先稍微介绍一下树的重心的特殊性质(放松一下思维):

性质
1. 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。

2. 把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
3. 把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
4. 一棵树最多有两个重心,且相邻。
5. 一个点是重心,等价于以这个点为根,它的每个子树的大小,都不会超过整个树大小的一半。

        好,我们具体到本题,get_res() 可以通过 排序双指针+容斥 来统计答案(不会qwq),或者直接套数据结构( 这个本人在行qwq ),例如:树状数组、线段树、平衡树 等等。 以上这些方法,都是带 logN 的,所以结合点分治的复杂度 ,那么总复杂度就是 O(N\times log^2 N) 的,解决该题还是绰绰有余。

        特别注意: 在进行点分治的时候最好不要使用 memset 来清数组,因为如果用 memset 的话,复杂度就会直接爆表,直接到 O(N^2\times log N) 。所以还是要老老实实的自己手动清一下。这一点可以参考 整体二分 或者 CDQ分治 ( QWQ ) 。

        本题 k 较小,就直接 树状数组 luo 过去了,只用稍微维护一下单点修改,求前缀和即可,还是比较 easy 的。        下面给出代码:

#include<bits/stdc++.h>
using namespace std;
const int N=4e4+10;
int n,m;
int h[N],ne[N*2],e[N*2],f[N*2],idx;
int c[N];
void build(int xx,int yy,int val)
{
	e[idx]=yy;
	f[idx]=val;
	ne[idx]=h[xx];
	h[xx]=idx++;
}
int lowbit(int xx)
{
	return xx&-xx;
}
void add(int u,int val)
{
	while(u<=m)
	{
		c[u]+=val;
		u+=lowbit(u);
	}
}
int query(int u)
{
	int sum=0;
	while(u)
	{
		sum+=c[u];
		u-=lowbit(u);
	}
	return sum;
}
// 以上是树状数组板子 
int rt;
int si[N];
bool vis[N];
void dfs_rt(int u,int fa,int pt)
{
	si[u]=1;
	int nn=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_rt(j,u,pt);
		si[u]+=si[j];
		nn=max(nn,si[j]);
	}
	nn=max(nn,pt-si[u]);
	if(nn*2<=pt) rt=u;
}// 找 重心 
void dfs_sz(int u,int fa)
{
	si[u]=1;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_sz(j,u);
		si[u]+=si[j];
	}
}// 以重心为根求size 
int cnt,dis[N];
void dfs_dis(int u,int fa,int w)
{
	if(w<=m) dis[++cnt]=w;
	else return ;//这里稍微优化一下 
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_dis(j,u,w+f[i]);
	}
}// 统计一下一端为根节点的不同长度的路径出现过的次数 
void dfs_clear(int u,int fa,int w)
{
	if(w<=m) add(w,-1);
	else return ;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_clear(j,u,w+f[i]);
	}
}// 手动清理一下树状数组 
int solve(int u,int pt)
{
	dfs_rt(u,0,pt);
	u=rt;
	vis[u]=true;
	dfs_sz(u,0);
	int ans=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		int w=f[i];
		if(vis[j]) continue ;
		cnt=0;
		dfs_dis(j,u,w);
		ans+=cnt;
		for(int k=1;k<=cnt;k++)
		ans+=query(m-dis[k]); // 相当于求把两条合法路径拼在一起还是合法路劲的条数 
		for(int k=1;k<=cnt;k++)
		add(dis[k],1); 
		// 这里必须先统计完了答案再加入树状数组,因为同一棵子树里的两条路劲不能拼在一起。 
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		int w=f[i];
		if(vis[j]) continue ;
		dfs_clear(j,u,w);
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		ans+=solve(j,si[j]);
	}// 继续向下递归(分治) 
	return ans;
}
int main()
{
	scanf("%d",&n);
	memset(h,-1,sizeof(h));
	for(int i=1;i<n;i++)
	{
		int xx,yy,ff;
		scanf("%d %d %d",&xx,&yy,&ff);
		build(xx,yy,ff),build(yy,xx,ff);
	}
	scanf("%d",&m);
	printf("%d\n",solve(1,n));
	return 0;
}

        点分治就相当于付出了 O(logN) 的时间代价,使得答案路径经过当前的分治中心这一条件。

        继续看一道题:252. 树 - AcWing题库

这道题的 k 较大,不能使用我们的树状数组,虽然看到大佬们都是用容斥一下就过了,但是本蒟蒻不会,就只能写个平衡树维护一下了。 代码和上面的一模一样,只用把树状数组部分换成平衡树即可。

       来道 IOI 的题题: [IOI2011]Race - 洛谷

还是点分治的模板题目,题目大意:给一棵树,每条边有权。求一条简单路径,权值和等于 k,且边的数量最小。

        我们发现题目中的 k 比较小,只有 10^6 ,那么就可以直接用 t[ dis[i] ] 来表示长度为 dis[i] 的路径所需的最小边数,其余代码同上:

#include<bits/stdc++.h>
#define fir first
#define sed second
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10,M=1e6+10,INF=0x3f3f3f3f;
int n,m;
int t[M];
int h[N],ne[N*2],e[N*2],f[N*2],idx;
void build(int xx,int yy,int val)
{
	e[idx]=yy;
	ne[idx]=h[xx];
	f[idx]=val;
	h[xx]=idx++;
}
bool vis[N];
int si[N],dep[N],rt;
void dfs_rt(int u,int fa,int pt)
{
	si[u]=1;
	int nn=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_rt(j,u,pt);
		si[u]+=si[j];
		nn=max(nn,si[j]);
	}
	nn=max(nn,pt-si[u]);
	if(nn*2<=pt) rt=u;
}
void dfs_si(int u,int fa)
{
	si[u]=1;
	dep[u]=dep[fa]+1;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_si(j,u);
		si[u]+=si[j];
	}
}
int cnt;
PII dis[N];
void dfs_dis(int u,int fa,int w)
{
	if(w<=m) 
	{
		dis[++cnt].sed=dep[u];
		dis[cnt].fir=w;
	}
	else return ;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_dis(j,u,w+f[i]);
	}
}
void dfs_clear(int u,int fa,int w)
{
	if(w<m) t[w]=0x3f3f3f3f;
	else return ;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_clear(j,u,w+f[i]);
	}
}
void solve(int u,int pt)
{
	dfs_rt(u,0,pt);
	u=rt;
	vis[u]=true;
	dfs_si(u,0);
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		cnt=0;
		dfs_dis(j,u,f[i]);
		for(int k=1;k<=cnt;k++)
		t[m]=min(t[m],t[m-dis[k].fir]+dis[k].sed);
		for(int k=1;k<=cnt;k++)
		t[dis[k].fir]=min(t[dis[k].fir],dis[k].sed);
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		dfs_clear(j,u,f[i]);
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		solve(j,si[j]);
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	memset(h,-1,sizeof(h));
	memset(t,0x3f,sizeof(t));
	dep[0]=-1;
	for(int i=1;i<n;i++)
	{
		int xx,yy,ff;
		scanf("%d %d %d",&xx,&yy,&ff);
		xx++,yy++;
		build(xx,yy,ff),build(yy,xx,ff);
	}
	solve(1,n);
	if(t[m]>1e9) printf("-1\n");
	else printf("%d\n",t[m]);
	return 0;
}

        继续看题:[国家集训队]聪聪可可 - 洛谷

        本题要求求 长度%3==0 的路径数量 ,还是比较简单,直接开一个ans数组,分别统计当前长度% 3==0/1/2 的路径数量,然后每次直接利用乘法原理求解即可。        代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e4+10;
int n;
int h[N],ne[N*2],e[N*2],f[N*2],idx;
LL ans1,ans2;
LL gcd(LL xx,LL yy)
{
	if(yy==0) return xx;
	else return gcd(yy,xx%yy);
}
void build(int xx,int yy,int val)
{
	e[idx]=yy;
	ne[idx]=h[xx];
	f[idx]=val;
	h[xx]=idx++;
}
int si[N],rt;
bool vis[N];
void dfs_rt(int u,int fa,int pt)
{
	si[u]=1;
	int nn=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_rt(j,u,pt);
		si[u]+=si[j];
		nn=max(nn,si[j]);
	}
	nn=max(nn,pt-si[u]);
	if(nn*2<=pt) rt=u;
}
void dfs_si(int u,int fa)
{
	si[u]=1;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_si(j,u);
		si[u]+=si[j];
	}
}
int cnt,d[4],c[4];
void dfs_dis(int u,int fa,LL w)
{
	d[w%3]++;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_dis(j,u,w+f[i]);
	}
}
LL solve(int u,int pt)
{
	dfs_rt(u,0,pt);
	u=rt;
	vis[u]=true;
	dfs_si(u,0);
	LL ans=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		for(int i=0;i<3;i++) d[i]=0;
		dfs_dis(j,u,1LL*f[i]);
		for(int i=0;i<3;i++)
		ans+=1LL*d[i]*c[(3-i)%3];
		ans+=d[0];
		for(int i=0;i<3;i++)
		c[i]+=d[i];
		//printf("%lld\n",ans);
	}
	for(int i=0;i<3;i++) c[i]=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		ans+=solve(j,si[j]);
	}
	return ans;
}
int main()
{
	scanf("%d",&n);
	memset(h,-1,sizeof(h));
	for(int i=1;i<n;i++)
	{
		int xx,yy,ff;
		scanf("%d %d %d",&xx,&yy,&ff);
		build(xx,yy,ff),build(yy,xx,ff);
	}
	ans1=solve(1,n)*2+n;
	ans2=1LL*n*n;
	LL g=gcd(ans1,ans2);
	printf("%lld/%lld\n",ans1/g,ans2/g);
	return 0;
}

【模板】点分治1 - 洛谷

模板题,不用多说,还是比较 luo,大家直接类比之前的代码即可。         代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=1e7+10,INF=1e7;
int n,m;
int h[N],ne[N*2],e[N*2],f[N*2],idx;
bool st[M];
int a[N];
void build(int xx,int yy,int val)
{
	e[idx]=yy;
	ne[idx]=h[xx];
	f[idx]=val;
	h[xx]=idx++;
}
int rt,si[N];
bool vis[N];
void dfs_rt(int u,int fa,int pt)
{
	si[u]=1;
	int nn=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_rt(j,u,pt);
		si[u]+=si[j];
		nn=max(nn,si[j]);
	}
	nn=max(nn,pt-si[u]);
	if(nn*2<=pt) rt=u;
}
void dfs_si(int u,int fa)
{
	si[u]=1;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_si(j,u);
		si[u]+=si[j];
	}
}
int cnt,dis[N];
bool t[M];
void dfs_dis(int u,int fa,int w)
{
	if(w<=INF) 
	{
		st[w]=true;
		dis[++cnt]=w;
	}
	else return ;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_dis(j,u,w+f[i]);
	}
}
void dfs_clear(int u,int fa,int w)
{
	if(w<=INF) t[w]=false;
	else return ;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_clear(j,u,w+f[i]);
	}
}
void solve(int u,int pt)
{
	dfs_rt(u,0,pt);
	u=rt;
	vis[u]=true;
	dfs_si(u,0);
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		cnt=0;
		dfs_dis(j,u,f[i]);
		t[0]=true;
		for(int k=1;k<=cnt;k++)
		{
			for(int p=1;p<=m;p++)
			if(dis[k]<=a[p]) st[a[p]]|=t[a[p]-dis[k]];
		}
		for(int k=1;k<=cnt;k++)
		t[dis[k]]=true;
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		dfs_clear(j,u,f[i]);
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		solve(j,si[j]);
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	memset(h,-1,sizeof(h));
	for(int i=1;i<n;i++)
	{
		int xx,yy,ff;
		scanf("%d %d %d",&xx,&yy,&ff);
		build(xx,yy,ff),build(yy,xx,ff);
	}
	for(int i=1;i<=m;i++)
	scanf("%d",&a[i]);
	solve(1,n);
	for(int i=1;i<=m;i++)
	{
		if(st[a[i]]) printf("AYE\n");
		else printf("NAY\n");
	}
	return 0;
}

树上游戏 - 洛谷        一道强行点分治的题目,比较恶心,我们要把每个点的颜色的贡献拆开看,记录对于每个分治中心,某个点能产生的贡献。 纯纯的树上乱搞题。乱搞一下就过了(也就wa了6、7发)。        代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int n;
int h[N],ne[N*2],e[N*2],idx;
int col[N];
void build(int xx,int yy)
{
	e[idx]=yy;
	ne[idx]=h[xx];
	h[xx]=idx++;
}
int rt,si[N];
bool vis[N];
void dfs_rt(int u,int fa,int pt)
{
	si[u]=1;
	int nn=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_rt(j,u,pt);
		si[u]+=si[j];
		nn=max(nn,si[j]);
	}
	nn=max(nn,pt-si[u]);
	if(nn*2<=pt) rt=u;
}
void dfs_si(int u,int fa)
{
	si[u]=1;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_si(j,u);
		si[u]+=si[j];
	}
}
LL sum;
LL d[N];
int cnt[N];
LL ans[N];
void dfs_sum(int u,int fa,int val)
{
	++cnt[col[u]];
	if(cnt[col[u]]==1)
	{
		d[col[u]]+=si[u]*val;
		sum+=1LL*si[u]*val;
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_sum(j,u,val);
	}
	--cnt[col[u]];
}
void dfs_ans(int u,int fa,int val,LL k)
{
	++cnt[col[u]];
	if(cnt[col[u]]==1)
	{
		++val;
		sum-=d[col[u]];
	}
	ans[u]+=sum+1LL*val*k;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_ans(j,u,val,k);
		
	}
	if(cnt[col[u]]==1) sum+=d[col[u]];
	--cnt[col[u]];
}
void dfs_clear(int u,int fa)
{
	d[col[u]]=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(j==fa||vis[j]) continue ;
		dfs_clear(j,u);
	}
}
void solve(int u,int pt)
{
	dfs_rt(u,0,pt);
	u=rt;
	vis[u]=true;
	dfs_si(u,0);
	sum=0;
	cnt[col[u]]++;
	ans[u]+=si[u];
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		dfs_sum(j,u,1);
	}
	ans[u]+=sum;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		dfs_sum(j,u,-1);
		dfs_ans(j,u,1,1LL*(si[u]-si[j]));
		dfs_sum(j,u,1);
	}
	cnt[col[u]]=0;
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		dfs_clear(j,u);
	}
	for(int i=h[u];~i;i=ne[i])
	{
		int j=e[i];
		if(vis[j]) continue ;
		solve(j,si[j]);
	}
}
int main()
{
	scanf("%d",&n);
	memset(h,-1,sizeof(h));
	for(int i=1;i<=n;i++)
	scanf("%d",&col[i]);
	for(int i=1;i<n;i++)
	{
		int xx,yy;
		scanf("%d %d",&xx,&yy);
		build(xx,yy),build(yy,xx);
	}
	solve(1,n);
	for(int i=1;i<=n;i++)
	printf("%lld\n",ans[i]);
	return 0;
 } 

        好吧,感觉挺简单的?        好像都是比较套路的题目?        反正到这里点分治算是入门了,毕竟还没有动态呢(点分树)

        这里再留道题题: [BJOI2017]树的难题 - 洛谷

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值