长链剖分
长链剖分在维护有关深度的信息时具有显著优势。
定义长链剖分中长儿子为子树内深度最大的儿子,不难使用类似重链剖分的方式求出长儿子:
void dfs1(int u, int f) {
fa[u] = f, len[u] = 1;
for (int v : G.e[u]) {
if (v == f)
continue;
dfs1(v, u);
if (len[v] >= len[u])
son[u] = v, len[u] = len[v] + 1;
}
}
性质
- 一个节点到所在长链底部的路径为其到子树内所有节点的路径中最长的一条。
- 任意节点 \(u\) 的 \(k\) 级祖先 \(f\) 所在链的长度一定 \(\geq k\) 。
- 任意节点到达根节点经过的长链数是 \(O(\sqrt{n})\) 的,即最多经过 \(O(\sqrt{n})\) 条虚边。
应用
树上 k 级祖先
长链剖分可以做到 \(O(n \log n)\) 预处理, \(O(1)\) 查询。
预处理:
- 每条长链的顶点和深度。
- 倍增求出每个点的 \(2^k\) 级祖先。
- 对于每条链,如果其长度为 \(len\) ,那么在顶点处记录顶点向上的 \(len\) 个祖先和向下的 \(len\) 个链上的点。
询问时利用倍增数组先将 \(x\) 跳到 \(x\) 的 \(2^{h_k}\) 级祖先(其中 \(h_k\) 表示 \(k\) 在二进制下的最高位)。设剩下还有 \(k^{\prime}\) 级,显然有 \(k^{\prime} < 2^{h_k}\) 。因此此时 \(x\) 所在的长链长度一定 \(\geq 2^{h_k} > k^{\prime}\) ,因此可以先将 \(x\) 跳到 \(x\) 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。
#include <bits/stdc++.h>
typedef long long ll;
typedef unsigned int uint;
using namespace std;
const int N = 5e5 + 7, LOGN = 21;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N][LOGN];
int buf[N << 2], *up[N], *down[N], *now = buf;
int LOG[N], dep[N], len[N], son[N], top[N];
ll ans;
uint s;
int n, q, root;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline uint get(uint x) {
return x ^= x << 13, x ^= x >> 17, x ^= x << 5, s = x;
}
inline void prework() {
LOG[0] = -1;
for (int i = 1; i <= n; ++i)
LOG[i] = LOG[i >> 1] + 1;
}
void dfs1(int u) {
dep[u] = dep[fa[u][0]] + 1, len[u] = 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u]) {
dfs1(v);
if (len[v] >= len[u])
son[u] = v, len[u] = len[v] + 1;
}
}
void dfs2(int u, int topf) {
top[u] = topf;
if (u == topf) {
up[u] = now, now += len[u];
for (int i = 1, x = fa[u][0]; i <= len[u]; ++i, x = fa[x][0])
up[u][i] = x;
down[u] = now, now += len[u];
for (int i = 1, x = son[u]; i <= len[u]; ++i, x = son[x])
down[u][i] = x;
}
if (son[u])
dfs2(son[u], topf);
for (int v : G.e[u])
if (v != son[u])
dfs2(v, v);
}
inline int query(int x, int k) {
if (!k)
return x;
x = fa[x][LOG[k]], k -= 1 << LOG[k];
if (!k)
return x;
k -= dep[x] - dep[top[x]], x = top[x];
return k ? (k > 0 ? up[x][k] : down[x][-k]) : x;
}
signed main() {
n = read(), q = read(), s = read<uint>();
prework();
for (int i = 1; i <= n; ++i) {
if (fa[i][0] = read())
G.insert(fa[i][0], i);
else
root = i;
}
dfs1(root), dfs2(root, root);
int lstans = 0;
for (int i = 1; i <= q; ++i) {
int x = (get(s) ^ lstans) % n + 1, k = (get(s) ^ lstans) % dep[x];
ans ^= 1ll * i * (lstans = query(x, k));
}
printf("%lld", ans);
return 0;
}
优化 DP
对于一类下标为深度的 DP,如果直接朴素实现,则可以通过构造一条链将时间复杂度卡到 \(O(n^2)\) 。
注意到一个点利用数组指针的变换,可以直接 \(O(1)\) 继承一个长儿子的信息,并添加当前点的信息。
考虑对每个点都继承长儿子的信息,暴力合并其他儿子的信息。
由于暴力合并的复杂度为短链的长度,所有链的长度之和是 \(O(n)\) 的,故暴力合并带来的总复杂度为 \(O(n)\) 。
求子树内深度的众数。
\(n \leq 10^6\)
设 \(f_{i, j}\) 为 \(i\) 的子树内到 \(i\) 距离为 \(j\) 的节点数量,则:
\[\begin{aligned} f_{i, 0} &= 1 \\ f_{i, j} &= \sum_{v \in son_u} f_{v, j - 1} \end{aligned} \]考虑优化,对于一个节点,先遍历它的重儿子,继承重儿子的结果,并添加当前点的信息。然后遍历轻儿子,将轻儿子的结果并到当前点上。
因为每条重链都只合并一次,时间复杂度 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int buf[N], *f[N], *now = buf;
int fa[N], len[N], son[N], ans[N];
int n;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs1(int u, int f) {
fa[u] = f, len[u] = 1;
for (int v : G.e[u]) {
if (v == f)
continue;
dfs1(v, u);
if (len[v] >= len[u])
son[u] = v, len[u] = len[v] + 1;
}
}
void dfs2(int u) {
auto cmp = [&](const int &a, const int &b) {
return (f[u][a] == f[u][b] ? a < b : f[u][a] > f[u][b]) ? a : b;
};
f[u][0] = 1, ans[u] = 0;
if (son[u])
f[son[u]] = f[u] + 1, dfs2(son[u]), ans[u] = cmp(ans[u], ans[son[u]] + 1);
for (int v : G.e[u]) {
if (v == fa[u] || v == son[u])
continue;
f[v] = now, now += len[v], dfs2(v);
for (int i = 0; i < len[v]; ++i)
f[u][i + 1] += f[v][i], ans[u] = cmp(ans[u], i + 1);
}
}
signed main() {
n = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
dfs1(1, n);
f[1] = now, now += len[1], dfs2(1);
for (int i = 1; i <= n; ++i)
printf("%d\n", ans[i]);
return 0;
}
给定一棵 \(n\) 个节点的有根树。定义:
- 设 \(a \neq b\) ,若 \(a\) 是 \(b\) 的祖先,那么称“\(a\) 比 \(b\) 更为厉害”。
- 设 \(a \neq b\) ,若 \(a\) 与 \(b\) 在树上的距离不超过某个给定常数 \(x\),那么称“ \(a\) 与 \(b\) 彼此彼此”。
\(q\) 次询问,每次给出 \(p\) 和 \(k\),求有多少个有序二元组 \((b, c)\) 满足:
- \(p, b, c\) 互异。
- \(p\) 和 \(b\) 都比 \(c\) 更为厉害。
- \(p\) 和 \(b\) 彼此彼此,这里彼此彼此中的常数为给定的 \(k\) 。
\(n, q, \leq 3\times 10^5\)
首先,\(b\) 比 \(p\) 更为厉害的情况是好处理的,\(b\) 能任取 \(p\) 向上 \(k\) 个点,\(c\) 能在 \(p\) 子树中任取。下面讨论 \(p\) 比 \(b\) 更为厉害的情况。
设 \(f_{u, k} = \sum_{v \in T(u) - \{ u \}} [dist(u, v) \leq k] (siz_v - 1)\) 表示对于所有 \(v\) 满足 \(u\) 比 \(v\) 更为厉害且彼此彼此的答案,则:
\[f_{u, k} = \sum_{v \in son_u} f_{v, k - 1} + siz_v - 1 \]长链剖分优化即可,实现时对于 \(siz_v - 1\) 的部分可以打全局标记实现。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<pair<int, int> > qry[N];
ll buf[N], *f[N], *now = buf;
ll tag[N], ans[N];
int fa[N], dep[N], len[N], son[N], siz[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs1(int u, int f) {
fa[u] = f, dep[u] = dep[f] + 1, len[u] = 1, siz[u] = 1;
for (int v : G.e[u]) {
if (v == f)
continue;
dfs1(v, u), siz[u] += siz[v];
if (len[v] >= len[u])
son[u] = v, len[u] = len[v] + 1;
}
}
void dfs2(int u) {
if (son[u])
f[son[u]] = f[u] + 1, dfs2(son[u]), tag[u] = tag[son[u]] + siz[son[u]] - 1;
for (int v : G.e[u]) {
if (v == fa[u] || v == son[u])
continue;
f[v] = now, now += len[v], dfs2(v);
tag[u] += tag[v] + siz[v] - 1;
for (int j = 0; j < len[v]; ++j)
f[u][j + 1] += f[v][j];
}
f[u][0] = -tag[u];
for (auto it : qry[u])
ans[it.second] += f[u][min(it.first, len[u] - 1)] + tag[u];
}
signed main() {
n = read(), m = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
dfs1(1, 0);
for (int i = 1; i <= m; ++i) {
int x = read(), k = read();
ans[i] = 1ll * min(dep[x] - 1, k) * (siz[x] - 1);
qry[x].emplace_back(k, i);
}
f[1] = now, now += len[1], dfs2(1);
for (int i = 1; i <= m; ++i)
printf("%lld\n", ans[i]);
return 0;
}
P5904 [POI2014] HOT-Hotels 加强版
给出一棵有 \(n\) 个点的树,求有多少组点 \((i,j,k)\) 满足 \(i,j,k\) 两两之间的距离都相等。 \((i,j,k)\) 与 \((i,k,j)\) 算作同一组。
\(n \leq 10^5\)
设 \(f_{i, j}\) 表示满足 \(x\) 在 \(i\) 子树中且 \(dist(x, i) = j\) 的点的数量,\(g_{i, j}\) 表示满足 \(x, y\) 在 \(i\) 的子树内且 \(dist(lca(x, y), x) = dist(lca(x, y), y) = dist(lca(x, y), i) + j\) 的无序数对 \((x, y)\) 的数量。则:
\[ans \leftarrow g_{u, 0} \\ ans \leftarrow \sum_{v, w \in son(u), v \neq w} f_{v, i - 1} \times g_{w, i + 1} \\ g_{u, i - 1} \leftarrow \sum_{v \in son(u)} g_{v, i} \\ g_{u, i + 1} \leftarrow \sum_{v, w \in son(u), v \neq w} f_{v, i} \times f_{w, i} \\ f_{u, i + 1} \leftarrow \sum_{v \in son(u)} f_{v, i} \]实现细节较多。
关于 \(g\) 开空间的解释:由于 g[son[u]] = g[u] - 1
,所以 \(g_u\) 前面要开 \(len_u\) ;由于 \(g_{u, i}\) 中 \(i\) 的取值为 \([0, len_u)\) ,所以 \(g_i\) 后面也要开 \(len_u\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
ll buf[N * 3], *f[N], *g[N];
int fa[N], len[N], son[N];
ll *now = buf, ans;
int n;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs1(int u, int f) {
fa[u] = f, len[u] = 1;
for (int v : G.e[u]) {
if (v == f)
continue;
dfs1(v, u);
if (len[v] + 1 > len[u])
len[u] = len[v] + 1, son[u] = v;
}
}
void dfs2(int u) {
if (son[u]) {
f[son[u]] = f[u] + 1, g[son[u]] = g[u] - 1;
dfs2(son[u]);
}
f[u][0] = 1;
for (int v : G.e[u]) {
if (v == fa[u] || v == son[u])
continue;
f[v] = now, now += len[v];
now += len[v], g[v] = now, now += len[v];
dfs2(v);
for (int i = 0; i < len[v]; ++i)
ans += g[u][i + 1] * f[v][i];
for (int i = 1; i + 1 < len[v]; ++i)
ans += f[u][i] * g[v][i + 1];
for (int i = 1; i < len[v]; ++i)
g[u][i - 1] += g[v][i];
for (int i = 0; i < len[v]; ++i)
g[u][i + 1] += f[u][i + 1] * f[v][i];
for (int i = 0; i < len[v]; ++i)
f[u][i + 1] += f[v][i];
}
ans += g[u][0];
}
signed main() {
n = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
dfs1(1, 0);
f[1] = now, now += len[1];
now += len[1], g[1] = now, now += len[1];
dfs2(1);
printf("%lld", ans);
return 0;
}
标签:长链,剖分,int,len,son,read,ans,now
From: https://www.cnblogs.com/wshcl/p/18536466/LongChainDivide