点分治与动态点分治
前言
点分治一般是用于解决树上路径问题。
前置知识
树的重心:把重心这个点割掉后,使所形成的最大的联通块大小最小的点。
可以证明重心子树的大小最大不会超过 \(n\over 2\)
重心可以通过 \(dfs\) 一遍求出。
//maxsiz[x] 表示割掉点x后所形成的的最大的联通块的大小
void dfs(int x,int fa)
{
siz[x] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa) continue;
dfs(to,x);
siz[x] += siz[to];
max_siz[x] = max(max_siz[x],siz[to]);
}
max_siz[x] = max(max_siz[x],n-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}
点分治
先来看到例题吧
给定一棵树和一个整数 \(k\),求树上边数等于 \(k\) 的路径有多少条
我们的暴力做法就是枚举每条两个点,然后在判断他们两个的距离是否为 \(k\)
大概是 O(\(n^3\)) 的复杂度,优化一下的话可以跑到 O(\(n^2 log n\))
这 \(n\) 的范围一大,还是会 \(TLE\), 考虑优化一下复杂度 。
我们现在主要想解决的是在以 \(s\) 为根的子树中符合条件的路径个数。
不难发现路径一共可以分为三类:
情况一
从 \(s\) 出发到他的子树中一个点 \(t\) 所形成的路径, 如图中的黄色路径:。
这个很好统计,可以直接由 \(s\) \(dfs\) 一遍即可, 复杂度 \(O(m)\) 。( \(m\) 为路径个数)
情况二:
不在 $s $ 的同一个子树中的两个点 \(u,v\) 所形成的路径,如图。
显然 \(u-v\) 的路径是肯定要经过点 \(s\) 的,那么 \(u-v\) 的路径也就可以拆成 \(u-s\) 和 \(s-v\) 的两条路径。
这两条路径我们在解决情况 \(1\) 的时候已经求出来了,剩下的就是考虑怎么把他们拼接起来。
设 \(d_u\) 表示 \(u\) 到 \(s\) 的距离,我们现在要解决的是 \(d_u + d_v = k\) 且 \(u\) 和 \(v\) 不在 \(s\) 的同一颗子树中的情况。
我们可以把 \(d_u\) 按从小到大排一下序,根据单调性,利用双指针就可以很好的解决这个问题。
但我们还要注意的是,需要排除两条在同一颗子树中的路径的干扰。
具体的做法就是对每一条路径记录他位于 \(s\) 的那一颗子树中。
这个可以和 \(d_u\) 一起在情况 \(1\) 的 \(dfs\) 中一并求出来。
假设路径条数为 \(m\) ,那么排序的复杂度为 \(O(mlogm)\), 双指针的复杂度为 \(O(m)\), 所以总的复杂度为 \(O(mlogm)\)
那么第二种情况我们就解决出来了。
情况三:
位于 \(s\) 的同一颗子树中的 \(u,v\) 两点形成的路径,如图:
这个显然是当前问题的子问题,递归继续求解即可。
三种情况我们已经考虑完了,现在分析一下时间复杂度的问题。
假设递归的深度为 \(k\), 每做一次的复杂度最坏为 \(O(nlogn)\) (主要来自于情况2的排序)。
那么总的时间复杂度就是 \(O(knlogn)\), 所以为了保证复杂度,我们要使递归深度尽可能的小。
根据我们上面提到的关于重心的知识,每次分治的时候选取子树的重心,这样可以保证递归深度为 \(logn\)。
所以总的复杂度最坏为 \(O(nlog^2n)\) (实际上是很难达到这个上界的)
在做点分治的时候,我们需要把两个子树的信息合并,我们暴力合并的复杂度过高,有的情况下会使用启发式合并的方法来合并。
一般点分治的题套路都是一样的,都是分治递归求解,唯一的不同点就在于怎么合并子树的信息。
解决了合并子树信息这个问题,剩下的就是套模板的事情了。
例题代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e4+10;
int n,m,tot,cnt,sum_siz,root,u,v,w;
int head[N],siz[N],max_siz[N],dis[N],k[N];
bool vis[N],ans[N];
struct bian
{
int to,net,w;
}e[N<<1];
struct node
{
int d,wh;
node() {};
node(int x, int y){d = x; wh = y;}
}a[N];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
return s * w;
}
void add(int x,int y,int w)
{
e[++tot].w = w;
e[tot].to = y;
e[tot].net = head[x];
head[x] = tot;
}
bool comp(node a,node b)
{
return a.d < b.d;
}
void get_root(int x,int fa)//找重心
{
max_siz[x] = 0; siz[x] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa || vis[to]) continue;
get_root(to,x);
siz[x] += siz[to];
max_siz[x] = max(max_siz[x],siz[to]);
}
max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}
void get_dis(int x,int fa,int who)//找到重心的距离, who 记录他是谁的子树
{
a[++cnt] = node(dis[x],who);
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa || vis[to]) continue;
dis[to] = dis[x] + e[i].w;
get_dis(to,x,who);
}
}
int search(int d)
{
int L = 1, R = cnt, res = 0;
while(L <= R)
{
int mid = (L+R)>>1;
if(a[mid].d >= d)
{
res = mid;
R = mid - 1;
}
else L = mid + 1;
}
return res;
}
void calc(int x,int d)
{
dis[x] = 0; cnt = 0;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(vis[to]) continue;
dis[to] = dis[x] + e[i].w;
get_dis(to,x,to);
}
a[++cnt] = node(0,0);
sort(a + 1, a + cnt + 1, comp);//排一下序
for(int i = 1; i <= m; i++)
{
if(ans[i]) continue;
int l = 1, r = cnt;
while(l <= cnt && a[l].d + a[r].d < k[i]) l++;
while(l <= cnt && !ans[i])
{
if(k[i] - a[l].d < a[l].d) break;
int id = search(k[i] - a[l].d);
while(a[id].d + a[l].d == k[i] && a[id].wh == a[l].wh) id++;
if(a[l].d + a[id].d == k[i]) ans[i] = 1;
l++;
}
}
}
void slove(int x)
{
calc(x,0); vis[x] = 1;//先算 x 点的贡献
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(vis[to]) continue;
max_siz[0] = n; sum_siz = siz[to]; root = 0;
get_root(to,0); slove(root);//递归处理子树
}
}
int main()
{
n = read(); m = read();
for(int i = 1; i <= n-1; i++)
{
u = read(); v = read(); w = read();
add(u,v,w); add(v,u,w);
}
for(int i = 1; i <= m; i++) k[i] = read();
max_siz[0] = sum_siz = n; root = 0;
get_root(1,0); slove(root);//找到一开始整颗树的重心
for(int i = 1; i <= m; i++)
{
if(ans[i]) puts("AYE");
else puts("NAY");
}
return 0;
}
例2: P4178 Tree
题目描述
给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量。
和模板题差不多,只要在合并子树的时候稍微改一下即可。
code:
#pragma GCC optimize(2)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 4e4+10;
int n,m,tot,cnt,root,sum_siz,u,v,w,k,ans;
int head[N],siz[N],max_siz[N],dis[N],a[100010];
bool vis[N];
struct node
{
int to,net,w;
}e[N<<1];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
return s * w;
}
void add(int x,int y,int w)
{
e[++tot].w = w;
e[tot].to = y;
e[tot].net = head[x];
head[x] = tot;
}
void get_root(int x,int fa)
{
max_siz[x] = 0; siz[x] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa || vis[to]) continue;
get_root(to,x);
siz[x] += siz[to];
max_siz[x] = max(max_siz[x],siz[to]);
}
max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}
void get_dis(int x,int fa)
{
a[++cnt] = dis[x];
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa || vis[to]) continue;
dis[to] = dis[x] + e[i].w;
get_dis(to,x);
}
}
int calc(int x,int d)
{
int res = 0;
dis[x] = d; cnt = 0;
a[++cnt] = dis[x];
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(vis[to]) continue;
dis[to] = dis[x] + e[i].w;
get_dis(to,x);
}
sort(a+1,a+cnt+1);
int L = 1, R = cnt;
while(L <= R)
{
if(a[L] + a[R] <= k)
{
res += R-L;
L++;
}
else R--;
}
return res;
}
void slove(int x)
{
ans += calc(x,0);
vis[x] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(vis[to]) continue;
ans -= calc(to,e[i].w);
max_siz[0] = n; sum_siz = siz[to]; root = 0;
get_root(to,0); slove(root);
}
}
int main()
{
n = read();
for(int i = 1; i <= n-1; i++)
{
u = read(); v = read(); w = read();
add(u,v,w); add(v,u,w);
}
k = read();
max_siz[0] = sum_siz = n; root = 0;
get_root(1,0); slove(root);
printf("%d\n",ans);
return 0;
}
动态点分治
动态点分治又叫点分树(个人觉得点分树更形象一些),主要解决的树上的一下带修改问题。
动态点分治还是基于点分治的那套理论,每次选重心分治。
但考虑到修改操作,我们不可能每次修改都做一遍。
我们可以建点分树来解决这个问题,具体来说就是:
设当前的分治中心为 \(x\), 由他子树中的重心 \(y\) 向 \(x\) 连边,不难发现这样会构成一棵树,这棵树也被叫做点分树。
不难发现当我们要修改 \(x\) 这个节点的信息的时候,发现他会影响到的是 \(x\) 到根节点路径上的点的信息。
查询的话同样会用到 \(x\) 到根节点路径上点的信息。
由于我们树的高度不超过 \(logn\), 所以直接暴力修改查询即可。
我们就可以拿数据结构来维护每个点的信息,巴拉巴拉。
代码,咕咕咕