树分治
点分治
点分治适合处理大规模的树上路径信息问题。
我们先随意选择一个节点作为根节点
在本题中,对于经过根节点
注意在清空 memset
,而应将之前占用过的
点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归
若我们 每次选择子树的重心作为根节点,可以保证递归层数最少,时间复杂度为
请注意在重新选择根节点之后一定要重新计算子树的大小,否则一点看似微小的改动就可能会使时间复杂度错误或正确性难以保证。
参考代码
/* size 处理子树 d[], 连通块大小 cnt
dp 最大子树 f[], 树的重心 rot
get 计算出点到重心的距离 t[], top
calc 点分治 bu[] 长度桶
hd to nx wg 链式前向星存图
ak[] as[] 离线处理询问
ok[] 点分治中已成为重心的点
*/
#include <iostream>
const int N = 1e4 + 4, M = 105, Q = 1e7 + 7;
int n, m, hd[N], to[N * 2], nx[N * 2], wg[N * 2];
int ak[M], d[N], f[N], t[N], top, cnt, rot;
bool as[M], ok[N], bu[Q];
int size(int u, int pa) {
cnt++, d[u] = 1;
for (int p = hd[u]; ~p; p = nx[p])
if (to[p] != pa && !ok[to[p]]) d[u] += size(to[p], u);
return d[u];
}
void dp(int u, int pa) {
f[u] = cnt - d[u];
for (int p = hd[u]; ~p; p = nx[p])
if (to[p] != pa && !ok[to[p]]) {
f[u] = std::max(f[u], d[to[p]]);
dp(to[p], u);
}
if (f[u] < f[rot]) rot = u;
}
void get(int u, int pa, int dis) {
t[top++] = dis;
for (int p = hd[u]; ~p; p = nx[p])
if (to[p] != pa && !ok[to[p]]) get(to[p], u, dis + wg[p]);
}
void calc(int u) {
cnt = 0, size(u, u);
rot = u, dp(u, u);
bu[0] = true, t[0] = 0, top = 1;
for (int p = hd[rot], i; ~p; p = nx[p])
if (!ok[to[p]]) {
i = top, get(to[p], rot, wg[p]);
for (int q = 0; q < m; q++)
for (int j = i; j < top && !as[q]; j++)
if (ak[q] >= t[j]) as[q] = bu[ak[q] - t[j]];
--i;
while (++i < top)
if (t[i] < Q) bu[t[i]] = true;
}
while (top--)
if (t[top] < Q) bu[t[top]] = false;
ok[rot] = true;
for (int p = hd[rot]; ~p; p = nx[p])
if (!ok[to[p]]) calc(to[p]);
}
int main() {
std::cin >> n >> m;
for (int i = 1; i <= n; i++) hd[i] = -1;
for (int i = 0, u, v; i + 2 < n * 2;) {
std::cin >> u >> v >> wg[i];
wg[i + 1] = wg[i];
to[i] = v, nx[i] = hd[u], hd[u] = i++;
to[i] = u, nx[i] = hd[v], hd[v] = i++;
}
for (int i = 0; i < m; i++) std::cin >> ak[i];
calc(1);
for (int i = 0; i < m; i++) std::cout << (as[i] ? "AYE\n" : "NAY\n");
}
由于这里查询的是树上距离为
参考代码
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
constexpr long long MAXN = 2000010;
constexpr long long inf = 2e9;
long long n, a, b, c, q, rt, siz[MAXN], maxx[MAXN], dist[MAXN];
long long cur, h[MAXN], nxt[MAXN], p[MAXN], w[MAXN], ret;
bool vis[MAXN];
void add_edge(long long x, long long y, long long z) {
cur++;
nxt[cur] = h[x];
h[x] = cur;
p[cur] = y;
w[cur] = z;
}
long long sum;
void calcsiz(long long x, long long fa) {
siz[x] = 1;
maxx[x] = 0;
for (long long j = h[x]; j; j = nxt[j])
if (p[j] != fa && !vis[p[j]]) {
calcsiz(p[j], x);
maxx[x] = max(maxx[x], siz[p[j]]);
siz[x] += siz[p[j]];
}
maxx[x] = max(maxx[x], sum - siz[x]);
if (maxx[x] < maxx[rt]) rt = x;
}
long long dd[MAXN], cnt;
void calcdist(long long x, long long fa) {
dd[++cnt] = dist[x];
for (long long j = h[x]; j; j = nxt[j])
if (p[j] != fa && !vis[p[j]])
dist[p[j]] = dist[x] + w[j], calcdist(p[j], x);
}
queue<long long> tag;
struct segtree {
long long cnt, rt, lc[MAXN], rc[MAXN], sum[MAXN];
void clear() {
while (!tag.empty()) update(rt, 1, 20000000, tag.front(), -1), tag.pop();
cnt = 0;
}
void print(long long o, long long l, long long r) {
if (!o || !sum[o]) return;
if (l == r) {
cout << l << ' ' << sum[o] << '\n';
return;
}
long long mid = (l + r) >> 1;
print(lc[o], l, mid);
print(rc[o], mid + 1, r);
}
void update(long long& o, long long l, long long r, long long x,
long long v) {
if (!o) o = ++cnt;
if (l == r) {
sum[o] += v;
if (!sum[o]) o = 0;
return;
}
long long mid = (l + r) >> 1;
if (x <= mid)
update(lc[o], l, mid, x, v);
else
update(rc[o], mid + 1, r, x, v);
sum[o] = sum[lc[o]] + sum[rc[o]];
if (!sum[o]) o = 0;
}
long long query(long long o, long long l, long long r, long long ql,
long long qr) {
if (!o) return 0;
if (r < ql || l > qr) return 0;
if (ql <= l && r <= qr) return sum[o];
long long mid = (l + r) >> 1;
return query(lc[o], l, mid, ql, qr) + query(rc[o], mid + 1, r, ql, qr);
}
} st;
void dfz(long long x, long long fa) {
// tf[0]=true;tag.push(0);
st.update(st.rt, 1, 20000000, 1, 1);
tag.push(1);
vis[x] = true;
for (long long j = h[x]; j; j = nxt[j])
if (p[j] != fa && !vis[p[j]]) {
dist[p[j]] = w[j];
calcdist(p[j], x);
for (long long k = 1; k <= cnt; k++)
if (q - dd[k] >= 0)
ret += st.query(st.rt, 1, 20000000, max(0ll, 1 - dd[k]) + 1,
max(0ll, q - dd[k]) + 1);
for (long long k = 1; k <= cnt; k++)
st.update(st.rt, 1, 20000000, dd[k] + 1, 1), tag.push(dd[k] + 1);
cnt = 0;
}
st.clear();
for (long long j = h[x]; j; j = nxt[j])
if (p[j] != fa && !vis[p[j]]) {
sum = siz[p[j]];
rt = 0;
maxx[rt] = inf;
calcsiz(p[j], x);
calcsiz(rt, -1);
dfz(rt, x);
}
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (long long i = 1; i < n; i++)
cin >> a >> b >> c, add_edge(a, b, c), add_edge(b, a, c);
cin >> q;
rt = 0;
maxx[rt] = inf;
sum = n;
calcsiz(1, -1);
calcsiz(rt, -1);
dfz(rt, -1);
cout << ret << '\n';
return 0;
}
例题 3 Luogu P2664 树上游戏
一棵每个节点都给定颜色的树,定义
这道题很考验对点分治思想的理解和应用,适合作为点分治的难度较高的例题和练习题。
首先,我们需要想明白一个转化。题目定义
考虑到点分治过程中,我们只需要分别考虑统计:
- 子树中以当前根节点为端点的路径对根的贡献
- lca 为当前根节点的路径对子树内每个点的贡献
1 部分比较好办,由于点分治中,递归层数不超过
而针对 2 部分,设当前根节点
路径上出现过的颜色,数量设为 , 除了 以外的其他所有子树的总大小设为 , 那么这些出现过的颜色对 的答案贡献为 。 路径上没有出现过的颜色 ,它们的贡献来自于 除了 以外的其他所有子树的 ,这部分答案为 。
以上是全部统计思路,实现细节详见参考代码。
参考代码
#include <algorithm>
#include <iostream>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); ++i)
constexpr int N = 200005;
int h[N], nxt[N * 2], to[N * 2], c[N], gr;
void tu(int x, int y) { to[++gr] = y, nxt[gr] = h[x], h[x] = gr; }
using ll = long long;
int n, nn, siz[N], mn, rt;
bool vis[N];
void get_root(int u, int f) {
siz[u] = 1;
int mx = 0;
for (int i = h[u]; i; i = nxt[i]) {
int d = to[i];
if (vis[d] || d == f) continue;
get_root(d, u);
siz[u] += siz[d];
mx = max(mx, siz[d]);
}
mx = max(mx, nn - siz[u]);
if (mx < mn) mn = mx, rt = u;
}
ll ans[N], sum;
int cnt[N], v[N];
// sum实时统计的是cnt[i]的和
int nowrt;
void get_dis(int u, int f, int now) { // now为当前树链上的颜色数量(不含u)
siz[u] = 1;
if (!v[c[u]]) {
sum -= cnt[c[u]]; // 减去在之前子树中已经出现过的颜色信息
now++;
}
v[c[u]]++;
ans[u] += sum + now * siz[nowrt]; // 统计过u点的路径对u的贡献
for (int i = h[u]; i; i = nxt[i]) {
int d = to[i];
if (d == f || vis[d]) continue;
get_dis(d, u, now);
siz[u] += siz[d];
}
v[c[u]]--;
if (!v[c[u]]) {
sum += cnt[c[u]]; // 回溯
}
}
void get_cnt(int u, int f) {
if (!v[c[u]]) {
cnt[c[u]] += siz[u];
sum += siz[u]; // 将刚遍历过的子树的信息整合到cnt[i]和sum上去
}
v[c[u]]++;
for (int i = h[u]; i; i = nxt[i]) {
int d = to[i];
if (vis[d] || d == f) continue;
get_cnt(d, u);
}
v[c[u]]--;
}
void clear(int u, int f, int now) {
if (!v[c[u]]) now++;
v[c[u]]++;
ans[u] -= now;
ans[nowrt] += now;
for (int i = h[u]; i; i = nxt[i]) {
int d = to[i];
if (vis[d] || d == f) continue;
clear(d, u, now);
}
v[c[u]]--;
cnt[c[u]] = 0;
}
void clear2(int u, int f) {
cnt[c[u]] = 0;
for (int i = h[u]; i; i = nxt[i]) {
int d = to[i];
if (vis[d] || d == f) continue;
clear2(d, u);
}
}
int son[N];
void divid(int u) {
vis[u] = true;
int tot = 0;
nowrt = u;
ans[u]++;
for (int i = h[u]; i; i = nxt[i]) {
if (vis[to[i]]) continue;
son[++tot] = to[i];
}
siz[u] = sum = cnt[c[u]] = 1;
v[c[u]]++;
rep(i, 1, tot) { // 统计每个子树和它之前的所有子树中节点组合产生的贡献
int d = son[i];
get_dis(d, u, 0);
get_cnt(d, u);
siz[u] += siz[d];
cnt[c[u]] += siz[d];
sum += siz[d];
}
clear2(u, 0); // 清空数组,记得不可以用memset
siz[u] = sum = cnt[c[u]] = 1;
for (int i = tot; i >= 1;
--i) { // 统计每个子树和它之后的所有子树中节点组合产生的贡献
int d = son[i];
get_dis(d, u, 0);
get_cnt(d, u);
siz[u] += siz[d];
cnt[c[u]] += siz[d];
sum += siz[d];
}
v[c[u]]--;
clear(u, 0, 0); // 清空的同时统计答案
for (int i = h[u]; i; i = nxt[i]) { // 继续向下进行点分治
int d = to[i];
if (vis[d]) continue;
nn = siz[d], mn = n + 1, rt = 0;
get_root(d, u);
divid(rt);
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
int u, v;
rep(i, 1, n) cin >> c[i];
rep(i, 2, n) cin >> u >> v, tu(u, v), tu(v, u);
rt = 0, nn = n, mn = n + 1;
get_root(1, 0);
divid(rt);
rep(i, 1, n) cout << ans[i] << '\n';
return 0;
}
边分治
与上面的点分治类似,我们选取一条边,把树尽量均匀地分成两部分(使边连接的两个子树的
但是这是不行的,考虑一个菊花图:
我们发现当一个点下有多个
如果这个图是个二叉树,就可以避免上面菊花图中应用边分治的弊端。因此我们考虑把一个多叉树转化成二叉树。
显然,我们只需像线段树那样建树就可以了。就像这样
新建出来的点根据题目要求给予恰当的信息即可。例如:统计路径长度时,将原边边权赋为
分析复杂度,发现最多会增加
几乎所有点分治的题边分都能做(常数上有差距,但是不卡),所以就不放例题了。
点分树
点分树是通过更改原树形态使树的层数变为稳定
常用于解决与树原形态无关的带修改问题。
算法分析
我们通过点分治每次找重心的方式来对原树进行重构。
将每次找到的重心与上一层的重心缔结父子关系,这样就可以形成一棵
由于树是
代码实现
有一个小技巧:每次用递归上一层的总大小
参考代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
using IT = vector<int>::iterator;
struct Edge {
int to, nxt, val;
Edge() {}
Edge(int to, int nxt, int val) : to(to), nxt(nxt), val(val) {}
} e[300010];
int head[150010], cnt;
void addedge(int u, int v, int val) {
e[++cnt] = Edge(v, head[u], val);
head[u] = cnt;
}
int siz[150010], son[150010];
bool vis[150010];
int tot, lasttot;
int maxp, root;
void getG(int now, int fa) {
siz[now] = 1;
son[now] = 0;
for (int i = head[now]; i; i = e[i].nxt) {
int vs = e[i].to;
if (vs == fa || vis[vs]) continue;
getG(vs, now);
siz[now] += siz[vs];
son[now] = max(son[now], siz[vs]);
}
son[now] = max(son[now], tot - siz[now]);
if (son[now] < maxp) {
maxp = son[now];
root = now;
}
}
struct Node {
int fa;
vector<int> anc;
vector<int> child;
} nd[150010];
int build(int now, int ntot) {
tot = ntot;
maxp = 0x7f7f7f7f;
getG(now, 0);
int g = root;
vis[g] = true;
for (int i = head[g]; i; i = e[i].nxt) {
int vs = e[i].to;
if (vis[vs]) continue;
int tmp = build(vs, ntot - son[vs]);
nd[tmp].fa = now;
nd[now].child.push_back(tmp);
}
return g;
}
int virtroot;
int main() {
int n;
cin >> n;
for (int i = 1; i < n; i++) {
int u, v, val;
cin >> u >> v >> val;
addedge(u, v, val);
addedge(v, u, val);
}
virtroot = build(1, n);
}
创建日期: 2018年7月11日