在阅读本文前,你应该对常见的树上问题有一定了解,并且有一定的练习量
前置知识:DFS序、树链剖分、树形DP等
树上启发式合并,又称dsu on tree、small to large merging,是一种能够在 O ( N l o g N ) O(NlogN) O(NlogN) 时间复杂度內回答子树问题的算法,优于树上莫队等。有一句话说得好——“提到子树,就想到树上启发式合并”,这更说明了这一算法的重要性。
我们知道数据结构信息可以合并。给出若干个集合,每次要求将两个集合合并为一个新集合。一种朴素做法是,对于每次 S A → S B S_A \rightarrow S_B SA→SB ,直接将源集合并入目标集合中。在合并次数较多、集合大小不定时,效率的低下是容易预见的。
启发式合并每次将小集合并入大集合,则对于每个元素而言,它每次被移动时,所在的集合大小至少扩大一倍,显然只能扩大 O ( l o g N ) O(logN) O(logN) 次,因此总复杂度 O ( N l o g N ) O(NlogN) O(NlogN)。
考虑单纯地把启发式合并迁移到树上,对于 N N N 个结点,建立 N N N 个集合,在DFS过程中,每当从子结点返回时,就将子结点集合与父结点集合合并,合并过程使用启发式合并优化。这一做法看似是 O ( N l o g N ) O(NlogN) O(NlogN) 的,实则无法达到,且通常至少是 O ( N l o g 2 N ) O(Nlog^2N) O(Nlog2N) 级别的。原因就在于,在前文中,我们只是期望集合的操作都为 O ( 1 ) O(1) O(1) 复杂度,这需要使用数组作为容器。而实际应用通常较为复杂,如果使用数组,空间复杂度常常会达到惊人的 O ( N 2 ) O(N^2) O(N2) 甚至更高,这是不可接受的。考虑更换一些动态分配内存的容器,如std::set、std::map等,这可以降下空间复杂度,但同时,由于其单次操作的时间复杂度为 O ( l o g N ) O(logN) O(logN),总体时间复杂度将升至 O ( N l o g 2 N ) O(Nlog^2N) O(Nlog2N),在 N = 1 0 5 N=10^5 N=105 左右的规模还可以接受。
真正的树上启发式合并又称为静态链分治,主要基于启发式合并的思想,考虑这样一个事实,重儿子所在子树总是最大的,可以作为大集合,轻儿子所在子树作为小集合,于是让子树根每次先继承重儿子的信息,再将轻儿子信息合并上来。这一过程可以使用一些技巧,让我们能够用数组达成目标,时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)。
算法过程如下:
复杂度证明如下:
由树链剖分性质可知,任意一点到根结点路径上的轻边数不超过 O ( l o g N ) O(logN) O(logN),每有一条轻边,该点就会被访问两次,一次删除贡献,一次加入贡献。直接与该点相连的轻边和重边还会使得该点被访问一次。此外,在第一遍预处理DFS中,每个点都被访问一次。故每个点的访问次数不会超过 O ( l o g N ) O(logN) O(logN),总体时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)。
由于可以使用全局数组实现,空间复杂度为 O ( N ) O(N) O(N)。
本文习题中,将尽可能提供两种写法,可以自行比较运行时间和编码难度等优劣。通过对题目的多角度思考,将能够更全面地理解和运用树上启发式合并。并在绝大多数情况下学会偷懒!
题目链接
题意:给定一棵树,树上每个结点有颜色 C i C_i Ci,每次询问以某个结点为根的子树中共有多少种颜色。
思路:树上启发式合并的模板题,可使用set记录颜色再大力合并,也可用数组。
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int c[MAXN], ans[MAXN];
vector<int> G[MAXN];
set<int> s[MAXN];
void dfs(int v, int fa)
{
s[v].insert(c[v]); // 加入根结点贡献
for (auto u : G[v])
{
if (u == fa)
continue;
dfs(u, v);
if (s[v].size() < s[u].size()) // 保证根结点是大集合,使得每次总是小集合并入大集合
swap(s[v], s[u]);
s[v].merge(s[u]); // 合并集合
s[u].clear(); // 清空旧集合,否则最终会爆内存
}
ans[v] = s[v].size();
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n;
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
for (int i = 1; i <= n; i++)
cin >> c[i];
dfs(1, 0);
cin >> m;
while (m--)
{
int v;
cin >> v;
cout << ans[v] << "\n";
}
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int sz[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int c[MAXN], s[MAXN], ans[MAXN], cnt;
vector<int> G[MAXN];
void dfs1(int v, int fa) // 第一遍dfs,预处理必要信息
{
L[v] = ++dfn; // 当前结点dfs序
id[dfn] = v; // dfs序对应的结点编号
sz[v] = 1; // 子树大小
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]]) // 子树更大的为重儿子
son[v] = u;
}
R[v] = dfn; // 当前子树中最大的dfs序(这说明子树內的dfs序是一段连续区间)
}
void dfs2(int v, int fa, bool keep) // 第二遍dfs,启发式合并,keep指示是否保留结点对集合的影响
{
for (auto u : G[v]) // 先遍历轻儿子,不保留其对集合的影响
if (u != son[v] && u != fa)
dfs2(u, v, 0);
if (son[v]) // 然后遍历重儿子,保留其对集合的影响
dfs2(son[v], v, 1);
if (!s[c[v]]) // 加入根结点对集合的贡献
++cnt, s[c[v]] = 1;
for (auto u : G[v]) // 最后重新遍历轻儿子,加入其影响
{
if (u == son[v] || u == fa)
continue;
for (int i = L[u]; i <= R[u]; i++) // 每棵子树内的dfs序都是连续区间
{
int x = id[i]; // 从dfs序反向获取结点编号
if (!s[c[x]]) // 加入子结点对集合的贡献
s[c[x]] = 1, ++cnt;
}
}
ans[v] = cnt; // 记录结点答案
if (!keep) // 即将从轻儿子返回,消去其对集合的影响
{
for (int i = L[v]; i <= R[v]; i++)
s[c[id[i]]] = 0;
cnt = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n;
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
for (int i = 1; i <= n; i++)
cin >> c[i];
dfs1(1, 0);
dfs2(1, 0, 1);
cin >> m;
while (m--)
{
int v;
cin >> v;
cout << ans[v] << "\n";
}
return 0;
}
题目链接
题意:给定一棵树,树上每个结点有颜色 C i C_i Ci,对以每个结点为根的子树,求其中出现次数最多的颜色的编号的和。
思路:
启发式合并,与上一题几乎一样,由于需要记录出现次数,可以使用map,此外需要在合并时手动更新答案。这里我们需要注意,如果你交换了两个集合,显然集合产生的出现次数最值和答案也是需要交换的,所以dfs可以顺便返回子结点情况。
树上启发式合并,思路基本一致,遍历轻儿子时根据计数情况更新答案,不要忘记加入根结点贡献。
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int c[MAXN];
int64_t ans[MAXN];
vector<int> G[MAXN];
map<int, int> mp[MAXN];
pair<int64_t, int> dfs(int v, int fa)
{
mp[v][c[v]] = 1;
int mx = 1;
int64_t sum = c[v];
for (auto u : G[v])
{
if (u == fa)
continue;
auto p = dfs(u, v);
if (mp[v].size() < mp[u].size())
{
swap(mp[v], mp[u]);
mx = p.second, sum = p.first;
}
for (auto [color, cnt] : mp[u])
{
mp[v][color] += cnt;
if (mp[v][color] > mx)
mx = mp[v][color], sum = color;
else if (mp[v][color] == mx)
sum += color;
}
mp[u].clear();
}
ans[v] = sum;
return {sum, mx};
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs(1, 0);
for (int i = 1; i <= n; i++)
cout << ans[i] << " \n"[i == n];
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int sz[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int c[MAXN], mp[MAXN], mxcnt;
int64_t ans[MAXN], sum;
vector<int> G[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void dfs2(int v, int fa, bool keep)
{
for (auto u : G[v])
if (u != son[v] && u != fa)
dfs2(u, v, 0);
if (son[v])
dfs2(son[v], v, 1);
++mp[c[v]];
if (mp[c[v]] > mxcnt)
mxcnt = mp[c[v]], sum = c[v];
else if (mp[c[v]] == mxcnt)
sum += c[v];
for (auto u : G[v])
{
if (u == son[v] || u == fa)
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i];
++mp[c[x]];
if (mp[c[x]] > mxcnt)
mxcnt = mp[c[x]], sum = c[x];
else if (mp[c[x]] == mxcnt)
sum += c[x];
}
}
ans[v] = sum;
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[c[id[i]]] = 0;
mxcnt = 0, sum = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs1(1, 0);
dfs2(1, 0, 1);
for (int i = 1; i <= n; i++)
cout << ans[i] << " \n"[i == n];
return 0;
}
题目链接
题意:给定一棵树,设 d ( u , x ) d(u,x) d(u,x) 为 u u u 子树中到 u u u 距离为 x x x 的结点数。对以每个点为根的子树,求最小的 k k k 使得 d ( u , k ) d(u,k) d(u,k) 最大。
思路:
启发式合并,用map记录子树中所有深度的结点数,合并时更新答案即可,同样地,如果交换了集合,答案也是需要交换的。
树上启发式合并,还是一样,把map换成数组。
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e6 + 5;
int dep[MAXN], ans[MAXN];
vector<int> G[MAXN];
map<int, int> mp[MAXN];
pair<int, int> dfs(int v, int fa)
{
dep[v] = dep[fa] + 1;
mp[v][dep[v]] = 1;
int mxcnt = 1, mndep = dep[v];
for (auto u : G[v])
{
if (u == fa)
continue;
auto p = dfs(u, v);
if (mp[v].size() < mp[u].size())
{
swap(mp[v], mp[u]);
mxcnt = p.first, mndep = p.second;
}
for (auto [h, cnt] : mp[u])
{
mp[v][h] += cnt;
if (mp[v][h] > mxcnt)
mxcnt = mp[v][h], mndep = h;
else if (mp[v][h] == mxcnt && h < mndep)
mndep = h;
}
}
ans[v] = mndep - dep[v];
return {mxcnt, mndep};
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1, v, u; i < n; i++)
{
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs(1, 0);
for (int i = 1; i <= n; i++)
cout << ans[i] << "\n";
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e6 + 5;
int sz[MAXN], dep[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int mp[MAXN], ans[MAXN], mxcnt, mndep;
vector<int> G[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
dep[v] = dep[fa] + 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void dfs2(int v, int fa, bool keep)
{
for (auto u : G[v])
if (u != son[v] && u != fa)
dfs2(u, v, 0);
if (son[v])
dfs2(son[v], v, 1);
++mp[dep[v]];
if (mp[dep[v]] > mxcnt)
mxcnt = mp[dep[v]], mndep = dep[v];
else if (mp[dep[v]] == mxcnt)
mndep = dep[v];
for (auto u : G[v])
{
if (u == son[v] || u == fa)
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i];
++mp[dep[x]];
if (mp[dep[x]] > mxcnt)
mxcnt = mp[dep[x]], mndep = dep[x];
else if (mp[dep[x]] == mxcnt && dep[x] < mndep)
mndep = dep[x];
}
}
ans[v] = mndep - dep[v];
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[dep[id[i]]] = 0;
mxcnt = 0, mndep = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1, v, u; i < n; i++)
{
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs1(1, 0);
dfs2(1, 0, 1);
for (int i = 1; i <= n; i++)
cout << ans[i] << "\n";
return 0;
}
题目链接
题意:给定一棵树,每个结点有小写字母 c h i ch_i chi。每次询问某个结点为根的子树中,某一深度上的所有子结点所带字母重排后能否形成回文串。
思路:显然一个字符串任意重排后若能形成回文串,最多只能有一种字符出现奇数次。考虑本题的暴力解法,大力统计每个结点为根的子树中,所有深度的结点分别对应的字符出现次数,即用map
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 5e5 + 5;
char c[MAXN];
int dep[MAXN], ans[MAXN];
vector<int> G[MAXN];
vector<pair<int, int>> q[MAXN];
map<int, bitset<32>> mp[MAXN];
void dfs(int v, int fa)
{
dep[v] = dep[fa] + 1;
mp[v][dep[v]][c[v]] = 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs(u, v);
if (mp[v].size() < mp[u].size())
swap(mp[v], mp[u]);
for (const auto &[h, bs] : mp[u])
mp[v][h] ^= bs;
mp[u].clear();
}
for (auto [k, id] : q[v])
ans[id] = mp[v][k].count() <= 1;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
for (int i = 2, p; i <= n; i++)
cin >> p, G[p].push_back(i);
string s;
cin >> s;
for (int i = 1; i <= n; i++)
c[i] = s[i - 1] - 'a';
for (int i = 0; i < m; i++)
{
int v, k;
cin >> v >> k, q[v].emplace_back(k, i);
}
dfs(1, 0);
for (int i = 0; i < m; i++)
cout << (ans[i] ? "Yes\n" : "No\n");
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 5e5 + 5;
int sz[MAXN], dep[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int c[MAXN], ans[MAXN];
bitset<32> mp[MAXN];
vector<int> G[MAXN];
vector<pair<int, int>> q[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
dep[v] = dep[fa] + 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void dfs2(int v, bool keep)
{
for (auto u : G[v])
if (u != son[v])
dfs2(u, 0);
if (son[v])
dfs2(son[v], 1);
mp[dep[v]].flip(c[v]);
for (auto u : G[v])
{
if (u == son[v])
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i];
mp[dep[x]].flip(c[x]);
}
}
for (auto [k, id] : q[v])
ans[id] = mp[k].count() <= 1;
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[dep[id[i]]].reset();
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
for (int i = 2, p; i <= n; i++)
cin >> p, G[p].push_back(i);
string s;
cin >> s;
for (int i = 1; i <= n; i++)
c[i] = s[i - 1] - 'a';
for (int i = 0; i < m; i++)
{
int v, k;
cin >> v >> k, q[v].emplace_back(k, i);
}
dfs1(1, 0);
dfs2(1, 1);
for (int i = 0; i < m; i++)
cout << (ans[i] ? "Yes\n" : "No\n");
return 0;
}
题目链接
题意:给定若干棵树,每次询问一个点与多少个点有公共k级祖先。
思路:显然处理每棵树互不干扰(注意,如果让前一棵树根的影响保留在数组中,处理其他树会出错)。一个点与多少个点有公共k级祖先,等价于这个k级祖先 v v v 子树中深度为 d e p [ v ] + k dep[v] + k dep[v]+k 的结点数减一。可以跑树上倍增把每个询问挂到其k级祖先上,直接在祖先上处理询问。记录所有深度对应的结点数,然后启发式合并即可。
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int dep[MAXN], father[MAXN][20], lg[MAXN], ans[MAXN];
vector<int> G[MAXN], rt;
vector<pair<int, int>> q[MAXN], qry[MAXN];
map<int, int> mp[MAXN];
void dfs1(int v, int fa)
{
father[v][0] = fa;
dep[v] = dep[fa] + 1;
for (int i = 1; i <= lg[dep[v]]; i++)
father[v][i] = father[father[v][i - 1]][i - 1];
for (auto u : G[v])
dfs1(u, v);
}
int mv(int v, int k)
{
for (int i = 0; i < 20; i++)
if (k & (1 << i))
v = father[v][i];
return v;
}
void dfs2(int v)
{
mp[v][dep[v]] = 1;
for (auto u : G[v])
{
dfs2(u);
if (mp[v].size() < mp[u].size())
swap(mp[v], mp[u]);
for (auto [dep, cnt] : mp[u])
mp[v][dep] += cnt;
}
for (auto [k, id] : q[v])
{
auto p = mv(v, k);
qry[p].emplace_back(v, id);
}
for (auto [u, id] : qry[v])
ans[id] = mp[v][dep[u]] - 1;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n;
for (int i = 1; i <= n; ++i)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
for (int i = 1, p; i <= n; i++)
{
cin >> p;
if (p == 0)
rt.push_back(i);
else
G[p].push_back(i);
}
cin >> m;
for (int i = 0, v, p; i < m; i++)
{
cin >> v >> p;
q[v].emplace_back(p, i);
}
for (auto v : rt)
dfs1(v, 0), dfs2(v);
for (int i = 0; i < m; i++)
cout << ans[i] << " \n"[i + 1 == m];
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int sz[MAXN], dep[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int father[MAXN][20], lg[MAXN];
int c[MAXN], mp[MAXN], ans[MAXN];
vector<int> G[MAXN], rt;
vector<pair<int, int>> q[MAXN], qry[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
dep[v] = dep[fa] + 1;
father[v][0] = fa;
for (int i = 1; i <= lg[dep[v]]; i++)
father[v][i] = father[father[v][i - 1]][i - 1];
for (auto u : G[v])
{
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
int mv(int v, int k)
{
for (int i = 0; i < 20; i++)
if (k & (1 << i))
v = father[v][i];
return v;
}
void dfs2(int v, bool keep)
{
for (auto u : G[v])
if (u != son[v])
dfs2(u, 0);
if (son[v])
dfs2(son[v], 1);
++mp[dep[v]];
for (auto u : G[v])
{
if (u == son[v])
continue;
for (int i = L[u]; i <= R[u]; i++)
++mp[dep[id[i]]];
}
for (auto [k, id] : q[v])
{
auto p = mv(v, k);
qry[p].emplace_back(v, id);
}
for (auto [u, id] : qry[v])
ans[id] = mp[dep[u]] - 1;
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[dep[id[i]]] = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n;
for (int i = 1; i <= n; ++i)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
for (int i = 1, p; i <= n; i++)
{
cin >> p;
if (p == 0)
rt.push_back(i);
else
G[p].push_back(i);
}
cin >> m;
for (int i = 0, v, p; i < m; i++)
{
cin >> v >> p;
q[v].emplace_back(p, i);
}
for (auto v : rt)
dfs1(v, 0), dfs2(v, 0); // 多棵树,你不能保留根对mp数组影响
for (int i = 0; i < m; i++)
cout << ans[i] << " \n"[i + 1 == m];
return 0;
}
题目链接
题意:给定若干棵树,每个结点有一个字符串代表结点名,每次询问某个结点为根的子树中,所有深度为根结点深度加k的结点有多少不同结点名。
思路:暴力做法是大力统计每个结点为根的子树中,所有深度的结点对应的名字种数,这可以通过map套set完成,使用启发式合并优化,答案就是对应深度map的size(但是询问深度可能超过树高)。如果你还想优化,也许可以用trie或哈希之类的进行字符串映射,但是暴力已经可以通过。
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int dep[MAXN], ans[MAXN];
vector<int> G[MAXN], rt;
vector<pair<int, int>> q[MAXN];
map<int, set<string>> mp[MAXN];
string name[MAXN];
void dfs(int v, int fa)
{
dep[v] = dep[fa] + 1;
mp[v][dep[v]].insert(name[v]);
for (auto u : G[v])
{
if (u == fa)
continue;
dfs(u, v);
if (mp[v].size() < mp[u].size())
swap(mp[v], mp[u]);
for (auto [dep, s] : mp[u])
mp[v][dep].merge(s);
}
for (auto [k, id] : q[v])
{
k += dep[v];
ans[id] = mp[v][k].size();
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n;
for (int i = 1, p; i <= n; i++)
{
cin >> name[i] >> p;
if (p == 0)
rt.push_back(i);
else
G[p].push_back(i);
}
cin >> m;
for (int i = 0, v, k; i < m; i++)
{
cin >> v >> k;
q[v].emplace_back(k, i);
}
for (auto v : rt)
dfs(v, 0);
for (int i = 0; i < m; i++)
cout << ans[i] << "\n";
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int sz[MAXN], dep[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int ans[MAXN];
set<string> mp[MAXN];
vector<int> G[MAXN], rt;
vector<pair<int, int>> q[MAXN];
string name[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
dep[v] = dep[fa] + 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void dfs2(int v, bool keep)
{
for (auto u : G[v])
if (u != son[v])
dfs2(u, 0);
if (son[v])
dfs2(son[v], 1);
mp[dep[v]].insert(name[v]);
for (auto u : G[v])
{
if (u == son[v])
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i];
mp[dep[x]].insert(name[x]);
}
}
for (auto [k, id] : q[v])
{
k += dep[v];
ans[id] = (k < MAXN ? mp[k].size() : 0); // 神坑,深度可能越界
}
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[dep[id[i]]].erase(name[id[i]]);
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n;
for (int i = 1, p; i <= n; i++)
{
cin >> name[i] >> p;
if (p == 0)
rt.push_back(i);
else
G[p].push_back(i);
}
cin >> m;
for (int i = 0, v, k; i < m; i++)
{
cin >> v >> k;
q[v].emplace_back(k, i);
}
for (auto v : rt)
dfs1(v, 0), dfs2(v, 0);
for (int i = 0; i < m; i++)
cout << ans[i] << "\n";
return 0;
}
题目链接
题意:给定一棵树,每个结点有颜色 C i C_i Ci,每次询问某个结点为根的子树中出现次数不少于 k k k 的颜色有多少种。
思路:将询问离线挂到结点上,使用启发式合并求出每个结点为根的子树中所有颜色出现次数后,问题在于如何快速求出出现次数不少于k的颜色数,这可以通过平衡树来解决,因为这是一个经典的查排名问题,虽然pbds提供的红黑树有这个功能,但是常数过大会T,而手写的treap可以胜任。如果用树上启发式合并,实际上更好解决,额外维护一个cnt数组记录出现次数不少于k的颜色数,显然加入每个点的贡献时都会更新数组,清空时不要memset,清空到当前最大计数即可。
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int c[MAXN], ans[MAXN];
vector<int> G[MAXN];
vector<pair<int, int>> q[MAXN];
map<int, int> mp[MAXN];
struct FHQTreap
{
struct Node
{
int ls, rs;
int key, pri;
int size;
Node(int _ls = 0, int _rs = 0, int _key = 0, int _size = 0) : ls(_ls), rs(_rs), key(_key), pri(rand()), size(_size) {}
};
int tot = 0, root = 0, size = 0;
vector<Node> t;
void newNode(int x)
{
if (t.empty())
t.emplace_back(0, 0, 0, 0);
++tot;
t.emplace_back(0, 0, x, 1);
}
void pushUp(int u) { t[u].size = t[t[u].ls].size + t[t[u].rs].size + 1; }
void split(int u, int x, int &l, int &r)
{
if (u == 0)
{
l = r = 0;
return;
}
if (t[u].key <= x)
l = u, split(t[u].rs, x, t[u].rs, r);
else
r = u, split(t[u].ls, x, l, t[u].ls);
pushUp(u);
}
int merge(int l, int r)
{
if (l == 0 || r == 0)
return l + r;
if (t[l].pri > t[r].pri)
{
t[l].rs = merge(t[l].rs, r);
pushUp(l);
return l;
}
else
{
t[r].ls = merge(l, t[r].ls);
pushUp(r);
return r;
}
}
void insert(int x)
{
++size;
int l, r;
split(root, x, l, r);
newNode(x);
int p = merge(l, tot);
root = merge(p, r);
}
void erase(int x)
{
--size;
int l, r, p;
split(root, x, l, r);
split(l, x - 1, l, p);
p = merge(t[p].ls, t[p].rs);
root = merge(merge(l, p), r);
}
int rank(int x)
{
int l, r;
split(root, x - 1, l, r);
auto res = t[l].size + 1;
root = merge(l, r);
return res;
}
void clear() { tot = root = size = 0, t.clear(); }
} tr[MAXN];
void dfs(int v, int fa)
{
mp[v][c[v]] = 1;
tr[v].insert(1);
for (auto u : G[v])
{
if (u == fa)
continue;
dfs(u, v);
if (mp[v].size() < mp[u].size())
swap(mp[v], mp[u]), swap(tr[v], tr[u]);
for (auto [color, val] : mp[u])
{
if (mp[v].count(color))
tr[v].erase(mp[v][color]);
mp[v][color] += val;
tr[v].insert(mp[v][color]);
}
mp[u].clear(), tr[u].clear();
}
for (auto [k, id] : q[v])
ans[id] = tr[v].size - tr[v].rank(k) + 1;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1, v, u; i < n; i++)
{
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
for (int i = 0, v, k; i < m; i++)
cin >> v >> k, q[v].emplace_back(k, i);
dfs(1, 0);
for (int i = 0; i < m; i++)
cout << ans[i] << "\n";
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int sz[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int c[MAXN], mp[MAXN], ans[MAXN], cnt[MAXN], mxcnt;
vector<int> G[MAXN];
vector<pair<int, int>> q[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void dfs2(int v, int fa, bool keep)
{
for (auto u : G[v])
if (u != son[v] && u != fa)
dfs2(u, v, 0);
if (son[v])
dfs2(son[v], v, 1);
++mp[c[v]], ++cnt[mp[c[v]]], mxcnt = max(mxcnt, mp[c[v]]);
for (auto u : G[v])
{
if (u == son[v] || u == fa)
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i];
++mp[c[x]], ++cnt[mp[c[x]]], mxcnt = max(mxcnt, mp[c[x]]);
}
}
for (auto [k, id] : q[v])
ans[id] = cnt[k];
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[c[id[i]]] = 0;
fill(cnt, cnt + mxcnt + 1, 0);
mxcnt = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1, v, u; i < n; i++)
{
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
for (int i = 0, v, k; i < m; i++)
cin >> v >> k, q[v].emplace_back(k, i);
dfs1(1, 0);
dfs2(1, 0, 1);
for (int i = 0; i < m; i++)
cout << ans[i] << "\n";
return 0;
}
题目链接
题意:给定一棵树,定义每个点的答案为以其为根的子树中,所有结点编号从小到大排序后,各相邻两项的差的平方和,即 ∑ i = 1 k − 1 ( a i + 1 − a i ) 2 \sum_{i=1}^{k-1}\left(a_{i+1}-a_i\right)^2 ∑i=1k−1(ai+1−ai)2。
解析:使用启发式合并求出每个点为根的子树的编号集合,与其每次暴力遍历集合求答案,不如在合并时一边逐个加入小集合中的编号,一边对当前加入的编号,增加它对前驱和后继的贡献,并减去前驱和后继二者之间的贡献,注意判断没有前驱或者后继的情况。本题必须用set,所以不妨直接写双log法。
参考代码:
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
vector<int> G[MAXN];
set<int> s[MAXN];
int64_t ans[MAXN];
int64_t dfs(int v)
{
int64_t sum = 0;
s[v].insert(v);
for (auto u : G[v])
{
auto res = dfs(u);
if (s[v].size() < s[u].size())
swap(s[v], s[u]), sum = res;
for (auto num : s[u])
{
int mid = 0;
auto it1 = s[v].insert(num).first, it2 = it1;
if (it1 != s[v].begin())
--it1, sum += 1ll * (num - *it1) * (num - *it1), ++mid;
if (++it2 != s[v].end())
sum += 1ll * (num - *it2) * (num - *it2), ++mid;
if (mid == 2)
sum -= 1ll * (*it1 - *it2) * (*it1 - *it2);
}
s[u].clear();
}
ans[v] = sum;
return sum;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 2, p; i <= n; i++)
cin >> p, G[p].push_back(i);
dfs(1);
for (int i = 1; i <= n; i++)
cout << ans[i] << "\n";
return 0;
}
题目链接
题意:给定一棵树,每个点有权值 a i a_i ai,求以每个结点为根的子树的聚集方差值。聚集方差定义为,对于一个可重集中的所有元素,分别找到权值最接近的元素,作差后平方并累加至答案中。如可重集 { 0 , 1 , 1 , 3 } \{0,1,1,3\} {0,1,1,3} 中, < 0 , 1 > <0,1> <0,1>, < 1 , 1 > <1,1> <1,1>, < 1 , 1 > <1,1> <1,1>, < 3 , 1 > <3,1> <3,1> 分别产生 1 , 0 , 0 , 4 1,0,0,4 1,0,0,4 的贡献。
思路:比较明显的启发式合并,麻烦之处在于合并时重新计算贡献。对于每个新加入的元素 v v v,显然要找到其前驱 p r e pre pre 和后继 s u c suc suc, v v v 的贡献在其中产生,注意判断没有前驱或后继的情况。对于前驱 p r e pre pre 和后继 s u c suc suc,要减去其原本带来的贡献,那么还要判断 p r e pre pre 是否有前驱, s u c suc suc 是否有后继,然后重新加上 p r e pre pre 和 s u c suc suc 的新贡献。总而言之就是十分麻烦。和上一题同样的原因,直接写双log法即可。
参考代码:
#include
using namespace std;
using iter = multiset<int>::iterator;
constexpr int MAXN = 3e5 + 5;
vector<int> G[MAXN];
multiset<int> s[MAXN];
int64_t ans[MAXN];
int64_t cal(const multiset<int> &ms, iter p)
{
if (p == ms.begin())
{
auto suc = next(p);
return 1ll * (*p - *suc) * (*p - *suc);
}
else if (next(p) == ms.end())
{
auto pre = prev(p);
return 1ll * (*p - *pre) * (*p - *pre);
}
auto pre = prev(p), suc = next(p);
auto mn = min(abs(*pre - *p), abs(*suc - *p));
return 1ll * mn * mn;
}
int64_t dfs(int v)
{
int64_t sum = 0;
for (auto u : G[v])
{
auto res = dfs(u);
if (s[v].size() < s[u].size())
swap(s[v], s[u]), sum = res;
for (auto num : s[u])
{
auto p = s[v].insert(num), it1 = p, it2 = next(p);
sum += cal(s[v], p);
if (it1 != s[v].begin())
{
--it1;
int64_t mn = 1e18;
if (it1 != s[v].begin())
{
auto tmp = prev(it1);
mn = 1ll * (*tmp - *it1) * (*tmp - *it1);
}
auto tmp = next(p);
if (tmp != s[v].end())
mn = min(mn, 1ll * (*tmp - *it1) * (*tmp - *it1));
if (mn != 1e18)
sum -= mn;
sum += cal(s[v], it1);
}
if (it2 != s[v].end())
{
int64_t mn = 1e18;
auto tmp = next(it2);
if (tmp != s[v].end())
mn = 1ll * (*tmp - *it2) * (*tmp - *it2);
tmp = it1;
if (p != s[v].begin())
mn = min(mn, 1ll * (*tmp - *it2) * (*tmp - *it2));
if (mn != 1e18)
sum -= mn;
sum += cal(s[v], it2);
}
}
s[u].clear();
}
ans[v] = sum;
return sum;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 2, p; i <= n; i++)
cin >> p, G[p].push_back(i);
for (int i = 1, x; i <= n; i++)
cin >> x, s[i].insert(x);
dfs(1);
for (int i = 1; i <= n; i++)
cout << ans[i] << "\n";
return 0;
}
题目链接
题意:给定一棵树,每个点有颜色 C i C_i Ci,每次询问某个结点为根的子树內,与根距离不超过k的点中,一共有多少种颜色。k是定值。
思路:
方法一,线段树合并。可以想到,每个点都会对它的1到k级祖先产生贡献(当然对自己也有贡献),这符合树上差分的性质,那么就可以利用线段树合并,自底向上快速求出每个结点的所有颜色计数情况。在权值线段树中再额外维护一个属性,统计所有计数不为0的单点的数量,即为答案。
方法二,启发式合并。整体思路依旧是利用树上差分,只是将权值线段树换为map,合并后计数为0的点需要删去,map的size即为答案。
方法三,树上启发式合并,遍历轻儿子的过程中,我们很容易想到只需要遍历前 k k k 层的子结点即可,问题在于重儿子的贡献只能保留 k k k 层如何实现,考虑到每个结点在 k + 1 k+1 k+1 级祖先处被消除贡献后,更高级的祖先都不会持有该贡献,因此我们跑倍增求出每个结点的 k + 1 k+1 k+1 级祖先,将结点编号加入祖先的vector里,到时消除即可。但是,我们无法区分vector中的哪些点属于重儿子所在子树,这可能导致我们多消除一些贡献。因此轻儿子可以改为遍历前 k + 1 k+1 k+1 层结点,然后统一消去第 k + 1 k+1 k+1 层的所有结点贡献,无论轻重。
参考代码(线段树合并):
#include
using namespace std;
#define ls tr[rt].lc
#define rs tr[rt].rc
constexpr int MAXN = 1e5 + 5;
struct Node
{
int lc, rc;
int val, cnt;
} tr[MAXN << 5];
int n, m, k;
int tp[MAXN], tot;
int dep[MAXN], father[MAXN][20], lg[MAXN];
int c[MAXN], ans[MAXN];
vector<int> G[MAXN];
void pushUp(int rt)
{
tr[rt].val = tr[ls].val + tr[rs].val;
tr[rt].cnt = tr[ls].cnt + tr[rs].cnt;
}
void add(int rt, int val, int C, int l, int r)
{
if (l == r)
{
tr[rt].val += C;
tr[rt].cnt = (tr[rt].val > 0);
return;
}
int mid = (l + r) >> 1;
if (val <= mid)
{
if (!ls) ls = ++tot;
add(ls, val, C, l, mid);
}
else
{
if (!rs) rs = ++tot;
add(rs, val, C, mid + 1, r);
}
pushUp(rt);
}
int merge(int p, int q, int l, int r)
{
if (!p || !q)
return p + q;
if (l == r)
{
tr[p].val += tr[q].val;
tr[p].cnt = (tr[p].val > 0);
return p;
}
int mid = (l + r) >> 1;
tr[p].lc = merge(tr[p].lc, tr[q].lc, l, mid);
tr[p].rc = merge(tr[p].rc, tr[q].rc, mid + 1, r);
pushUp(p);
return p;
}
int query(int rt, int L, int R, int l, int r)
{
if (L <= l && r <= R)
return tr[rt].cnt;
int mid = (l + r) >> 1;
int res = 0;
if (L <= mid)
res += query(ls, L, R, l, mid);
if (R > mid)
res += query(rs, L, R, mid + 1, r);
return res;
}
void dfs1(int v, int fa)
{
tp[v] = ++tot;
father[v][0] = fa;
dep[v] = dep[fa] + 1;
for (int i = 1; i <= lg[dep[v]]; i++)
father[v][i] = father[father[v][i - 1]][i - 1];
for (auto u : G[v])
if (u != fa)
dfs1(u, v);
}
int mv(int v, int k)
{
for (int i = 19; i >= 0; i--)
if (k & (1 << i))
v = father[v][i];
return v;
}
void dfs2(int v, int fa)
{
for (auto u : G[v])
{
if (u != fa)
dfs2(u, v), merge(tp[v], tp[u], 1, n);
}
auto p = mv(v, k + 1);
add(tp[v], c[v], 1, 1, n);
if (p != 0)
add(tp[p], c[v], -1, 1, n);
ans[v] = query(tp[v], 1, n, 1, n);
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs1(1, 0);
dfs2(1, 0);
cin >> m;
while (m--)
{
int x;
cin >> x;
cout << ans[x] << "\n";
}
return 0;
}
参考代码(启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int n, m, k;
int dep[MAXN], father[MAXN][20], lg[MAXN];
int c[MAXN], ans[MAXN];
vector<int> G[MAXN];
map<int, int> mp[MAXN];
void dfs1(int v, int fa)
{
father[v][0] = fa;
dep[v] = dep[fa] + 1;
for (int i = 1; i <= lg[dep[v]]; i++)
father[v][i] = father[father[v][i - 1]][i - 1];
for (auto u : G[v])
if (u != fa)
dfs1(u, v);
}
int mv(int v, int k)
{
for (int i = 19; i >= 0; i--)
if (k & (1 << i))
v = father[v][i];
return v;
}
void dfs2(int v, int fa)
{
for (auto u : G[v])
{
if (u == fa)
continue;
dfs2(u, v);
vector<int> del;
if (mp[v].size() < mp[u].size())
swap(mp[v], mp[u]);
for (auto [color, cnt] : mp[u])
{
mp[v][color] += cnt;
if (mp[v][color] == 0)
del.push_back(color);
}
for (auto color : del)
mp[v].erase(color);
}
auto p = mv(v, k + 1);
++mp[v][c[v]], --mp[p][c[v]];
ans[v] = mp[v].size();
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs1(1, 0);
dfs2(1, 0);
cin >> m;
while (m--)
{
int x;
cin >> x;
cout << ans[x] << "\n";
}
return 0;
}
参考代码(树上启发式合并):
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int n, m, k;
int sz[MAXN], dep[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int father[MAXN][20], lg[MAXN];
int c[MAXN], mp[MAXN], ans[MAXN], cnt;
vector<int> G[MAXN], del[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
dep[v] = dep[fa] + 1;
father[v][0] = fa;
for (int i = 1; i <= lg[dep[v]]; i++)
father[v][i] = father[father[v][i - 1]][i - 1];
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
int mv(int v, int k)
{
for (int i = 19; i >= 0; i--)
if (k & (1 << i))
v = father[v][i];
return v;
}
void dfs2(int v, int fa, bool keep)
{
for (auto u : G[v])
if (u != son[v] && u != fa)
dfs2(u, v, 0);
if (son[v])
dfs2(son[v], v, 1);
del[mv(v, k + 1)].push_back(v);
if (++mp[c[v]] == 1)
++cnt;
for (auto u : G[v])
{
if (u == son[v] || u == fa)
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i];
if (dep[x] - dep[v] <= k + 1)
{
if (++mp[c[x]] == 1)
++cnt;
}
}
}
for (auto u : del[v])
{
if (--mp[c[u]] == 0)
--cnt;
}
ans[v] = cnt;
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[c[id[i]]] = 0;
cnt = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs1(1, 0);
dfs2(1, 0, 1);
cin >> m;
while (m--)
{
int x;
cin >> x;
cout << ans[x] << "\n";
}
return 0;
}
题目链接
题意:给定一棵树,每个结点有权值 a i a_i ai,可以进行任意次操作,每次操作将一个点的权值更换为任意正整数,求最小操作次数,使得任意两点路径异或和不为0。
思路:注意观察本题操作性质,可以任意更换权值,那么我们一定可以使得一个点的权值被更换后,所有经过它的路径异或和均不为0。由此我们得到一个贪心思想,以一个点为根的子树中,如果有经过根的路径异或和为0,则只需要对根结点操作一次,这个贪心过程是自底向上检查的。于是问题转换为如何快速支持查询。固定整个树根为1,令 X O R [ p ] XOR[p] XOR[p] 表示从1到 p p p 的路径异或和。设以 v v v 为根的子树中,存在点 x , y x,y x,y 使得 x − v − y x-v-y x−v−y 的路径异或和为0,则满足 X O R [ x ] ⊕ X O R [ y ] ⊕ a [ v ] = = 0 XOR[x] \oplus XOR[y] \oplus a[v] == 0 XOR[x]⊕XOR[y]⊕a[v]==0。
这是一个经典的问题,我们只需要用 s e t [ v ] set[v] set[v] 记录当前已经遍历的 v v v 的子树中,所有结点的 X O R [ c h i l d ] XOR[child] XOR[child] 值,对于新遍历的子树,枚举其 s e t [ u ] set[u] set[u] 中所有值 v a l val val,若 s e t [ v ] set[v] set[v] 中存在 a [ v ] ⊕ v a l a[v] \oplus val a[v]⊕val,则存在异或和为0的路径,说明我们需要对 v v v 使用一次操作,则 v v v 所在子树内的所有结点都不会与整棵树上的其他结点形成异或和为0的路径,因此 s e t [ v ] set[v] set[v] 相当于被清空。这个过程可以使用启发式合并优化。
由于 a i a_i ai 范围达到 2 30 2^{30} 230,需要使用set,因此直接写双log法更方便。
参考代码:
#include
using namespace std;
constexpr int MAXN = 2e5 + 5;
vector<int> G[MAXN];
int a[MAXN], XOR[MAXN], ans;
set<int> s[MAXN];
void dfs(int v, int fa)
{
XOR[v] = XOR[fa] ^ a[v];
s[v].insert(XOR[v]);
bool modify = 0;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs(u, v);
if (s[v].size() < s[u].size())
swap(s[v], s[u]);
for (auto val : s[u])
{
if (s[v].count(a[v] ^ val))
modify = 1;
}
if (!modify)
s[v].merge(s[u]);
s[u].clear();
}
if (modify)
s[v].clear(), ++ans;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i < n; i++)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs(1, 0);
cout << ans << endl;
return 0;
}
题目链接
题意:给定一棵树,每条树边上有小写字母 c h i ch_i chi,求每个结点为根的子树中最长的Dokhtar-kosh路径的长度。Dokhtar-kosh路径要求路径上的字符重排后可形成回文串。
思路:与上一题整体思路大同小异,同时也可认为是 CF570D - Tree Requests 的加强版。首先可以将边权下放至点权,即令深度更大的结点带上边权值,并预处理出根到每个点的路径异或和。考虑子树如何给根带来贡献,显然根的答案可以对子树答案取max,这是不经过根的路径;对于经过根的路径,同上一题一样,使用启发式合并维护map以快速查询,map中保存各个异或值所对应的最大深度。合并时,假设当前树根为 r t rt rt,正在遍历的子树中的点为 v v v,枚举 X O R [ v ] XOR[v] XOR[v] 变化每位的情况及保持不变的情况,设其值为 v a l val val,若能在map中找到 v a l val val,说明有符合的点(因为原本是边权,你并不需要再异或上 X O R [ r t ] XOR[rt] XOR[rt]),它们间的路径长就是 d e p [ v ] + m p [ v a l ] − d e p [ r t ] ∗ 2 dep[v]+mp[val]-dep[rt]*2 dep[v]+mp[val]−dep[rt]∗2。
注:本题正解就是树上启发式合并,由于题目数据范围达到 5 e 5 5e5 5e5 级别,且查询复杂,不写单log法将被卡至TLE(因为你不得不使用map等数据结构,枚举变化也需要log时间,你将达到3log)。作者特别标明:
Corner case: You must use an array, no map or unordered map for bag, these solutions got TLE.
边界情况: 你必须使用数组,不能使用map和unordered_map等数据结构,这些解法都会TLE。
参考代码:
#include
using namespace std;
constexpr int MAXN = 5e5 + 5;
int dep[MAXN], sz[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int XOR[MAXN], mp[1 << 22], ans[MAXN];
vector<int> G[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
dep[v] = dep[fa] + 1;
XOR[v] = XOR[fa] ^ XOR[v];
for (auto u : G[v])
{
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void dfs2(int v, bool keep)
{
for (auto u : G[v])
if (u != son[v])
dfs2(u, 0), ans[v] = max(ans[v], ans[u]);
if (son[v])
dfs2(son[v], 1), ans[v] = max(ans[v], ans[son[v]]);
mp[XOR[v]] = max(mp[XOR[v]], dep[v]);
ans[v] = max(ans[v], mp[XOR[v]] - dep[v]);
for (int i = 0; i < 22; i++)
{
auto val = XOR[v] ^ (1 << i);
if (mp[val])
ans[v] = max(ans[v], mp[val] - dep[v]);
}
for (auto u : G[v])
{
if (u == son[v])
continue;
for (int i = L[u]; i <= R[u]; i++)
{
auto val = XOR[id[i]], h = dep[id[i]];
for (int j = 0; j < 22; j++)
{
if (mp[val ^ (1 << j)])
ans[v] = max(ans[v], h + mp[val ^ (1 << j)] - dep[v] * 2);
}
if (mp[val])
ans[v] = max(ans[v], h + mp[val] - dep[v] * 2);
}
for (int i = L[u]; i <= R[u]; i++)
{
auto val = XOR[id[i]];
mp[val] = max(mp[val], dep[id[i]]);
}
}
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
mp[XOR[id[i]]] = 0;
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 2; i <= n; i++)
{
int p;
char ch;
cin >> p >> ch;
G[p].push_back(i), XOR[i] = 1 << (ch - 'a');
}
dfs1(1, 0);
dfs2(1, 1);
for (int i = 1; i <= n; i++)
cout << ans[i] << " \n"[i == n];
return 0;
}
题目链接
题意:给定一棵树,每个点有权值 a i a_i ai,对于所有满足 i < j i
思路:显然两点间路径形态必须是 “/\” 形,而不能是直链(因为输入保证 a i ≠ 0 a_i \neq 0 ai=0)。通过启发式合并,我们很容易判断对于当前正在遍历的子树中的点 u u u,是否存在符合的点 v v v,以及这样的 v v v 的数量,但问题在于如何求贡献。考虑一种暴力做法,把点丢到点权对应的vector里,这样就能大力遍历编号求异或和,理论上会被卡T,但是数据比较水(所以这题也从银牌题掉到了铜牌题)。
正解是将位拆开考虑,这在涉及位运算的题目中是一种十分常见的思想。对于符合的两个点 v , u v,u v,u,设其二进制位为 v b i v_{b_i} vbi 和 u b i u_{b_i} ubi,显然它们对答案的贡献为 ∑ i = 0 k ( 1 < < i ) ∗ ( v b i ⊕ u b i ) \sum_{i=0}^{k} (1<∑i=0k(1<<i)∗(vbi⊕ubi),故按位考虑并不会改变结果。因此,对于每个异或值,分别记录所有点编号每位上0、1的个数之和,这样对于当前点 u u u,只需查看 m p [ a [ u ] ⊕ a [ l c a ] ] mp[a[u] \oplus a[lca]] mp[a[u]⊕a[lca]] 中每位情况即可求出 u u u 的贡献。
参考代码:
#include
using namespace std;
constexpr int MAXN = 1e5 + 5;
int sz[MAXN], son[MAXN], L[MAXN], R[MAXN], id[MAXN], dfn;
int a[MAXN], mp[1 << 20][17][2];
int64_t ans;
vector<int> G[MAXN];
void dfs1(int v, int fa)
{
L[v] = ++dfn;
id[dfn] = v;
sz[v] = 1;
for (auto u : G[v])
{
if (u == fa)
continue;
dfs1(u, v);
sz[v] += sz[u];
if (sz[u] > sz[son[v]])
son[v] = u;
}
R[v] = dfn;
}
void add(int v)
{
auto &dst = mp[a[v]];
for (int i = 0; i < 17; i++)
(v & (1 << i)) ? ++dst[i][1] : ++dst[i][0];
}
void del(int v)
{
auto &dst = mp[a[v]];
for (int i = 0; i < 17; i++)
(v & (1 << i)) ? --dst[i][1] : --dst[i][0];
}
void dfs2(int v, int fa, bool keep)
{
for (auto u : G[v])
if (u != son[v] && u != fa)
dfs2(u, v, 0);
if (son[v])
dfs2(son[v], v, 1);
add(v);
for (auto u : G[v])
{
if (u == son[v] || u == fa)
continue;
for (int i = L[u]; i <= R[u]; i++)
{
int x = id[i], val = a[x];
auto &dst = mp[val ^ a[v]];
for (int j = 0; j < 17; j++)
ans += (1ll << j) * dst[j][!(x & (1 << j))];
}
for (int i = L[u]; i <= R[u]; i++)
add(id[i]);
}
if (!keep)
{
for (int i = L[v]; i <= R[v]; i++)
del(id[i]);
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1, v, u; i < n; i++)
{
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
}
dfs1(1, 0);
dfs2(1, 0, 1);
cout << ans << endl;
return 0;
}
树上启发式合并是解决相当一类子树问题的利器,在诸多比赛题目中都有广泛的应用,能够用 O ( N l o g N ) O(NlogN) O(NlogN) 或 O ( N l o g 2 N ) O(Nlog^2N) O(Nlog2N) 的时间复杂度解决问题,有时也可转换为线段树合并等。在绝大多数情况下, O ( N l o g 2 N ) O(Nlog^2N) O(Nlog2N) 的写法都不会被卡,所以为了追求过题效率,可以大力双log法。
在没有思路时,牢记一句话:“提到子树,就想到树上启发式合并。”