Processing math: 100%

Virtual Tree 学习笔记

Virtual Tree 略解

引子

像大部分虚树介绍一样,以一道 烂大街 的例题 [SDOI2011]消耗战 引入:

Description

给定一棵大小为 n 的树,每条边有边权,m 组询问,每次询问给定 k关键点,要求切断 1 与所有关键点的路径,求最小代价。

Data Constraint

2n2.5×105m1k5×1051kn1

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 同阶的,所以考虑能否 浓缩信息,大树变小

这时引出 虚树 的概念。

简介

直观的感受一下:

vtree-1

选取不同关键点,建出来的虚树如下所示:

vtree-3

vtree-4

vtree-5

vtree-6

(以上图转自 OI Wiki

任意两个关键点的 LCA 也需要保存重要信息,我们需要将其保留,所以 虚树中不一定只包含关键点

以及我们可以发现,虚树中祖先后代关系并不会改变。

算法流程

接下来直接搬运一波建树方法:

首先很直观的,可以将所有关键点按 DFS 序排序,遍历一遍,两两间求求 LCA,判判重,连连边,就建完啦!(逃)

朴素算法复杂度较高,考虑单调栈,单调栈的作用是:维护虚树上的一条链

为了方便,首先将根节点加入栈中,然后按 DFS 序遍历关键点:

  1. 若当前点 u 与栈顶节点的 LCA 是栈顶节点,那么说明 u 与栈中节点在一条链上,直接将 u 压入栈中
  2. 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
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; #define N 1000000 #define L 19 #define Fo(i, u) for(int i = head[u]; i; i = edge[i].next) #define fo(i, x, y) for(int i = x, end_##i = y; i <= end_##i; ++ i) #define fd(i, x, y) for(int i = x; i >= y; i --) 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; }

CF613D.KingdomanditsCities

Solution

待补充......

posted @   buzzhou  阅读(117)  评论(0编辑  收藏  举报
编辑推荐:
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
阅读排行:
· 手把手教你更优雅的享受 DeepSeek
· AI工具推荐:领先的开源 AI 代码助手——Continue
· 探秘Transformer系列之(2)---总体架构
· V-Control:一个基于 .NET MAUI 的开箱即用的UI组件库
· 乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开