点分治小结

点分治小结

说在前面

本文面向个人。

基本步骤

一般点分治统计答案时有两种形式。

  1. 容斥
  2. 只计算之前的子树对之后的子树的贡献

不同的题目有不同的适宜的方法,需要看题目。

容斥

找到子树的重心。从重心开始分治。

image-20210305150934564

首先计算这一棵子树中的所有的答案。无论其是否经过当前的重心。

但是这样计算时,我们也会将在同一颗子树中的两个点统计进去啊!之后分治递归的时候,不就会重复计算了吗?

所以这里再一次减去以重心为根每一棵子树中的答案。

然后进入每一棵子树中,继续找重心,计算,容斥,分治……

只计算之前的子树对之后的子树的贡献

其实我们在每次确定一个子树之后,想要计算的是它的跨过重心的点对贡献。那么有没有办法使得只会统计跨重心的点对贡献呢?当然有。

首先像上面一样确定好子树及其重心之后开始枚举其子树(当然是还未被标记的),对其进行\(\text{dfs}\)这样就得到了这一棵子树中的数据。那么这里计算贡献就只计算分别从当前子树,之前计算过的子树这两部分中的点对贡献。显然这样的点对一定跨过重心,就不会有重复计算了。

第二种方法貌似还是比较抽象,所以要具体题目具体分析。

注意事项

一种基于错误的寻找重心方法的点分治的复杂度分析

写完了三道模板题之后才发现自己求的东西不一定是重心,但是时间复杂度不会错。以下我使用的都是错误的找重心方法

下面开始例题。

例题

洛谷 P4178 Tree

给定一棵有\(n\)个点的树

求出树上两点距离小于等于\(k\)的点对数量。

  • \(1\le n\le 4\times 10^4\).
  • \(1\le u,v\le n\).
  • \(0\le w\le 10^3\).
  • \(0\le k\le 2\times10^4\).

我觉得这道题比模板题还要清晰容易,所以就把它挪到前面来了。

使用的是容斥法。

计算重心

int book[N],sze[N],dp[N];
int sum,root;
void getG(int x,int f)
{
	sze[x]=1;
	dp[x]=0;
	for(int i=head[x];i;i=nxt[i])
	{
		if(ver[i]==f||book[ver[i]]) continue;
		getG(ver[i],x);
		sze[x]+=sze[ver[i]];
		dp[x]=max(dp[x],sze[ver[i]]);
	}
	dp[x]=max(dp[x],sum-sze[x]);
	if(dp[x]<dp[root]) root=x;
}

计算子树中的贡献

int dis[N];
int stac[N],top;
void dfs(int x,int f)
{
	stac[++top]=dis[x];
	for(int i=head[x];i;i=nxt[i])
	{
		if(ver[i]==f||book[ver[i]]) continue;
		dis[ver[i]]=dis[x]+w[i];
		dfs(ver[i],x);
	}
}
int calc(int x,int w)
{
	top=0,dis[x]=w,dfs(x,0);
	sort(stac+1,stac+top+1);
	int l=1,r=top,ans=0;
	while(l<=r)
	{
		if(stac[l]+stac[r]<=k)
		{
			ans+=r-l;
			l++;
		}
		else r--;
	}
	return ans;
}

计算贡献时需要枚举所有距离。如果是\(n^2\)枚举的话,总时间复杂度将会变成\(O(n^2\log n)\)。这里采取排序+双指针法,排序\(O(n\log n)\),双指针\(O(n)\),总时间复杂度就是\(O(n\log^2n)\),可以接受。

分治(容斥)

void solve(int x)
{
	book[x]=1;
	ans+=calc(x,0);
	for(int i=head[x];i;i=nxt[i])
	{
		if(book[ver[i]]) continue;
		ans-=calc(ver[i],w[i]);
		sum=sze[ver[i]];
		dp[root=0]=n;
		getG(ver[i],x);
		solve(root);
	}
}
  • \(line5\):计算整棵子树中的贡献
  • \(line8\):去除之后会重复计算的点对贡献
  • \(line9\sim11\):预处理出子树中新的根。

Code

/**************************************************************
 * Problem: P4178 Point divide and rule (Inclusion exclusion principle)
 * Author: Vanilla_chan
 * Date: 20210305 
**************************************************************/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<queue>
#include<vector>
#include<limits.h>
#define IL inline
#define re register
#define LL long long
#define ULL unsigned long long
#ifdef TH
#define debug printf("Now is %d\n",__LINE__);
#else
#define debug 
#endif
#ifdef ONLINE_JUDGE
char buf[1<<23],* p1=buf,* p2=buf,obuf[1<<23],* O=obuf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
#endif
using namespace std;

