点分治,是处理树上路径的一个极好的工具。
一般如果需要大规模处理树上路径,点分治是一个不错的选择。
这里我就来讲一讲我自己对于点分治的一点理解和感悟(帮助新手入坑……)
现在就开始吧!
1.点分治的基本思想
点分治,顾名思义就是基于树上的节点进行分治。
如果我们在深入一点呢?对于点的拆开其实就是对于树的拆开。
所以我认为点分治的本质其实是将一棵树拆分成许多棵子树处理,并不断进行。
这应该也是点分治的精髓。
2.分治点的选择
既然我们要将一个点进行分治,那么选点肯定最首要的。
思考下面一个问题:
如果树退化为一个链,
我们选取链首作为分治点,理论时间复杂度?
而如果选择链的中心,它的理论时间复杂度又是多少?
答案其实还是挺简单的。
选择链首:O( n )
选择链心:O( logn )
通过这个例子,我们不难发现:
如果选点后左右子树越大,递归层数越多,时间越慢,反之则越快。
所以我们的选点标准就出来了,而我们把有这个性质的点叫做:树的重心!
3.重心的O( n )求解<求解分治点>
正式一点定义一下重心:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,
先贴出找重心的代码(各位结合着理解下面的内容):
void getroot(int u,int fa)
{
sim[u] = 1; mxson[u] = 0;
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v]||v == fa)continue;
getroot(v,u);
sim[u] = sim[u] + sim[v];
mxson[u] = max(mxson[u],sim[v]);
}
mxson[u] = max(mxson[u],Smer - sim[u]);
if(mxson[u]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
其中sim[i]表示以i为根节点的子树的节点数量(即子树大小),mxson[i],表示i节点为根的子树中的最大子树。
Smer为这棵树(当前处理树)的大小。MX记录当前已经找到的最小 最大子树值。
那么究竟是怎么找的呢?其实也蛮简单的。
就是暴力(当然不是正真意义上的暴力啦,毕竟人家是O(n)的)搞出所有子树的大小,找出最大子树,与原来答案比较,更新答案。
但在这里要特别注意一下这一句:
mxson[u] = max(mxson[u],Smer - sim[u]);
这一句其实使用到了容斥原理的思想(后面还会用到),求出的是经过父亲节点的子树大小。
读者们细细体会一下代码,这部分应该还是很好懂~(≧▽≦)/~啦啦啦。
4.点分治的实现
有了前面的基础知识我们就可以正式进入点分治的探讨了。
点分治是用递归实现的,先给出代码:
void Divide(int tr)
{
ans = ans + solve(tr,0);
vis[tr] = true;
for(int i = head[tr];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v])continue;
ans = ans - solve(v,t[i].len);
Smer = sim[v]; root = 0;
MX = INF; getroot(v,0);
Divide(root);
}
return;
}
我们一点一点的分析。
首先对于当前点,求解经过此点的所有贡献(solve)。
ans = ans + solve(tr,0);
把当前点标记(防止陷入死循环)。
vis[tr] = true;
然后进行分治,分别处理此点的每一棵子树。
for(int i = head[tr];i;i = t[i].next)
容斥原理去除非法答案。
ans = ans - solve(v,t[i].len);
这里要着重讲一下。
对于以下这棵树:
显然A点是它的重心。
我们假设现在分治到了A点(当前点为A)
我们一开始求解贡献时,会有以下路径被处理出来:
A—>A
A—>B
A—>B—>C
A—>B—>D
A—>E
A—>E—>F (按照先序遍历顺序罗列)
那么我们在合并答案是会将上述6条路径两两进行合并。
这是注意到:
合并A—>B—>C 和 A—>B—>D 肯定是不合法的!!
因为这并不是一条树上(简单)路径,出现了重边,我们要想办法把这种情况处理掉。
处理方法很简单,减去每个子树的单独贡献。
例如对于以B为根的子树,就会减去:
B—>B
B—>C
B—>D
这三条路径组合的贡献
读者可能会有疑问,这与上面的6条路径并不一样啊。
我们再回过头来看一看这两句代码:
ans = ans + solve(tr,0);
ans = ans - solve(v,t[i].len);
注意到了吧,solve函数的第二个初始值并不相同。
我们在处理子树时,将初始长度设为连接边长,这样做就相当于个子树的每个组合都加上了A—>的路径,从而变得与上面一样。
个人认为这是点分治一个极其重要的地方,读者们一定要理解清楚。
重设当前总树大小,寻找新的分治点(重心)
Smer = sim[v]; root = 0;
MX = INF; getroot(v,0);
递归处理新的分治点
Divide(root);
5.具体的习题练习:
【例题1】【poj1741】tree
给一颗n个节点的树,每条边上有一个距离v。定义d(u,v)为u到v的最小距离。给定k值,求有多少点对(u,v)使u到v的距离小于等于k。
点分治最经典的题目了(笔者瞎猜这是不是点分治的起源)。
用上文的模板直接套,求出所有点对的距离,注意合并答案时用二分保证复杂度。
#include
#include
#include
#include
#include
#include
#define maxn 10002
#define INF 2147483646
#define ll long long
using namespace std;
inline int gi()
{
int date = 0,m = 1; char ch = 0;
while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();
if(ch=='-'){m = -1; ch = getchar();}
while(ch>='0' && ch<='9')
{
date = date*10+ch-'0';
ch = getchar();
}return date*m;
}
inline void write(ll qw)
{
if(qw<0){putchar('-'); qw = -qw;}
if(qw>9)write(qw/10);
putchar(qw%10+'0');
}
struct node{int to,next,len;}t[2*maxn+5];
int head[maxn+5];
int sim[maxn+5],mxson[maxn+5];
bool vis[maxn+5];
int MX,root,dis[maxn+5],summar;
ll ans;
int n,k,cnt,S;
void addedge(int u,int v,int l)
{
cnt ++;
t[cnt].to = v;
t[cnt].next = head[u];
t[cnt].len = l;
head[u] = cnt;
return;
}
void getroot(int u,int fa)
{
sim[u] = 1; mxson[u] = 0;
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(v == fa || vis[v])continue;
getroot(v,u);
sim[u] = sim[u] + sim[v];
mxson[u] = max(sim[v],mxson[u]);
}
mxson[u] = max(mxson[u],S-sim[u]);
if(mxson[u]void getdis(int u,int fa,int dist)
{
dis[++summar] = dist;
for(int i = head[u];i;i=t[i].next)
{
int v = t[i].to;
if(vis[v]||v == fa)continue;
getdis(v,u,dist+t[i].len);
}
return;
}
int consolate(int sta,int len1)
{
summar = 0;
memset(dis,0,sizeof(dis));
getdis(sta,0,len1);
sort(dis+1,dis+summar+1);
int L = 1,R = summar,tep=0;
while(L<=R)
{
if(dis[R] + dis[L]<=k){tep = tep + R - L; L++;}
else R--;
}
return tep;
}
void Divide(int tr)
{
ans = ans + consolate(tr,0);
vis[tr] = true;
for(int i = head[tr];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v])continue;
ans = ans - consolate(v,t[i].len);
S = sim[v]; root = 0;
MX = INF; getroot(v,0);
Divide(root);
}
return;
}
int main()
{
freopen("Tree.in","r",stdin);
int u,v,l;
while(1)
{
n = gi(); k = gi();
if(n == 0 && k == 0)break;
cnt = 0; memset(head,0,sizeof(head));
for(int i = 1; i <= n - 1; i ++)
{
u = gi(); v = gi(); l = gi();
addedge(u,v,l); addedge(v,u,l);
}
ans = 0;
memset(vis,0,sizeof(vis));
MX = INF ; S = n; getroot(1,0);
Divide(root);
write(ans);putchar('\n');
}
return 0;
}
【例题2】【luogu P2634】聪聪可可
聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
计算答案的方法:利用点分治在计算所有路径长度,把路径长度对3取模,用t[0],t[1],t[2]分别记录模为0、1、2的情况,那么显然答案就是t[1]*t[2]*2+t[0]*t[0]。
#include
#include
#include
#include
#include
#include
#define maxn 20002
#define INF 2147483646
#define mod 3
#define ll long long
using namespace std;
inline int gi()
{
int date = 0,m = 1; char ch = 0;
while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();
if(ch=='-'){m = -1; ch = getchar();}
while(ch>='0' && ch<='9')
{
date = date*10+ch-'0';
ch = getchar();
}return date*m;
}
inline void write(ll qw)
{
if(qw<0){putchar('-'); qw = -qw;}
if(qw>9)write(qw/10);
putchar(qw%10+'0');
}
struct node{int to,next,len;}t[2*maxn+5];
int head[maxn+5],cnt;
int sim[maxn+5],mxson[maxn+5],sum[4];
ll ans;
int Sumer,MX,dec1,root,n;
bool vis[maxn+5];
void addedge(int u,int v,int l)
{
cnt ++;
t[cnt].to = v;
t[cnt].next = head[u];
t[cnt].len = l;
head[u] = cnt;
return;
}
void getroot(int u,int fa)
{
sim[u] = 1;mxson[u] = 0;
for(int i = head[u];i;i = t[i].next)
{
int v= t[i].to;
if(v == fa || vis[v])continue;
getroot(v,u);
sim[u] = sim[u] + sim[v];
mxson[u] = max(mxson[u],sim[v]);
}
mxson[u] = max(mxson[u],Sumer - sim[u]);
if(mxson[u]int u,int fa,int lon)
{
sum[lon%mod]++;
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v]||v == fa)continue;
query(v,u,(lon+t[i].len)%mod);
}return;
}
ll solve(int st,int prim_len)
{
sum[0] = sum[1] = sum[2] = 0;
query(st,0,prim_len);
ll tep = 2*sum[1]*sum[2] + sum[0]*(sum[0]-1) + sum[0];
return tep;
}
void Divide(int temproot)
{
ans = ans + solve(temproot,0);
vis[temproot] = true;
for(int i = head[temproot];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v])continue;
ans = ans - solve(v,t[i].len);
MX = INF; root = 0; Sumer = sim[v];
getroot(v,0);
Divide(root);
}
return;
}
void gcd(ll a,ll b)
{
if(b == 0)dec1 = a;
else gcd(b,a%b);
}
int main()
{
freopen("game.in","r",stdin);
n = gi();
for(int i = 1; i <= n-1 ; i ++)
{
int u = gi(),v= gi(),l = gi()%mod;
addedge(u,v,l);addedge(v,u,l);
}
MX = INF; root = 0;
Sumer = n; ans = 0;
getroot(1,0);
Divide(root);
ll tep1 = n*n; gcd(ans,tep1);
write(ans/dec1); putchar('/'); write(tep1/dec1);
return 0;
}
【例题3】【luogu P3806】点分治1
给定一棵树,询问长度为k的树上路径是否存在。
对于每个k每行输出一个答案,存在输出“AYE”,否则输出”NAY”。(k<=10000000)
注意到k的大小在数组可开范围之内,开一个数组sum[i]记录长度为i的路径条数。
点分治计算所有路径长度时注意容斥原理去重,然后在线直接O(1)回答询问即可。
#include
#include
#include
#include
#include
#include
#define maxn 100100
#define INF 2147483646
#define ll long long
using namespace std;
inline int gi()
{
int date = 0,m = 1; char ch = 0;
while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();
if(ch=='-'){m = -1; ch = getchar();}
while(ch>='0' && ch<='9')
{
date = date*10+ch-'0';
ch = getchar();
}return date*m;
}
int sum[10000010];
struct node{int to,next,len;}t[maxn+5];
int head[maxn+5],cnt;
int sim[maxn+5],mxson[maxn+5],dis[maxn+5];
int n,m,MX,root,sumary,Smer;
bool vis[maxn+5];
void addedge(int s,int f,int l)
{
cnt ++;
t[cnt].to = f;
t[cnt].next = head[s];
t[cnt].len = l;
head[s] = cnt;
return;
}
void getroot(int u,int fa)
{
sim[u] = 1; mxson[u] = 0;
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v]||v == fa)continue;
getroot(v,u);
sim[u] = sim[u] + sim[v];
mxson[u] = max(mxson[u],sim[v]);
}
mxson[u] = max(mxson[u],Smer - sim[u]);
if(mxson[u]void query(int u,int fa,int lon)
{
dis[++sumary] = lon;
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v]||v == fa)continue;
query(v,u,lon+t[i].len);
}
return;
}
void solve(int st,int prim,int jud)
{
sumary = 0;
query(st,0,prim);
if(jud == 1)
{
for(int i = 1;i <= sumary-1; i ++)
for(int j = i+1;j <= sumary; j ++)
sum[dis[i]+dis[j]]++;
}
else if(jud == 0)
{
for(int i = 1; i <= sumary-1; i ++)
for(int j = i+1; j <= sumary; j ++)
sum[dis[i]+dis[j]]--;
}return;
}
void divide(int tr)
{
vis[tr] = true;
solve(tr,0,1);
for(int i = head[tr];i;i = t[i].next)
{
int v = t[i].to;
if(vis[v])continue;
solve(v,t[i].len,0);
MX = INF; root = 0; Smer = sim[v];
getroot(v,0);
divide(root);
}
return;
}
void give_ans()
{
for(int i = 1; i <= m ; i ++)
{
int k = gi();
if(sum[k])printf("AYE\n");
else printf("NAY\n");
}
return;
}
int main()
{
freopen("divide.in","r",stdin);
n = gi(); m = gi(); cnt = 0;
for(int i = 1; i <= n-1 ; i++)
{
int x = gi(),y = gi(),l = gi();
addedge(x,y,l); addedge(y,x,l);
}
root = 0; MX = INF; Smer = n;
getroot(1,0);
divide(root);
give_ans();
return 0;
}
总之,点分治其实就是一种高级的暴力(至少我是这么觉得的),它可以用O(nlogn)的时间求解出需要O(n^2)~O(n^3)才能解决的路径问题。点分治的应用还有很多,更深层次的东西还需要读者自己去理解体会,我这里也只是抛砖引玉,希望能够帮助到各位读者。