题目特征
点分治用于解决树上不带修改的路径问题(静态问题)
点分治
记
r
o
o
t
root
root为当前的树根,则对于所有路径来说,有两种情况:
①经过
r
o
o
t
root
root的路径
②不经过
r
o
o
t
root
root的路径
对于①类路径来说,可以看做从
r
o
o
t
root
root前往不同子树的两条路径之和(下面介绍的和都是指两点间距离)
对此处路径
(
4
,
7
)
(4, 7)
(4,7)来说,可以看做是路径
(
4
,
1
)
(4,1)
(4,1)和路径
(
1
,
7
)
(1,7)
(1,7)之和
若是即
d
i
s
[
i
]
dis[i]
dis[i]为点
i
i
i到达当前树根的距离,那么
d
i
s
t
a
n
c
e
(
4
,
7
)
=
d
i
s
[
4
]
+
d
i
s
[
7
]
=
4
distance(4,7)=dis[4]+dis[7]=4
distance(4,7)=dis[4]+dis[7]=4
还有要注意是不同子树,如果这里求的是
d
i
s
t
a
n
c
e
(
6
,
7
)
distance(6,7)
distance(6,7),那么显然
d
i
s
t
a
n
c
e
(
6
,
7
)
=
2
distance(6,7)=2
distance(6,7)=2不等于
d
i
s
[
6
]
+
d
i
s
[
7
]
=
4
dis[6]+dis[7]=4
dis[6]+dis[7]=4
现在看看怎么处理②类路径
对于②类路径来说,它一定可以在当前
r
o
o
t
root
root的子树中找到一个点作为新的
r
o
o
t
root
root来求得
如
d
i
s
t
a
n
c
e
(
6
,
7
)
distance(6,7)
distance(6,7)在以
3
3
3为根时有,
d
i
s
t
a
n
c
e
(
6
,
7
)
=
2
=
d
i
s
[
6
]
+
d
i
s
[
7
]
distance(6,7)=2=dis[6]+dis[7]
distance(6,7)=2=dis[6]+dis[7]
所以②类路径可以看做是子树中的①类路径,这样所有路径都可以转化为①类路径
那么我们只需要以每一个点都作为
r
o
o
t
root
root求①类路径就能求得所有的路径
那么怎么做呢,我们这里可以看到,每次处理的都是同一个问题——求①类路径
我们这里就是一个普通的
d
f
s
dfs
dfs递归过程而已,中间顺带处理了①类路径问题,每次处理的手法还是一模一样的
复杂度
这里我们处理树各层需要的复杂度为,第一层
O
(
s
i
z
e
[
1
]
)
=
O
(
N
)
O(size[1])=O(N)
O(size[1])=O(N),第二层
O
(
s
i
z
e
[
2
]
)
+
O
(
s
i
z
e
[
3
]
)
=
O
(
N
−
1
)
O(size[2])+O(size[3])=O(N-1)
O(size[2])+O(size[3])=O(N−1),第三层
∑
i
=
4
7
O
(
s
i
z
e
[
i
]
)
=
O
(
N
−
3
)
\displaystyle\sum_{i=4}^7 O(size[i])=O(N-3)
i=4∑7O(size[i])=O(N−3),所以处理每一层可以看做是
O
(
N
)
O(N)
O(N)
则递归层数为T,那么总体复杂度就是
O
(
T
N
)
O(TN)
O(TN)
若我们处理的树形结构变成了线性结构,并且我们从端头开始递归,那么复杂度将会到达 O ( N 2 ) O(N^2) O(N2)
所以这里我们要降低我们的递归层数
这里就需要我们求树的重心,这是因为树的重心有一个性质:
以重心为根,所有的子树的大小都不超过整个树大小的一半
这样每次递归的时候,先找出子树的重心,从重心进入,最后能保证我们递归层数在
l
o
g
N
logN
logN层
我对这里递归层数的理解:递归层数和树的大小有关,这里就假设每次最大的子树恰好为原树的一半(不超过一半),那么这个最大的子树中,最大的子树也是一半…以此类推到最后只需要
l
o
g
N
logN
logN次就能到达叶节点
(
2
l
o
g
2
N
=
N
2^{log_2N}=N
2log2N=N)
故复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)
模板
int maxp[MAX], siz[MAX], vis[MAX], rt;
void getRt(int u, int fa, int all) {//求树的重心
siz[u] = 1, maxp[u] = 0;
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (v != fa && !vis[v]) {
getRt(v, u, all);
siz[u] += siz[v];
maxp[u] = max(maxp[u], siz[v]);
}
maxp[u] = max(maxp[u], all - siz[u]);//当前点为根的话,fa那边的子树就是all-siz[u]
if (maxp[u] < maxp[rt]) rt = u;
}
void calc(int u) {//具体题目具体分析
}
void dfs(int u) {
vis[u] = 1;
calc(u);//计算第一类问题
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (!vis[v]) {
maxp[rt = 0] = N; getRt(v, 0, siz[v]);//每次都要求重心
dfs(rt);
}
}
int main() {
//前面预处理
maxp[rt = 0] = N; getRt(1, 0, N);
dfs(rt);//dfs求解答案
return 0;
}
练习题
1. P3806 【模板】点分治1
题意
N N N个点的树, M M M次询问,每次问长度为 K K K的路径是否存在
分析
K
≤
1
0
7
K\leq10^7
K≤107,我们这里可以用桶来记录一下之前找到的路径长度
每次从子树中找到路径,但先不加入桶中,因为我们要保证两个路径不在同一个子树里面
若某路径长度为
d
i
s
dis
dis,那么只需要看桶
f
[
K
−
d
i
s
]
f[K-dis]
f[K−dis]是否为1即可
还有最开始要往桶中加入
0
0
0这个长度,这样不会漏掉直接从
r
o
o
t
root
root到那个点的路径
代码
void getDis(int u, int fa, int d) {
if (d < MAX_N) t[++t[0]] = d;//每次将当前路径记录在t数组中
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (v != fa && !vis[v]) getDis(v, u, d + e[i].w);
}
void calc(int u) {
int num = 0;
f[0] = 1;//长度为0的可以
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (!vis[v]) {
t[0] = 0; getDis(v, u, e[i].w);//找当前子树里面的路径
for (int j = 1; j <= M; j++)//对于每一个问题,找答案
if (!ans[j]) {//如果没找到
for (int k = 1; k <= t[0]; k++)//从这个子树里面的路径开始
if (query[j] - t[k] >= 0)
ans[j] |= f[query[j] - t[k]];
}
for (int j = 1; j <= t[0]; j++)//先找完再加到桶里面,这样能保证之前找的路径和本次的不在同一个子树中
f[t[j]] = 1, store[++num] = t[j];//因为最后memset桶会超时,所以这里用一个store来记录改了哪里
}
for (int i = 1; i <= num; i++)
f[store[i]] = 0;//最后清空
}
2. P4178 Tree
题意
给你一棵TREE,以及这棵树上边的距离.问有多少对点它们两者间的距离小于等于 K K K
分析
当前路径为
d
i
s
dis
dis,这里我用树状数组来查路径长度小于等于
K
−
d
i
s
K-dis
K−dis的路径数量
一样的处理,先找路径,然后先查询在将路径加入树状数组
还有因为树状数组不能查长度为
0
0
0路径,所以我把所有路径长度都+1,那么
K
K
K应该是+2,而不是+1
代码
void getDis(int u, int fa, int d) {//这里我K已经提前+2了
if (d <= K - 2) t[++t[0]] = d + 1;//路径长度+1
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (v != fa && !vis[v]) getDis(v, u, d + e[i].w);
}
void calc(int u) {
int num = 0;
upd(1, 1);//加入路径长度为0, 此处路径长度+1, 所以是1
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (!vis[v]) {
t[0] = 0; getDis(v, u, e[i].w);//先找路径
for (int j = 1; j <= t[0]; j++)//查询
if (K - t[j] >= 0) ans += query(K - t[j]);
for (int j = 1; j <= t[0]; j++)//再加入树状数组
upd(t[j], 1), store[++num] = t[j];//更新,并记录加了哪些值
}
upd(1, -1);//最后清空
for (int i = 1; i <= num; i++)
upd(store[i], -1);
}
3. P4149 [IOI2011]Race
题意
给一棵树,每条边有权。求一条简单路径,权值和等于 K K K,且边的数量最小
分析
和前面差不多,多记一个边数就行
f
[
i
]
f[i]
f[i]同样是桶,记得是长度为
i
i
i的最小边数的路径
代码
void getDis(int u, int fa, int d, int num) {
if (d <= K) t[++p] = make_pair(d, num);
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (v != fa && !vis[v]) getDis(v, u, d + e[i].w, num + 1);
}
void calc(int u) {
int num = 0;
for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
if (!vis[v]) {
p = 0; getDis(v, u, e[i].w, 1);
for (int j = 1; j <= p; j++)
if (K >= t[j].first && f[K - t[j].first] != INF)
ans = min(ans, f[K - t[j].first] + t[j].second);
for (int j = 1; j <= p; j++)
f[t[j].first] = min(f[t[j].first], t[j].second), store[++num] = t[j].first;
}
for (int i = 1; i <= num; i++)
f[store[i]] = INF;
}
4. P3714 [BJOI2017]树的难题
分析
这里比较复杂,要考虑两条路径合并的时候合并的部分颜色是否相同
并且要查询
[
L
−
d
i
s
,
R
−
d
i
s
]
[L-dis,R-dis]
[L−dis,R−dis]中边权最大的
所以这里建了两个线段树维护最大值,一个是颜色相同的
s
a
m
(
s
a
m
e
)
sam(same)
sam(same)线段树,一个颜色不同的
d
i
f
(
d
i
f
f
e
r
e
n
t
)
dif(different)
dif(different)线段树
我们需要先将边排序,按照颜色排序,这样一旦当前边的颜色和上一个边的颜色不同的时候,说明上一种颜色已经处理完毕,就可以清空线段树,然后将之前的这种颜色全部放入
d
i
f
dif
dif线段树(因为这个时候前面那种颜色和当前这种颜色是不一样的)
代码
代码比较复杂,放全部的
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define lc u<<1
#define rc u<<1|1
#define m (l+r)/2
typedef long long ll;
typedef pair<int, int> pii;
const int MAX = 2e5 + 10;
struct edge {
int to, color;
bool operator < (const edge &rhs) const {
return color < rhs.color;
}
};
int N, M, L, R;
int val[MAX], ans, cnt;
vector<edge> g[MAX];
int maxp[MAX], siz[MAX], vis[MAX], rt;
pii t[MAX];
vector<pii> tmp;
struct Tree {
struct SegementTree {
int mx, tag;//mx记录当前最大, tag记录是否要清空
void upd() {
mx = -INF;
tag = 1;
}
} t[MAX << 1];
void push_up(int u) { t[u].mx = max(t[lc].mx, t[rc].mx); }
void push_down(int u) {
if (t[u].tag) {
t[lc].upd();
t[rc].upd();
t[u].tag = 0;
}
}
void update(int u, int l, int r, int p, int k) {
if (l == r) {
t[u].mx = max(t[u].mx, k);
return;
}
push_down(u);
if (p <= m) update(lc, l, m, p, k);
else update(rc, m + 1, r, p, k);
push_up(u);
}
int query(int u, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return t[u].mx;
if (t[u].tag) return -INF;
int res = -INF;
if (ql <= m) res = max(res, query(lc, l, m, ql, qr));
if (qr > m) res = max(res, query(rc, m + 1, r, ql, qr));
return res;
}
void clear() { t[1].upd(); }
} sam, dif;
void getRt(int u, int fa, int all) {
siz[u] = 1, maxp[u] = 0;
for (auto &i: g[u]) {
int v = i.to;
if (v != fa && !vis[v]) {
getRt(v, u, all);
siz[u] += siz[v];
maxp[u] = max(maxp[u], siz[v]);
}
}
maxp[u] = max(maxp[u], all - siz[u]);
if (maxp[u] < maxp[rt]) rt = u;
}
void getRoad(int u, int fa, int nowc, int nume, int dis) {
if (nume >= L) ans = max(ans, dis);//路径长度>=L直接判,因为线段树不处理长度0的路径,所以直接判
if (nume == R) return;//长度为R的找的对应路径长度为0, 这里我不存, 后面就没啥意义了, 所以直接返回
t[++cnt] = make_pair(nume, dis);
for (auto &i: g[u]) {
int v = i.to;
if (v != fa && !vis[v]) getRoad(v, u, i.color, nume + 1, dis + (nowc == i.color ? 0 : val[i.color]));
}
}
void calc(int u) {
int pre = 0;
sam.clear();
dif.clear();
tmp.clear();
for (auto &i: g[u]) {
int v = i.to;
if (!vis[v]) {
cnt = 0; getRoad(v, u, i.color, 1, val[i.color]);
if (pre == i.color) {//颜色一样
//先查
for (int j = 1; j <= cnt; j++) ans = max(ans, dif.query(1, 1, R - 1, max(1, L - t[j].first), R - t[j].first) + t[j].second);
for (int j = 1; j <= cnt; j++) ans = max(ans, sam.query(1, 1, R - 1, max(1, L - t[j].first), R - t[j].first) + t[j].second - val[i.color]);
//将点加入颜色相同的sam线段树,并且记录加了哪些点,这样后面直接再加入dif线段树
for (int j = 1; j <= cnt; j++) sam.update(1, 1, R - 1, t[j].first, t[j].second), tmp.push_back(t[j]);
}
else {
//将之前颜色相同的点都加入dif线段树
for (auto &j: tmp) dif.update(1, 1, R - 1, j.first, j.second);
//要先加tmp再查, 因为前面那些颜色已经和当前颜色不一样了
for (int j = 1; j <= cnt; j++) ans = max(ans, dif.query(1, 1, R - 1, max(1, L - t[j].first), R - t[j].first) + t[j].second);
tmp.clear();//清空tmp
sam.clear();//清空sam线段树
//当前这个颜色是新的颜色,加入sam线段树
for (int j = 1; j <= cnt; j++) sam.update(1, 1, R - 1, t[j].first, t[j].second), tmp.push_back(t[j]);
}
pre = i.color;//上一种颜色
}
}
}
void dfs(int u) {
vis[u] = 1;
calc(u);
for (auto &i: g[u]) {
int v = i.to;
if (!vis[v]) {
maxp[rt = 0] = N; getRt(v, 0, siz[v]);
dfs(rt);
}
}
}
int main() {
scanf("%d%d%d%d", &N, &M, &L, &R);
for (int i = 1; i <= M; i++) scanf("%d", &val[i]);
for (int i = 1; i < N; i++) {
int u, v, color; scanf("%d%d%d", &u, &v, &color);
g[u].push_back(edge{v, color});
g[v].push_back(edge{u, color});
}
for (int i = 1; i <= N; i++) sort(g[i].begin(), g[i].end());//将边按颜色排序
maxp[rt = 0] = N; getRt(1, 0, N);
ans = -INF;
dfs(rt);
printf("%d\n", ans);
return 0;
}