template<class T>inline void read(T& x)
{
	char ch=getchar();
	int fu;
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)) { x=x*10+ch-'0';ch=getchar(); }
	x*=fu;
}
inline int read()
{
	int x=0,fu=1;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)) { x=x*10+ch-'0';ch=getchar(); }
	return x*fu;
}
int G[55];
template<class T>inline void write(T x)
{
	int g=0;
	if(x<0) x=-x,putchar('-');
	do { G[++g]=x%10;x/=10; } while(x);
	for(int i=g;i>=1;--i)putchar('0'+G[i]);putchar('\n');
}
int n,m,k;
#define N 40010
int head[N],ver[N<<1],nxt[N<<1],w[N<<1],cnt;
void insert(int x,int y,int z)
{
	nxt[++cnt]=head[x];
	head[x]=cnt;
	ver[cnt]=y;
	w[cnt]=z;

	nxt[++cnt]=head[y];
	head[y]=cnt;
	ver[cnt]=x;
	w[cnt]=z;
}
int book[N],sze[N],dp[N];
int sum,root;
void getG(int x,int f)
{
	sze[x]=1;
	dp[x]=0;
	for(int i=head[x];i;i=nxt[i])
	{
		if(ver[i]==f||book[ver[i]]) continue;
		getG(ver[i],x);
		sze[x]+=sze[ver[i]];
		dp[x]=max(dp[x],sze[ver[i]]);
	}
	dp[x]=max(dp[x],sum-sze[x]);
	if(dp[x]<dp[root]) root=x;
}
int dis[N];
int stac[N],top;
void dfs(int x,int f)
{
	stac[++top]=dis[x];
	for(int i=head[x];i;i=nxt[i])
	{
		if(ver[i]==f||book[ver[i]]) continue;
		dis[ver[i]]=dis[x]+w[i];
		dfs(ver[i],x);
	}
}
int calc(int x,int w)
{
	top=0,dis[x]=w,dfs(x,0);
	sort(stac+1,stac+top+1);
	int l=1,r=top,ans=0;
	while(l<=r)
	{
		if(stac[l]+stac[r]<=k)
		{
			ans+=r-l;
			l++;
		}
		else r--;
	}
	return ans;
}
LL ans;
void solve(int x)
{
	book[x]=1;
	ans+=calc(x,0);
	for(int i=head[x];i;i=nxt[i])
	{
		if(book[ver[i]]) continue;
		ans-=calc(ver[i],w[i]);
		sum=sze[ver[i]];
		dp[root=0]=n;
		getG(ver[i],x);
		solve(root);
	}
}
int main()
{
	n=read();
	for(int i=1,a,b,c;i<n;i++)
	{
		a=read();
		b=read();
		c=read();
		insert(a,b,c);
	}
	k=read();
	dp[root=0]=sum=n;
	getG(1,0);
	solve(root);
	write(ans);
	return 0;
}


洛谷 P3806 【模板】点分治1

给定一棵有\(n\)个点的树

询问树上距离为\(k\)的点对是否存在。

\(1\le n\le 10^4,1\le m\le 100,1\le k\le10^7,1\le u,v\le n,1\le w\le 10^4\).

注意这道题虽然固定了距离是个值而非范围,但是有了多次询问。当然可以对于每一个询问,单独对每一个询问计算当然是可以的(?),时间复杂度为\(O(nm\log n)\),但是这道题卡常,时限只有\(200ms\),在线的话会卡掉\(1\sim2\)个点。

考虑将操作离线后,在计算贡献的时候可以处理所有的询问。

我认为这个的时间复杂度还是\(O(nm\log n)\)的。但是将操作离线常数会减小,大概是因为只需要做一次点分治,也不需要清空数组。

这道题我使用的是第二种方法:只计算之前的子树对之后的子树的贡献

int change[N],p;
bool judge[10000010];
void calc(int x)
{
	p=0;
	for(int i=head[x];i;i=nxt[i])
	{
		if(book[ver[i]]) continue;
		top=0;
		dis[ver[i]]=w[i];
		dfs(ver[i],x);

		for(int j=top;j>0;j--)
		{
			for(int k=1;k<=m;k++)
			{
				if(query[k]>=stac[j]&&query[k]-stac[j]<=10000000)
				{
					exist[k]|=judge[query[k]-stac[j]];
				}
			}
		}
		for(int j=top;j>0;j--)
		{
			change[++p]=stac[j];
			judge[stac[j]]=1;
		}
	}
	for(int i=1;i<=p;i++) judge[change[i]]=0;
}
  • \(line6\sim11\):对于重心,扫描每一棵子树。
  • \(line13\sim22\):计算在当前子树中选一个点时,前面的子树中是否正好有一条路径长度等于某个询问。这里用\(judge_i\)表示之前的子树中是否有一条长度为\(i\)的路径。
  • \(line23\sim28\):将当前子树也加入\(judge\)中,它就成为了“之前的子树”了。用\(change\)储存修改过的\(judge\),方便之后清空\(judge\)
  • \(line29\):撤销存储的\(judge\)。这里不能用\(memset\),不然T飞飞哟。

