点分治小结
点分治小结
说在前面
本文面向个人。
基本步骤
一般点分治统计答案时有两种形式。
- 容斥
- 只计算之前的子树对之后的子树的贡献
不同的题目有不同的适宜的方法,需要看题目。
容斥
找到子树的重心。从重心开始分治。
首先计算这一棵子树中的所有的答案。无论其是否经过当前的重心。
但是这样计算时,我们也会将在同一颗子树中的两个点统计进去啊!之后分治递归的时候,不就会重复计算了吗?
所以这里再一次减去以重心为根每一棵子树中的答案。
然后进入每一棵子树中,继续找重心,计算,容斥,分治……
只计算之前的子树对之后的子树的贡献
其实我们在每次确定一个子树之后,想要计算的是它的跨过重心的点对贡献。那么有没有办法使得只会统计跨重心的点对贡献呢?当然有。
首先像上面一样确定好子树及其重心之后开始枚举其子树(当然是还未被标记的),对其进行\(\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。
具体的可以看这里:对于本题数据的一些说明。