树分治
点分治
点分治适合处理大规模的树上路径信息问题。
给定一棵有
我们先随意选择一个节点作为根节点
在本题中,对于经过根节点
注意在清空 memset
,而应将之前占用过的
点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归
若我们 每次选择子树的重心作为根节点,可以保证递归层数最少,时间复杂度为
请注意在重新选择根节点之后一定要重新计算子树的大小,否则一点看似微小的改动就可能会使时间复杂度错误或正确性难以保证。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn = 20010;
const int inf = 2e9;
int n, m, a, b, c, q[maxn], rt, siz[maxn], maxx[maxn], dist[maxn];
int cur, h[maxn], nxt[maxn], p[maxn], w[maxn];
bool tf[10000010], ret[maxn], vis[maxn];
void add_edge(int x, int y, int z) {
cur++;
nxt[cur] = h[x];
h[x] = cur;
p[cur] = y;
w[cur] = z;
}
int sum;
void calcsiz(int x, int fa) {
siz[x] = 1;
maxx[x] = 0;
for (int 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;
}
int dd[maxn], cnt;
void calcdist(int x, int fa) {
dd[++cnt] = dist[x];
for (int 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<int> tag;
void dfz(int x, int fa) {
tf[0] = true;
tag.push(0);
vis[x] = true;
for (int j = h[x]; j; j = nxt[j])
if (p[j] != fa && !vis[p[j]]) {
dist[p[j]] = w[j];
calcdist(p[j], x);
for (int k = 1; k <= cnt; k++)
for (int i = 1; i <= m; i++)
if (q[i] >= dd[k]) ret[i] |= tf[q[i] - dd[k]];
for (int k = 1; k <= cnt; k++)
if (dd[k] < 10000010) tag.push(dd[k]), tf[dd[k]] = true;
cnt = 0;
}
while (!tag.empty()) tf[tag.front()] = false, tag.pop();
for (int 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);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i < n; i++)
scanf("%d%d%d", &a, &b, &c), add_edge(a, b, c), add_edge(b, a, c);
for (int i = 1; i <= m; i++) scanf("%d", q + i);
rt = 0;
maxx[rt] = inf;
sum = n;
calcsiz(1, -1);
calcsiz(rt, -1);
dfz(rt, -1);
for (int i = 1; i <= m; i++)
if (ret[i])
printf("AYE\n");
else
printf("NAY\n");
return 0;
}
给定一棵有
由于这里查询的是树上距离为
```cpp
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#define int long long
using namespace std;
const int maxn = 2000010;
const int inf = 2e9;
int n, a, b, c, q, rt, siz[maxn], maxx[maxn], dist[maxn];
int cur, h[maxn], nxt[maxn], p[maxn], w[maxn], ret;
bool vis[maxn];
void add_edge(int x, int y, int z) {
cur++;
nxt[cur] = h[x];
h[x] = cur;
p[cur] = y;
w[cur] = z;
}
int sum;
void calcsiz(int x, int fa) {
siz[x] = 1;
maxx[x] = 0;
for (int 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;
}
int dd[maxn], cnt;
void calcdist(int x, int fa) {
dd[++cnt] = dist[x];
for (int 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<int> tag;
struct segtree {
int 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(int o, int l, int r) {
if (!o || !sum[o]) return;
if (l == r) {
printf("%lld %lld\n", l, sum[o]);
return;
}
int mid = (l + r) >> 1;
print(lc[o], l, mid);
print(rc[o], mid + 1, r);
}
void update(int& o, int l, int r, int x, int v) {
if (!o) o = ++cnt;
if (l == r) {
sum[o] += v;
if (!sum[o]) o = 0;
return;
}
int 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;
}
int query(int o, int l, int r, int ql, int qr) {
if (!o) return 0;
if (r < ql || l > qr) return 0;
if (ql <= l && r <= qr) return sum[o];
int mid = (l + r) >> 1;
return query(lc[o], l, mid, ql, qr) + query(rc[o], mid + 1, r, ql, qr);
}
} st;
void dfz(int x, int fa) {
// tf[0]=true;tag.push(0);
st.update(st.rt, 1, 20000000, 1, 1);
tag.push(1);
vis[x] = true;
for (int j = h[x]; j; j = nxt[j])
if (p[j] != fa && !vis[p[j]]) {
dist[p[j]] = w[j];
calcdist(p[j], x);
for (int 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 (int 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 (int 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() {
scanf("%lld", &n);
for (int i = 1; i < n; i++)
scanf("%lld%lld%lld", &a, &b, &c), add_edge(a, b, c), add_edge(b, a, c);
scanf("%lld", &q);
rt = 0;
maxx[rt] = inf;
sum = n;
calcsiz(1, -1);
calcsiz(rt, -1);
dfz(rt, -1);
printf("%lld\n", ret);
return 0;
}
```
一棵每个节点都给定颜色的树,定义
这道题很考验对点分治思想的理解和应用,适合作为点分治的难度较高的例题和练习题。
首先,我们需要想明白一个转化。题目定义
考虑到点分治过程中,我们只需要分别考虑统计:
- 子树中以当前根节点为端点的路径对根的贡献
- lca 为当前根节点的路径对子树内每个点的贡献
1 部分比较好办,由于点分治中,递归层数不超过
而针对 2 部分,设当前根节点
路径上出现过的颜色,数量设为 , 除了 以外的其他所有子树的总大小设为 , 那么这些出现过的颜色对 的答案贡献为 。 路径上没有出现过的颜色 ,它们的贡献来自于 除了 以外的其他所有子树的 ,这部分答案为 。
以上是全部统计思路,实现细节详见参考代码。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); ++i)
const int N = 200005;
int h[N], nxt[N * 2], to[N * 2], c[N], gr;
#define il inline
il void tu(int x, int y) { to[++gr] = y, nxt[gr] = h[x], h[x] = gr; }
typedef long long ll;
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] = 1;
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() {
scanf("%d", &n);
int u, v;
rep(i, 1, n) scanf("%d", &c[i]);
rep(i, 2, n) scanf("%d%d", &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) printf("%lld\n", ans[i]);
return 0;
}
边分治
与上面的点分治类似,我们选取一条边,把树尽量均匀地分成两部分(使边连接的两个子树的
ちょっとまって,这不行吧……
考虑一个菊花图
我们发现当一个点下有多个
如果这个图是个二叉树,就可以避免上面菊花图中应用边分治的弊端。因此我们考虑把一个多叉树转化成二叉树。
显然,我们只需像线段树那样建树就可以了。就像这样
新建出来的点根据题目要求给予恰当的信息即可。例如:统计路径长度时,将原边边权赋为
分析复杂度,发现最多会增加
几乎所有点分治的题边分都能做(常数上有差距,但是不卡),所以就不放例题了。
点分树
点分树是通过更改原树形态使树的层数变为稳定
常用于解决与树原形态无关的带修改问题。
算法分析
我们通过点分治每次找重心的方式来对原树进行重构。
将每次找到的重心与上一层的重心缔结父子关系,这样就可以形成一棵
由于树是
代码实现
有一个小技巧:每次用递归上一层的总大小
#include <bits/stdc++.h>
using namespace std;
typedef vector<int>::iterator IT;
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] = 1;
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);
}
贡献者:@Flex@shuzhouliu-bot@Ir1d@chenyanlann@kenlig@caijimengfanyu@Nathan@ksyx@mgt@Thallium@StudyingFather@Chrogeek@Henry-ZHR@ouuan@partychicken@雷蒻@Xeonacid@orzcyand1317
本页面最近更新:2/3/2023, 12:00:00 AM,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用