Code

/**************************************************************
 * Problem: 2634
 * Author: Vanilla_chan
 * Date: 20210305
**************************************************************/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<queue>
#include<vector>
#include<limits.h>
#define IL inline
#define re register
#define LL long long
#define ULL unsigned long long
#ifdef TH
#define debug printf("Now is %d\n",__LINE__);
#else
#define debug 
#endif
#ifdef ONLINE_JUDGE
char buf[1<<23],* p1=buf,* p2=buf,obuf[1<<23],* O=obuf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
#endif
using namespace std;

template<class T>inline void read(T& x)
{
	char ch=getchar();
	int fu;
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)) { x=x*10+ch-'0';ch=getchar(); }
	x*=fu;
}
inline int read()
{
	int x=0,fu=1;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)) { x=x*10+ch-'0';ch=getchar(); }
	return x*fu;
}
int G[55];
template<class T>inline void write(T x)
{
	int g=0;
	if(x<0) x=-x,putchar('-');
	do { G[++g]=x%10;x/=10; } while(x);
	for(int i=g;i>=1;--i)putchar('0'+G[i]);putchar('\n');
}
#define N 20010
int n;
int head[N],ver[N<<1],nxt[N<<1],w[N<<1];
int cnt;
void insert(int x,int y,int z)
{
	nxt[++cnt]=head[x];
	head[x]=cnt;
	ver[cnt]=y;
	w[cnt]=z;

	nxt[++cnt]=head[y];
	head[y]=cnt;
	ver[cnt]=x;
	w[cnt]=z;
}
int gcd(int a,int b)
{
	if(!b) return a;
	return gcd(b,a%b);
}
int book[N];
int p[3];
int sum;
int sze[N],dp[N],root;
void calcG(int x,int f)
{
	sze[x]=1;
	dp[x]=0;
	for(int i=head[x];i;i=nxt[i])
	{
		if(ver[i]==f||book[ver[i]]) continue;
		calcG(ver[i],x);
		sze[x]+=sze[ver[i]];
		dp[x]=max(dp[x],sze[ver[i]]);
	}
	dp[x]=max(dp[x],sum-sze[x]);
	if(dp[x]<dp[root]) root=x;
}
int dis[N];
void dfs(int x,int f)
{
	p[dis[x]]++;
	for(int i=head[x];i;i=nxt[i])
	{
		if(ver[i]==f||book[ver[i]]) continue;
		dis[ver[i]]=dis[x]+w[i];
		dis[ver[i]]%=3;
		dfs(ver[i],x);
	}
}
int calc(int x,int w)
{
	p[0]=p[1]=p[2]=0;
	dis[x]=w%3;
	dfs(x,0);
	return p[0]*p[0]+p[1]*p[2]*2;
}
LL ans;
void solve(int x)
{
	ans+=calc(x,0);
	book[x]=1;
	for(int i=head[x];i;i=nxt[i])
	{
		if(book[ver[i]]) continue;
		ans-=calc(ver[i],w[i]);
		dp[root=0]=100000;
		sum=sze[ver[i]];
		calcG(ver[i],0);
		solve(root);
	}
}
int main()
{
	n=read();
	for(int i=1,a,b,c;i<n;i++)
	{
		a=read();
		b=read();
		c=read();
		insert(a,b,c);
	}
	dp[root=0]=sum=n;
	calcG(1,0);
	solve(root);
	int g=gcd(ans,n*n);
	cout<<ans/g<<"/"<<n*n/g<<endl;
	return 0;
}

注意

需要注意的是,虽然路径的总长度会达到\(wn=10^8\)级别,但是\(k\le10^7\),否则数组\(judge\)也会开不下。这里要当路径长度超过\(10^7\)时及时停止搜索及计算贡献,否则会RE。

具体的可以看这里:对于本题数据的一些说明

posted @ 2021-03-05 17:04  Vanilla_chan  阅读(118)  评论(0编辑  收藏  举报