Virtual Tree 学习笔记
Virtual Tree 略解
引子
像大部分虚树介绍一样,以一道 烂大街 的例题 [SDOI2011]消耗战 引入:
Description
给定一棵大小为 n 的树,每条边有边权,m 组询问,每次询问给定 k 个 关键点,要求切断 1 与所有关键点的路径,求最小代价。
Data Constraint
2≤n≤2.5×105,m≥1,∑k≤5×105,1≤k≤n−1
Simple Solution
设 fu 表示切断 u 与其子树中所有 关键点 的最小代价。
设 wu,v 表示边 (u,v) 的权值。
枚举 u 的儿子 v,转移分为两类:
- v 是关键点:fu=fu+w(u,v)
- v 不是关键点:fu=fu+min(fv,w(u,v))
朴素做法的时间复杂度为 O(nq),并过不掉这题,所以我们需要进行优化。
可以注意到,我们浪费的很多时间 dp 非关键点,并且关键点的总数是与 n 同阶的,所以考虑能否 浓缩信息,大树变小。
这时引出 虚树 的概念。
简介
直观的感受一下:
选取不同关键点,建出来的虚树如下所示:
(以上图转自 OI Wiki)
任意两个关键点的 LCA 也需要保存重要信息,我们需要将其保留,所以 虚树中不一定只包含关键点。
以及我们可以发现,虚树中祖先后代关系并不会改变。
算法流程
接下来直接搬运一波建树方法:
首先很直观的,可以将所有关键点按 DFS 序排序,遍历一遍,两两间求求 LCA,判判重,连连边,就建完啦!(逃)
朴素算法复杂度较高,考虑单调栈,单调栈的作用是:维护虚树上的一条链。
为了方便,首先将根节点加入栈中,然后按 DFS 序遍历关键点:
- 若当前点 u 与栈顶节点的 LCA 是栈顶节点,那么说明 u 与栈中节点在一条链上,直接将 u 压入栈中
- 若 LCA 不是栈顶节点,
-
首先进行退栈,每弹出一个栈顶节点,在虚树中加入其与其虚树中父亲的连边。
-
直到栈顶节点为 LCA 的父亲或为 LCA,若栈顶节点不为 LCA,则将 LCA 加入栈中
-
最后将 u 加入栈中。
需注意一些细节:
退栈时,一般栈顶节点在虚树中的父亲为第二节点,但是最后弹出的栈顶节点的父亲为 LCA (注意我们最后把 LCA 加入栈中了);
如例题,我们需要多次建虚树,则每次需要清空存图的数组,一般在节点入栈时清空即可(如邻接表,前向行 head 数组等);
如例题,每一条虚树中的边(或点)可能浓缩的原树中几条边的信息,所以我们有时需要先求出浓缩信息,然后在连边赋上。例题中我用了倍增的方法在连边时 logn 求出一条虚树上边的权值(其实就是几条原树边取个 min)。
最后
对于例题,建出虚树后,直接在上面跑朴素 dp 就可以啦!
例题
[SDOI2011]消耗战
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
std;
typedef long long ll;
void read(int &x) {
char ch = getchar(); x = 0;
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - 48, ch = getchar();
}
struct EDGE { int next, to, w; } edge[N << 1];
int head[N + 1], pre[N + 1][L + 1], g[N + 1][L + 1], sta[N + 1], dfn[N + 1], a[N << 1], dep[N + 1], key[N + 1];
ll f[N + 1];
int n, m, len, now = 0;
int cnt_edge = 1;
void Add(int u, int v, int w) { edge[ ++ cnt_edge ] = (EDGE) { head[u], v, w }, head[u] = cnt_edge; }
void Link(int u, int v, int w) { Add(u, v, w), Add(v, u, w); }
void Input() {
read(n);
for (int i = 1, x, y, z; i < n; i ++)
read(x), read(y), read(z), Link(x, y, z);
read(m);
}
int tot_dfn = 0;
void Dfs1(int u, int la) {
pre[u][0] = edge[la].to, g[u][0] = edge[la].w;
dfn[u] = ++ tot_dfn, dep[u] = dep[pre[u][0]] + 1;
Fo(i, u) if (i != la)
Dfs1(edge[i].to, i ^ 1);
}
void Init() {
Dfs1(1, 0);
fo(j, 1, L) fo(i, 1, n)
pre[i][j] = pre[pre[i][j - 1]][j - 1],
g[i][j] = min(g[i][j - 1], g[pre[i][j - 1]][j - 1]);
}
bool Cmp(int x, int y) { return dfn[x] < dfn[y]; }
int Get_lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (int i = 0, x = dep[u] - dep[v]; x; ++ i, x >>= 1)
if (x & 1) u = pre[u][i];
if (u == v) return u;
fd(i, L, 0) if (pre[u][i] != pre[v][i])
u = pre[u][i], v = pre[v][i];
return pre[u][0];
}
int Get_dis(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int Dis = g[u][0];
for (int i = 0, x = dep[u] - dep[v]; x; ++ i, x >>= 1)
if (x & 1) Dis = min(Dis, g[u][i]), u = pre[u][i];
return Dis;
}
ll min(ll a, ll b) { return a < b ? a : b; }
void Build() {
sort(a + 1, a + 1 + len, Cmp);
sta[1] = 1, head[1] = 0; cnt_edge = 0;
int top = 1;
for (int i = 1, lca = 0; i <= len; i ++) {
// if (a[i] == 1) continue; 以防重复加入
lca = Get_lca(sta[top], a[i]);
if (lca != sta[top]) {
while (dfn[lca] < dfn[sta[top - 1]])
Add(sta[top - 1], sta[top], Get_dis(sta[top], sta[top - 1])), -- top;
if (dfn[lca] > dfn[sta[top - 1]])
head[lca] = 0, Add(lca, sta[top], Get_dis(sta[top], lca)), sta[top] = lca;
else
Add(lca, sta[top], Get_dis(sta[top], lca)), -- top;
}
sta[ ++ top ] = a[i], head[a[i]] = 0;
}
fo(i, 1, top - 1)
Add(sta[i], sta[i + 1], Get_dis(sta[i + 1], sta[i]));
}
void Dfs2(int u) {
f[u] = 0;
Fo(i, u) {
Dfs2(edge[i].to);
f[u] += key[edge[i].to] == now ? edge[i].w : min(f[edge[i].to], edge[i].w);
}
}
void Solve() {
Build();
Dfs2(1);
printf("%lld\n", f[1]);
}
int main() {
Input();
Init();
fo(Case, 1, m) {
read(len); ++ now;
fo(i, 1, len)
read(a[i]), key[a[i]] = now;
Solve();
}
return 0;
}
using namespace
CF613D.KingdomanditsCities
Solution
待补充......
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· 手把手教你更优雅的享受 DeepSeek
· AI工具推荐:领先的开源 AI 代码助手——Continue
· 探秘Transformer系列之(2)---总体架构
· V-Control:一个基于 .NET MAUI 的开箱即用的UI组件库
· 乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现