子树求和问题通常有三种解法。
像求子树 size 那样,可以用直接的子树合并来解决的问题,大家想必都会了。
我们来介绍一个技巧,可以把对路径的询问或操作,转化为子树求和问题。
给你一个
有
求
考虑以点
我们可以把每个点的权值看作一种子树和。
把点
我们把从
我们把这个技巧称为树上差分。
在上述问题中,如果把点权换成边权。把操作改成
又该如何处理呢?
const int maxn = 5e4 + 5;
vector<int> g[maxn];
int num, L[maxn], R[maxn];
bool is_ancestor(int a, int b) { // a 是不是 b 的祖先
return L[a] <= L[b] && L[b] <= R[a];
}
int anc[maxn][16];
int lca(int u, int v) {
if (is_ancestor(u, v)) return u;
if (is_ancestor(v, u)) return v;
for (int i = 15; i >= 0; i--) {
if (anc[u][i] && !is_ancestor(anc[u][i], v))
u = anc[u][i];
}
return anc[u][0];
}
void dfs(int u, int p) {
L[u] = ++num;
anc[u][0] = p;
for (int i = 1; i < 16; i++)
anc[u][i] = anc[anc[u][i - 1]][i - 1];
for (int v : g[u])
if (v != p) {
dfs(v, u);
}
R[u] = num;
}
int a[maxn];
void get_sum(int u, int p) { // 求子树和
for (int v : g[u])
if (v != p) {
get_sum(v, u);
a[u] += a[v];
}
}
int main() {
int n, k; cin >> n >> k;
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
while (k--) {
int s, t; cin >> s >> t;
int u = lca(s, t);
a[s]++; a[t]++;
a[u]--; a[anc[u][0]]--;
}
get_sum(1, 0);
cout << *max_element(a + 1, a + n + 1) << '\n';
}
给你一个有
第
有
你可以选一条边把它改造成虫洞,走过虫洞花费的时间是
所有运输计划同时开始。求完成全部计划最少要花多少时间。
枚举边
给定总时间的上限
考虑原本总时间大于
问题化为
每条边有一个权值,最初都是
long long dist[maxn]; // dist[i]:根到点i的距离
int we[maxn]; //we[i]:点i的父边的长度。
vector<int> postorder; //用非递归的方式计算子树和
void dfs(int u, int p) {
L[u] = ++num;
anc[u][0] = p;
for (int i = 1; i < 19; i++)
anc[u][i] = anc[anc[u][i - 1]][i - 1];
for (auto [v, w] : g[u])
if (v != p) {
dist[v] = dist[u] + w;
dfs(v, u);
we[v] = w;
}
R[u] = num;
postorder.push_back(u);
}
int a[maxn];
int n, m;
int s[maxn], t[maxn], LCA[maxn];
long long len[maxn];
long long max_len;
bool check(long long k) {
memset(a, 0, sizeof a);
int cnt = 0;
for (int i = 0; i < m; i++)
if (len[i] > k) {
cnt++;
// 把 s[i] 到 t[i] 的路径上的边值加 1
a[s[i]]++; a[t[i]]++;
a[LCA[i]] -= 2;
}
//计算子树和
for (int v : postorder)
a[anc[v][0]] += a[v];
for (int i = 1; i <= n; i++)
if (a[i] == cnt && max_len - we[i] <= k)
return true;
return false;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i < n; i++) {
int u, v, w; cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
dfs(1, 0);
for (int i = 0; i < m; i++) {
cin >> s[i] >> t[i];
LCA[i] = lca(s[i], t[i]);
len[i] = dist[s[i]] + dist[t[i]] - 2 * dist[LCA[i]];
}
max_len = *max_element(len, len + m);
long long ok = max_len, ng = -1;
while (ok - ng > 1) {
long long k = (ok + ng) / 2;
if (check(k))
ok = k;
else
ng = k;
}
cout << ok << '\n';
}
前缀:有根树的先序遍历所得序列的前缀。
给你一个有
每个点有一个颜色,点
回答
在 DFS 的过程中,我们维护一个数组
要知道子树
const int maxn = 2e5 + 5;
vector<int> g[maxn];
int a[maxn];
vector<pair<int,int>> query[maxn];
int cnt[maxn];
int ans[maxn];
void dfs(int u, int p) {
for (auto [c, i] : query[u])
ans[i] = cnt[c];
cnt[a[u]]++;
for (int v : g[u])
if (v != p)
dfs(v, u);
for (auto [c, i] : query[u])
ans[i] = cnt[c] - ans[i];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 0; i < n - 1; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
int m; cin >> m;
for (int i = 0; i < m; i++) {
int u, c; cin >> u >> c;
query[u].push_back({c, i});
}
dfs(1, 0);
for (int i = 0; i < m; i++)
cout << ans[i] << '\n';
}
上面的解法是离线的
现在来考虑在线的解法
求 DFS 序号。对每个点
把所有点按颜色分类。对每个颜色
我们知道,点
所以,子树
后者可以通过在有序的列表
const int maxn = 2e5 + 5;
vector<int> g[maxn];
int a[maxn];
vector<int> V[maxn];
int L[maxn], R[maxn], num;
void dfs(int u, int p) {
L[u] = ++num;
V[a[u]].push_back(L[u]);
for (int v : g[u])
if (v != p)
dfs(v, u);
R[u] = num;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 0; i < n - 1; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
int m; cin >> m;
for (int i = 0; i < m; i++) {
int u, c; cin >> u >> c;
cout <<
upper_bound(V[c].begin(), V[c].end(), R[u])
- lower_bound(V[c].begin(), V[c].end(), L[u])
<< '\n';
}
}
给你一个有
点
回答
在本题里,约定根的深度是
在 DFS 的过程中,我们维护
为了回答询问
若结果中 1 不超过
const int maxn = 5e5 + 5;
vector<int> g[maxn];
char c[maxn];
vector<pair<int,int>> query[maxn];
bitset<26> p[maxn]; //全局数据结构
bitset<26> ans[maxn];
void dfs(int u, int depth) {
for (auto [h, id] : query[u])
ans[id] = a[h];
p[depth].flip(c[u] - 'a');
for (int v : g[u])
dfs(v, depth + 1);
for (auto [h, id] : query[u])
ans[id] ^= p[h];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, m; cin >> n >> m;
for (int i = 2; i <= n; i++) {
int p; cin >> p;
g[p].push_back(i);
}
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 0; i < m; i++) {
int v, h; cin >> v >> h;
query[v].push_back({h, i});
}
dfs(1, 1);
for (int i = 0; i < m; i++)
cout <<
(ans[i].count() < 2 ? "Yes" : "No")
<< '\n';
}
上面的解法是离线的。现在来考虑在线解法。
求 DFS 序号。对每个点
把所有点按深度分组。对每个深度
对每一组点,求前缀和。这里介绍两种实现方法。
方法一:对每个深度
方法二:定义序列
对于询问
如果采用上述方法一来表示前缀和,设
如果采用上述方法二来表示前缀和,设
下列代码是采用上述方法二来表示前缀和。
bitset<26> s[maxn];
vector<int> V[maxn];
int L[maxn], R[maxn], num;
void dfs(int u, int depth) {
L[u] = ++num;
s[num] = s[V[depth.back()]];
s[num].flip(c[u] - 'a');
V[depth].push_back(num);
for (int v : g[u])
dfs(v, depth + 1);
R[u] = num;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
// 输入 ...
for (int i = 1; i <= n; i++)
V[i].push_back(0);
dfs(1, 1);
for (int i = 0; i < m; i++) {
int v, h; cin >> v >> h;
bitset<26> ans;
auto l = lower_bound(V[h].begin(), V[h].end(), L[v]);
auto r = upper_bound(V[h].begin(), V[h].end(), R[v]);
bitset<26> ans = s[*(r - 1)] ^ s[*(l - 1)];
cout << (ans.count() > 1 ? "No" : "Yes") << '\n';
}
}
上面代码里的 bitset<26> 也可换为 int。
int s[maxn];
vector<int> V[maxn];
int L[maxn], R[maxn], num;
void dfs(int u, int depth) {
L[u] = ++num;
s[num] = s[V[depth.back()]] ^ 1 << (c[u] - 'a');
V[depth].push_back(num);
for (int v : g[u])
dfs(v, depth + 1);
R[u] = num;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
// 输入 ...
for (int i = 1; i <= n; i++)
V[i].push_back(0);
dfs(1, 1);
for (int i = 0; i < m; i++) {
int v, h; cin >> v >> h;
auto l = lower_bound(V[h].begin(), V[h].end(), L[v]);
auto r = upper_bound(V[h].begin(), V[h].end(), R[v]);
int ans = s[*(r - 1)] ^ s[*(l - 1)];
cout << (ans & (ans - 1) ? "No" : "Yes") << '\n';
}
}
离线解法的优点
有一个有
每天有
在每个点上都有一个观察员。一个人能被点
求每个观察员会观察到多少人?
注意:设某人的终点是
考虑以点
设某人从
每个观察员会观察到多少个上行的人?
某人从
即
我们可以这样看
观察员
每个观察员会观察到多少个下行的人?
某人从
即
我们可以这样看
观察员
设
向路径
然后,对每个点
const int maxn = 3e5 + 5;
int num, L[maxn], R[maxn];
bool is_ancestor(int a, int b) { // a 是不是 b 的祖先
return L[a] <= L[b] && L[b] <= R[a];
}
int anc[maxn][19];
int lca(int u, int v) {
if (is_ancestor(u, v)) return u;
if (is_ancestor(v, u)) return v;
for (int i = 18; i >= 0; i--)
if (anc[u][i] && !is_ancestor(anc[u][i], v))
u = anc[u][i];
return anc[u][0];
}
vector<int> g[maxn];
int depth[maxn];
void dfs(int u, int p) {
L[u] = ++num;
anc[u][0] = p;
depth[u] = depth[p] + 1;
for (int i = 1; i < 19; i++)
anc[u][i] = anc[anc[u][i - 1]][i - 1];
for (int v : g[u])
if (v != p)
dfs(v, u);
R[u] = num;
}
int w[maxn];
int cnt[2 * maxn];
int ans[maxn];
vector<pair<int,int>> op_up[maxn], op_down[maxn];
void get_up(int u, int p) {
int key = w[u] + depth[u];
int before = cnt[key];
for (auto [x, delta] : op_up[u])
cnt[x] += delta;
for (int v : g[u])
if (v != p)
get_up(v, u);
ans[u] += cnt[key] - before;
}
void get_down(int u, int p) {
int key = w[u] - depth[u] + maxn;
int before = cnt[key];
for (auto [x, delta] : op_down[u])
cnt[x + maxn] += delta;
for (int v : g[u])
if (v != p)
get_down(v, u);
ans[u] += cnt[key] - before;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, m; cin >> n >> m;
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++)
cin >> w[i];
dfs(1, 0);
for (int i = 0; i < m; i++) {
int s, t; cin >> s >> t;
int u = lca(s, t);
op_up[s].push_back({depth[s], 1});
op_up[anc[u][0]].push_back({depth[s], -1});
op_down[t].push_back({depth[s] - 2 * depth[u], 1});
op_down[u].push_back({depth[s] - 2 * depth[u], -1});
}
get_up(1, 0);
memset(cnt, 0, sizeof cnt);
get_down(1, 0);
for (int i = 1; i <= n; i++)
cout << ans[i] << ' ';
cout << '\n';
}
有些子树求和问题不适合用前两种方法解决。
其中一类问题是这样的
给你一个有
回答
对每个点
朴素的解法:
时间是
在这个问题里,一个子树里的信息量可以用它的 size(即点的数量)来衡量。
对上面的朴素解法,有这么一种优化。
第一步:预处理
我们把
第二步:从根节点开始对树做一次 DFS。
dfs(u):
对于 u 的每个轻孩子 v:
dfs(v)
清空全局状态
dfs(heavy[u])
把子树 u 的除了重子树之外的部分加入全局状态
DFS 的另一种写法
dfs(u):
对于 u 的每个轻孩子 v:
dfs(v)
dfs(heavy[u])
把子树 u 的除了重子树之外的部分加入全局状态
if (u 不是重孩子)
清空全局状态
const int maxn = 1e5 + 5;
vector<int> g[maxn];
int sz[maxn], heavy_child[maxn];
void get_size(int u, int p) {
sz[u] = 1;
for (int v : g[u])
if (v != p) {
get_size(v, u);
sz[u] += sz[v];
if (sz[heavy_child[u]] < sz[v])
heavy_child[u] = v;
}
}
int cnt[maxn], nc; //全局数据结构
int col[maxn];
int ans[maxn];
int preorder[maxn], num;
void dfs(int u, int p, bool keep) {
int l = num;
preorder[num++] = u;
for (int v : g[u])
if (v != p && v != heavy_child[u])
dfs(v, u, false);
int r = num;
if (heavy_child[u])
dfs(heavy_child[u], u, true);
for (int i = l; i < r; i++)
nc += ++cnt[col[preorder[i]]] == 1;
ans[u] = nc;
if (!keep) {
for (int i = l; i < num; i++)
cnt[col[preorder[i]]] = 0;
nc = 0;
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n; cin >> n;
for (int i = 0; i < n - 1; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++)
cin >> col[i];
get_size(1, 0);
dfs(1, 0, true);
int m; cin >> m;
while (m--) {
int u; cin >> u;
cout << ans[u] << '\n';
}
}