一.基本思想
点分治即是在树上使用分治算法以更高效的求解 树上路径 类问题。基本的时间复杂度为
O
(
n
l
o
g
n
)
O(nlog_n)
O(nlogn)。对于一棵已经 确定了根 的树, 那么所有的路径即分为两类: 1.经过根的路径 2.不经过根的路径。 对于这些 不经过根 的路径, 显然 是在当前树的子树中, 我们需要做的只是 求出第一类路径 后 将根节点删除,在递归子树求解第二类根。
二.基本步骤
1.求重心最为当前树的根
这是点分治的复杂度降低 的 关键, 如果我们每次选定当前树的的重心作为根, 那么与重心相连的最大子树的大小一定 不会超过该子树大小的一半 (可以用 反证法 去思考)。那么最多递归 l o g n log_n logn层, 对于每一层而言的复杂度是 O ( n ) O(n) O(n), 则总复杂度为 O ( n l o g n ) O(nlog_n) O(nlogn)。
下面是求重心的代码:
void get_root(int x, int fa){
cnt[x] = 1, Maxn[x] = 0;//cnt[x] 表示以x为根的子树的大小, Maxn[x]表示与x相连的最大连通块大小
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(vis[v] || v == fa) continue;
get_root(v, x);
cnt[x] += cnt[v];
Maxn[x] = max(Maxn[x], cnt[v]);
}
Maxn[x] = max(Maxn[x], all - cnt[x]);
if(Maxn[x] < Maxn[root]) root = x;
}
2.求以重心为总根(当前求解的连通块的总根), 各个子树的大小
这一步是为了后期求all 的值。
代码:
void get_size(int x, int fa){
cnt[x] = 1;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(vis[v] || v == fa) continue;
get_size(v, x);
cnt[x] += cnt[v];
}
}
3.求解经过当前root的路径
针对不同的题目会有 不同的处理方法, 但有一个共同的框架: 一条 经过根的路径 是两条在根的不同子树的点到根的路径 拼接得到。
<1>.依次递归每一个子树,并利用 前面子树的点到根的路径信息 与 当前子树中的点到根的路径信息 更新答案。
<2>.加入当前子树中的点到根的所有路径信息。
代码板子:
void solve(int rt){//处理根为rt,经过rt的合法最优解
vis[rt] = 1;
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
....//给v节点附上一个初始信息
calc(v, 0);//递归解决子树v
change(v, n + 1, 1);//加上子树v中的信息
}
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
change(v, n + 1, 0);//回溯清空
}
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
root = 0;
all = cnt[v];
get_root(v, n + 1);//找v所在联通块的中心
get_size(root, n + 1);
solve(root);//分治解决
}
}
三.例题
1.poj1741—tree
题目大意:给定一棵 n n n 个节点的树,每条边有边权,求出树上两点距离小于等于 k k k 的点对数量。
分析:考虑点分治, 开数组存下当前树中所有节点到根的路径长度, 排序后 双指针 扫描统计答案。 但是这样有可能会统计到在 同一棵子树的两个节点 之间的不合法路径, 需要考虑 容斥 减掉这部分不合法方案。 具体做法可以 给dis[v]赋上一个初值为 根与v连边的边权值,做一遍统计后减去所有 "合法"方案。(这里的 合法与上文提到的不合法路径是 一摸一样 的)。至此,问题得到解决。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 4e4 + 10;
int n, u, v, w, k, head[N], tot, T, cnt[N], f[N], now, INF = 1e9, d[N], dis[N], dcnt, root, ans;//cnt[i] 表示以i为根的子树的大小 f[i] 表示与i相连的联通块的最大大小
bool vis[N];//标记一个点是否被删除
struct edge{
int v, last, w;
}E[N * 2];
void add(int u, int v, int w){
E[++tot].v = v;
E[tot].w = w;
E[tot].last = head[u];
head[u] = tot;
}
void get_size(int x, int fa){
cnt[x] = 1;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
get_size(v, x);
cnt[x] += cnt[v];
}
}
void get_root(int x, int fa){//找根
cnt[x] = 1, f[x] = 0 ;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;//父亲和已经删过的的点都不进入
get_root(v, x);
cnt[x] += cnt[v];
f[x] = max(f[x], cnt[v]);
}
f[x] = max(f[x], now - cnt[x]);
if( f[x] < f[root] ) root = x;
}
void get_dis(int x, int fa){
if(d[x] > k) return ;
dis[++dcnt] = d[x];
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
d[v] = d[x] + E[i].w;
get_dis(v, x);
}
}
int count(int x, int w){
d[x] = w, dcnt = 0;
get_dis(x, 0);
sort(dis + 1, dis + dcnt + 1);
int l = 1, r = dcnt, sum = 0;
while(l < r){//双指针
if(dis[l] + dis[r] <= k) sum += r - l, l++;//r - l 对都可以
else r--;
}
return sum;
}
void solve(int rt){//处理当前的根
ans += count(rt, 0);//计算贡献
vis[rt] = 1;
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
ans -= count(v, E[i].w);//减去不合法方案
now = cnt[v], root = 0;
get_root(v, 0);//求解v所在的联通块
get_size(root, 0);
solve(root);
}
}
int main(){
while(scanf("%d%d", &n, &k), n, k){
memset(head, 0, sizeof(head));
memset(vis, 0, sizeof(vis));
tot = 0, root = ans = 0;
f[0] = INF;
now = n;
for(int i = 1; i < n; i++){
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
get_root(1, 0);//找到一个根
get_size(root, 0);//确定每个子树的大小
solve(root);
printf("%d\n", ans);
}
return 0;
}
2.[IOI2011]Race
题目大意:给一棵树,每条边有权。求一条简单路径,权值和等于 k k k ,且边的数量最小。
分析:注意到 k k k 的范围较小,考虑开桶。 设 M i n [ x ] Min[x] Min[x] 表示 到当前子树为止, 到根的边权为 x x x 的路径的最小边数量。 那么对于当前的路径,设它到根的边权和为 s u m sum sum,边数为 n u m num num, 则可以用 n u m + M i n [ k − s u m ] num + Min[k - sum] num+Min[k−sum] 更新 a n s ans ans。考虑完一棵子树后将它的信息加入即可。最后 初始化 M i n [ ] Min[] Min[],递归子连通块。 问题得到了解决。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
const int M = 1e6 + 10;
int read(){
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * f;
}
int n, k, u, v, w, tot, head[N], Min[M], cnt[N], f[N], INF = 1e9, all, root, ans, d[N], dep[N];//Min[i] 表示到当前根的距离为i的最短边数
bool vis[N], flag;
struct edge{
int v, last, w;
}E[N * 2];
void add(int u, int v, int w){
E[++tot].v = v;
E[tot].w = w;
E[tot].last = head[u];
head[u] = tot;
}
void get_root(int x, int fa){
cnt[x] = 1, f[x] = 0;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
get_root(v, x);
cnt[x] += cnt[v];
f[x] = max(f[x], cnt[v]);
}
f[x] = max(f[x], all - cnt[x]);
if(f[x] < f[root]) root = x;
}
void get_size(int x, int fa){
cnt[x] = 1;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
get_size(v, x);
cnt[x] += cnt[v];
}
}
void get_dis(int x, int fa){
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
dep[v] = dep[x] + 1;
d[v] = d[x] + E[i].w;
get_dis(v, x);
}
}
void calc(int x, int fa){
if(d[x] > k) return ;
ans = min(ans, dep[x] + Min[k - d[x]]);//更新答案
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
calc(v, x);
}
}
void change(int x, int fa, int tp){//tp -> 0 赋初值, tp -> 1 加入信息
if(d[x] > k) return ;
if(tp) Min[d[x]] = min(Min[d[x]], dep[x]);
else Min[d[x]] = n;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
change(v, x, tp);
}
}
void solve(int rt){//处理根为rt,经过rt的合法最优解
Min[0] = 0, vis[rt] = 1;
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
d[v] = E[i].w, dep[v] = 1;//附上初值
get_dis(v, n + 1);
calc(v, n + 1);
change(v, n + 1, 1);//加上
}
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
change(v, n + 1, 0);//附回初值
}
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
root = n + 1;
all = cnt[v];
get_root(v, n + 1);//找v所在联通块的中心
get_size(root, n + 1);
solve(root);
}
}
int main(){
n = read(), k = read();
ans = n;
for(int i = 1; i < n; i++){
u = read(), v = read(), w = read();
add(u, v, w);
add(v, u, w);
}
for(int i = 0; i <= k; i++) Min[i] = n;
all = n, f[n + 1] = INF, root = n + 1;//有0号节点,所以初始root赋值为n + 1
get_root(1, n + 1);
get_size(root, n + 1);
solve(root);
ans = (ans == n ? -1 : ans);
printf("%d\n", ans);
return 0;
}
3.usaco yinyang 【bzoj3697】采药人的路径
题目大意:给出一棵n个点的树,每条边为黑色或白色。问满足以下条件的路径条数:路径上存在一个不是端点的点,使得两端点到该点的路径上两种颜色的边数都相等。
分析:
由于点分治 所有的限制条件是基于两端节点到根的路径信息之间的关系, 对于此题而言,若一条经过根的路径是平衡的, 首先应满足两条拼接的路径中的黑白数量是和相等,其次要满足这条路径之中有一个休息站。因此我们不能仅仅考虑节点到根路径的边权和, 还要考虑是否存在休息站。 仅开一维是不够的,我们考虑开两维存储信息。 设
g
[
x
]
[
0
/
1
]
g[x][0/1]
g[x][0/1] 表示当前为止,已经处理过的子树中到根节点边权和为
x
x
x, 这个点到根节点之间 无/有 休息站的 点的数量。
f
[
x
]
[
0
/
1
]
f[x][0/1]
f[x][0/1] 表示当前子树中到根节点边权和为
x
x
x,这个点到根节点之间 无/有休息站的 点的数量。 那么设当前节点 到根节点 的边权和为
m
m
m。并且可以开一个桶
t
[
x
]
t[x]
t[x] 标记
m
m
m 是否出现过。 若
t
[
m
]
>
0
t[m] > 0
t[m]>0, 则当前节点到根至少可以找到一个 休息点, 可以令
f
[
m
]
[
1
]
+
+
f[m][1]++
f[m][1]++, 否则令
f
[
m
]
[
0
]
+
+
f[m][0]++
f[m][0]++, 统计完一棵子树后, 它对答案的贡献即为
∑
i
=
m
m
i
n
m
m
a
x
f
[
i
]
[
1
]
∗
g
[
−
i
]
[
1
]
+
f
[
i
]
[
1
]
∗
g
[
−
i
]
[
0
]
+
f
[
i
]
[
0
]
∗
g
[
i
]
[
1
]
\sum_{i = m_{min}}^{m_{max}}f[i][1] * g[-i][1] + f[i][1]*g[-i][0]+f[i][0]*g[i][1]
∑i=mminmmaxf[i][1]∗g[−i][1]+f[i][1]∗g[−i][0]+f[i][0]∗g[i][1],统计完后将
f
[
x
]
[
0
/
1
]
f[x][0/1]
f[x][0/1] 加入
g
g
g 数组中。
代码:
#include<bits/stdc++.h>//路径有特殊限制, 可尝试多开一维
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int read(){
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * f;
}
int INF = 1e9, t[N * 2];
int n, dis[N], u, v, w, head[N], tot, root, cnt[N], Maxn[N], all, max_deep, mx;
LL f[N * 2][2], g[N * 2][2], ans;//f[i][0/1]->当前子树到root距离为i并且无/有休息站, g[i][0/1]->到当前位置到root距离为i无/有休息站
bool vis[N];
struct edge{
int v, last, w;
}E[N * 2];
void add(int u, int v, int w){
E[++tot].v = v;
E[tot].w = w;
E[tot].last = head[u];
head[u] = tot;
}
void get_root(int x, int fa){//找x所在连通块的中心
cnt[x] = 1, Maxn[x] = 0;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
get_root(v, x);
cnt[x] += cnt[v];
Maxn[x] = max(Maxn[x], cnt[v]);
}
Maxn[x] = max(Maxn[x], all - cnt[x]);
if(Maxn[x] < Maxn[root]) root = x;
}
void get_size(int x, int fa){
cnt[x] = 1;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
get_size(v, x);
cnt[x] += cnt[v];
}
}
void get(int x, int fa){//t[]不可标记
max_deep = max(max_deep, abs(n - dis[x]));//求当前子树中的最大上下差值
if(t[dis[x]]) f[dis[x]][1]++;
else f[dis[x]][0]++;
t[dis[x]]++;//放下面!!!!!!!!!!!!
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(v == fa || vis[v]) continue;
dis[v] = dis[x] + E[i].w;
get(v, x);
}
t[dis[x]]--;//回溯时清空t数组\
}
void change(int max_deep){
for(int i = n - max_deep; i <= n + max_deep; i++){//将f的信息加入g并清空
g[i][0] += f[i][0];
g[i][1] += f[i][1];
f[i][0] = f[i][1] = 0;
}
}
void clear(int mx){//清空g数组
for(int i = n - mx; i <= n + mx; i++) g[i][0] = g[i][1] = 0, f[i][0] = f[i][1] = 0;
}
void solve(int rt){
vis[rt] = 1; g[n][0] = 1;//根自己本身的贡献
mx = 0;//mx表示处理当前根时的最大上下差值
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
dis[v] = n + E[i].w, max_deep = 1;//max_depp表示处理v时所涉及的最大上下差值 向右偏移n防止越界
get(v, 0);
mx = max(mx, max_deep);
ans = ans + (g[n][0] - (1LL * 1)) * f[n][0];//到根节点是正好平衡*另一边到根节点正好平衡 (-1) 是减去自己一个根的贡献
for(int j = n - max_deep; j <= n + max_deep; j++){//求答案
ans = ans + f[j][1] * g[2 * n - j][1] + f[j][1] * g[2 * n - j][0] + f[j][0] * g[2 * n - j][1];
}
change(max_deep);
}
clear(mx);
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
root = 0;
all = cnt[v];
get_root(v, 0);
get_size(root, 0);
solve(root);
}
}
int main(){
n = read();
for(int i = 1; i < n; i++){
u = read();
v = read();
w = read();
if(w == 0) w = -1;//将0赋值为-1, 技巧
add(u, v, w);
add(v, u, w);
}
Maxn[0] = INF, root = 0, all = n;
get_root(1, 0);
get_size(root, 0);
solve(root);
printf("%lld\n", ans);
return 0;
}
4.[国家集训队]聪聪可可
分析:求出所有边权和为三的倍数的路径( x x x -> y y y)的个数,仅需满足 (( ( d i s [ x (dis[x (dis[x -> r o o t ] ) root]) root])% 3 3 3 + + + ( d i s [ y (dis[y (dis[y -> r o o t ] ) root]) root])% 3 3 3)) % 3 3 3 = 0。类比 Race 开桶统计即可。
5.树分治例2[SPOJ1825]免费旅行II
题目大意:求一条树上路径的边权和最大值,满足这条路径经过的拥挤点不超过 K K K 个。
分析:与前面的题类似,设对于根到当前节点之间的路径已经经过了 m m m 个拥挤点,则需要求出 已经处理的经过的拥挤点不超过 k − m k - m k−m 的路径的最大路径和。开一个 树状数组 维护即可。
树状数组代码:
int lowbit(int x){
return x & -x;
}
LL ask(int x){//查询前缀最大
LL res = NNF;
for(; x; x -= lowbit(x)) res = max(res, c[x]);
res = max(res, c[0]);
return res;
}
void add(int x, LL y){//修改
if(x != 0) for(; x <= k; x += lowbit(x)) c[x] = max(c[x], y);
else c[0] = max(c[0], y);
}
清空:
void modify(int x){
if(x != 0) for(; x <= k; x += lowbit(x)) c[x] = NNF;
else c[0] = NNF;
}
6.路径规划
题目大意:给定一棵n个点的无根树,树上的边有一个权值,要求找出一条路径使得该路径权的最小值乘边权和最大
分析:由于 最小值可能在任意一条路径(拼接的两条)中产生,因此我们考虑 固定最小值。及对于当前处理的路径, 以它中的边权最小值作为拼接路径的最小值处理。那么我们只需查询 已经处理过的到根的路径中最小值大于当前最小值的路径和最大值。用 线段树 查询即可。 但线段树常数较大,注意到我们查询的是一个 后缀, 可以用 树状数组 实现。只需要将 原来查询和修改的枚举顺序调换即可。
代码:
#include<bits/stdc++.h>//查询和插入相反则为后缀
using namespace std;
typedef long long LL;
const int N = 3e5 + 10;
const int M = 1e6 + 10;
int read(){
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * f;
}
struct edge{
int v, last;
int w;
}E[N * 2];
stack< int > s;
int n, u, v, w, head[N], tot, cnt[N], Maxn[N], root, all, f[N], p, l[N];
int INF = 1e8;
int dis[N], Minn, w_max;
LL ans, sum, c[M];
bool vis[N];
void add(int u, int v, int w){
E[++tot].v = v;
E[tot].w = w;
E[tot].last = head[u];
head[u] = tot;
}
void get_root(int x, int fa){
cnt[x] = 1, Maxn[x] = 0;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(vis[v] || v == fa) continue;
get_root(v, x);
cnt[x] += cnt[v];
Maxn[x] = max(Maxn[x], cnt[v]);
}
Maxn[x] = max(Maxn[x], all - cnt[x]);
if(Maxn[x] < Maxn[root]) root = x;
}
void get_size(int x, int fa){
cnt[x] = 1;
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(vis[v] || v == fa) continue;
get_size(v, x);
cnt[x] += cnt[v];
}
}
int lowbit(int x){return x & -x;}
LL ask(int x){//查询
LL res = 0;
for(; x < M; x += lowbit(x)) res = max(res, c[x]);//查询后缀,每次加 lowbit(x)
return res;
}
void add(int x, LL y){//修改,每次减lowbit(x)
for(; x; x -= lowbit(x)) c[x] = max(c[x], y);
}
void del(int x){//赋初值
for(; x; x -= lowbit(x)) c[x] = 0;
}
void op(int x, int fa){//处理x这棵子树
LL tmax = ask(Minn);//查询
ans = max(ans, (1LL * (tmax + sum)) * 1LL * Minn);
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(vis[v] || v == fa) continue;
sum = sum + 1LL * E[i].w;
int t_min = Minn;
Minn = min(Minn, E[i].w);
op(v, x);
sum = sum - 1LL * E[i].w;
Minn = t_min;
}
}
void get(int x, int fa){//将x这棵子树贡献加上
add(Minn, sum);
s.push(Minn);
for(int i = head[x]; i; i = E[i].last){
int v = E[i].v;
if(vis[v] || v == fa) continue;
sum = sum + 1LL * E[i].w;
LL t_min = Minn;
Minn = min(Minn, E[i].w);
get(v, x);
sum = sum - 1LL * E[i].w;
Minn = t_min;
}
}
void solve(int rt){
vis[rt] = 1, p = 0;
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
f[++p] = v;
l[p] = E[i].w;
dis[v] = E[i].w, Minn = E[i].w, sum = 1LL * E[i].w;
op(v, 0);
sum = 1LL * E[i].w, Minn = E[i].w;
get(v, 0);
}
while(!s.empty()){
int Top = s.top();
s.pop();
del(Top);
}
for(int i = p; i >= 1; --i){
int v = f[i];
if(vis[v]) continue;
dis[v] = l[i], Minn = l[i], sum = 1LL * l[i];
op(v, 0);
sum = 1LL * l[i], Minn = l[i];
get(v, 0);
}
while(!s.empty()){
int Top = s.top();
s.pop();
del(Top);
}
for(int i = head[rt]; i; i = E[i].last){
int v = E[i].v;
if(vis[v]) continue;
root = 0, all = cnt[v];
get_root(v, 0);
get_size(root, 0);
solve(root);
}
}
int main(){
n = read();
for(int i = 1; i < n; ++i){
u = read(), v = read(), w = read();
add(u, v, w);
add(v, u, w);
}
Maxn[0] = INF, root = 0, all = n;
get_root(1, 0);
get_size(root, 0);
solve(root);
printf("%lld\n", ans);
return 0;
}
//152147457856000000
//299999000000000000