[SDOI 2011]消耗战[虚树][虚树学习笔记]
虚树(Virtual Tree)指在原树的基础上,选取部分点为 关键点,然后将这些点按照原树上的 父子关系 建树
不难发现,要想要保证 父子关系 不变化,虚树上除了 关键点 以外,还需要有一些关键点的 LCA,我们暂且称之为 LCA点
如图,红色点为 关键点,蓝色点是所必须的 LCA点,右图是左图的虚树

这样虚树上的一条边,在原树上代表一条链
可以发现如果关键点有 k 个,则整棵虚树最多只有 $2k-1$ 个点,这样构建出来的树可以用于剪掉一些不必要的情况
那么怎么建树?
对于一个点 u,如果它的几个儿子的子树中,有大于等于两个儿子的子树中有 key 点,那么这个点,就一定是 LCA点
这个感性理解一下大概挺显然的?
所以只需要一次 DFS,按照上述规则找到所有虚树上的点,再一遍 DFS 来连边即可
以 [SDOI 2019]世界地图 代码为例:
1 bool buildVT(int now, int f) { // 处理出所有虚树点 2 int tot = isKey[now]; 3 // tot 是 自己是否是key点 与 有key点的子树 的数量和 4 for(edge* e = g[now]; e; e = e->next) { 5 int to = e->to; 6 if(to == f) continue; 7 tot += buildVT(to, now); 8 } 9 if(tot >= 2) isKey[now] = 1; 10 return !!tot; 11 } 12 13 // 将上面处理出来的 key 连边形成虚树 14 // last 是上面的那个 key 点, max 是路径上的权值最大值 15 void processVal(int now, int f, int last, int max, std::vector <MSTedge>& V) { 16 if(isKey[now]) { 17 if(last) { 18 V.push_back(MSTedge(now, last, max)); 19 ansdis -= max; 20 } 21 last = now, max = 0; 22 } 23 for(edge* e = g[now]; e; e = e->next) { 24 int to = e->to; 25 if(to == f) continue; 26 processVal(to, now, last, std::max(max, e->val), V); 27 } 28 return; 29 }
复杂度:$O(n)$
然而这个复杂度很多时候不是很能接受
其实可以用栈来维护建树
首先要知道一个结论,从左到右的将 key 点排序后,虚树上的 LCA 点一定是某一对相邻的 key 的 LCA
我们用一个栈来维护当前这个点到根节点这条链上的所有点(或者被称为最右链,因为我们是从左到右处理)
一开始将第一个点直接压入栈中,然后从左向右的枚举 key 点,分情况讨论
以下图中,红色点为当前的 key 点,蓝色点是 栈顶点,绿色点是栈顶下面的那个点(栈中第二个点),黄色为栈中其他点,箭头指向 栈顶点 和 当前点 的 LCA
情况①:如图

这种情况直接将当前点加入栈
情况②:如图

这种情况把 lca 和 栈顶点 连个边然后扔掉 栈顶点 就好,然后栈中加入 lca 和 当前点
情况③:如图

和 ② 相同,不过 lca 点已经在栈中了,没必要入栈了
情况④:如图

将左侧那条链全部连上边,然后全部出栈,再入当前点就好
复杂度:$O(k)$ (但还需要加上求 LCA 的复杂度)
[SDOI 2011]消耗战
简述题意:给定一棵树,边有边权,T 次询问,每次钦点 k 个点,要求断开若干条边(断开这条边需要花费这条边边权的费用),使得任意一个钦定点到根(1号点)不连通,求最小费用
数据范围:$n\leq 2.5\times 10^5, T\leq 5\times 10^5, \sum k\leq 5\times 10^5$
可以有树形 dp:
dp[i] 表示以 i 为根的子树中,i 与子树中钦定点不连通的最小花费
$dp[i]=dp[i]+\min\{dp[son_i], val(i\rightarrow son_i)\}$ ($son_i$ 不是钦定点)
$dp[i]=dp[i]+val(i\rightarrow son_i)$ ($son_i$ 是钦定点)
期望得分:40pts
这么做显然复杂度爆炸,考虑优化
发现这个 dp 中用了很多不必要的点,按照那些钦定点建一棵虚树,在虚树上跑 dp,能快非常多
dp 的时候式子也会有变化,$m(i)$ 表示从当前点到根节点的最小边权
$dp[i]=m(i)$ (叶子节点)
$dp[i] =\min\{\sum_{i}dp[son_i], m(i)\}$ (非叶子节点)
样例二即可 hack 该做法,原因是有若干个 key 点在虚树上形成了一个链
可以发现一个显然的结论,如果若干 key 点在虚树上形成链,假设这条链的顶端是 u 点,那么对于这道题来说,最优策略一定是用 $m(u)$ 的代价删除对应边
所以第一个式子应该改成
$dp[i]=m(i)$ (叶子节点或 key 点)
但是不知道是不是我自己的问题(),我获得了 MLE+TLE+WA 的好成绩
有另一种方法,就是建树的时候遇到 key 点就不向下建了,即 情况① 的时候直接跳出(详见代码)
不过似乎如果求 LCA 用倍增法的话好像有一些别的奇妙做法,但是我求 LCA 已经只会树剖了(
期望得分:100pts
复杂度:$O(mK\log K)$?
1 #include <ctime> 2 #include <cmath> 3 #include <cstdio> 4 #include <cstring> 5 #include <cstdlib> 6 #include <iostream> 7 #include <algorithm> 8 #include <vector> 9 #include <queue> 10 #define inf 500010 11 #define INF 0x7fffffffffffffff 12 #define ll long long 13 14 namespace chiaro{ 15 16 using std::cerr; 17 using std::endl; 18 #define cerro(x) std::cerr << #x <<' '<< x << std::endl; 19 20 template <class I> 21 inline void read(I &num){ 22 num = 0; char c = getchar(), up = c; 23 while(c < '0' || c > '9') up = c, c = getchar(); 24 while(c >= '0' && c <= '9') num = (num << 1) + (num << 3) + (c ^ '0'), c = getchar(); 25 up == '-' ? num = -num : 0; return; 26 } 27 template <class I> 28 inline void read(I &a, I &b) {read(a); read(b);} 29 template <class I> 30 inline void read(I &a, I &b, I &c) {read(a); read(b); read(c);} 31 32 struct edge { 33 int to; 34 ll val; 35 edge* next; 36 }; 37 38 struct Stack { 39 ll a[inf]; 40 int ptr; 41 42 inline void push(int x) {a[++ptr] = x;} 43 inline void pop() {--ptr;} 44 inline int top() {return a[ptr];} 45 inline int topnext() {return a[ptr - 1];} 46 inline int size() {return ptr;} 47 }; 48 49 int n; 50 int key[inf]; 51 int deg[inf]; 52 bool book[inf]; 53 edge* g[inf << 1]; 54 edge* gv[inf << 1]; 55 Stack S; 56 57 bool edgeClearFlag; 58 inline void connect(int from, int to, int val, edge** g) { 59 static edge pool[inf << 2]; 60 static edge* p = pool; 61 if(edgeClearFlag) p = pool, edgeClearFlag = 0; 62 p->to = to; 63 p->val = val; 64 p->next = g[from]; 65 g[from] = p; p++; 66 deg[from] += (val == 0); 67 return; 68 } 69 70 namespace lcaWorker { 71 int index; 72 int fa[inf]; 73 int dfn[inf]; 74 int dep[inf]; 75 int top[inf]; 76 int son[inf]; 77 ll min[inf]; 78 int size[inf]; 79 80 void dfs1(int now, int f, int depth) { 81 fa[now] = f; 82 dep[now] = depth; 83 size[now] = 1; 84 int heavy = 0; 85 for(edge* e = g[now]; e; e = e->next) { 86 int y = e->to; 87 if(y == f) continue; 88 min[y] = std::min (e->val, min[now]); 89 dfs1(y, now, depth + 1); 90 size[now] += size[y]; 91 if(size[y] > heavy) { 92 heavy = size[y]; 93 son[now] = y; 94 } 95 } 96 return; 97 } 98 99 inline void dfs2(int now, int point) { 100 dfn[now] = ++index; 101 top[now] = point; 102 if(son[now] == 0) return; 103 dfs2(son[now], point); 104 for(edge* e = g[now]; e; e = e->next) { 105 int y = e->to; 106 if(y == fa[now] || y == son[now]) continue; 107 dfs2(y, y); 108 } 109 return; 110 } 111 112 inline int getLCA(int a, int b) { 113 while(top[a] != top[b]) { 114 if(dep[top[a]] < dep[top[b]]) b = fa[top[b]]; 115 else a = fa[top[a]]; 116 } 117 return (dep[a] > dep[b] ? b : a); 118 } 119 } 120 121 namespace VTree { 122 using lcaWorker::dfn; 123 using lcaWorker::dep; 124 125 inline bool cmp(int a, int b) {return dfn[a] < dfn[b];} 126 127 inline void build(int k) { 128 std::sort(key + 1, key + 1 + k, cmp); 129 S.push(1); S.push(key[1]); 130 for(int i = 2; i <= k; i++) { 131 int now = key[i]; 132 int lca = lcaWorker::getLCA(now, S.top()); 133 while(1) { 134 if(dep[lca] >= dep[S.topnext()]) { 135 // break 建完整 VT, goto 建部分 VT 136 if(lca == S.top()) goto Line1; 137 else { 138 connect(lca, S.top(), 0, gv); 139 S.pop(); 140 if(lca != S.top()) S.push(lca); 141 } 142 break; 143 } 144 connect(S.topnext(), S.top(), 0, gv); 145 S.pop(); 146 } 147 S.push(now); 148 Line1:; 149 } 150 while(S.size() > 1) { 151 connect(S.topnext(), S.top(), 0, gv); 152 S.pop(); 153 } 154 S.pop(); 155 return; 156 } 157 } 158 159 namespace dpWorker { 160 using lcaWorker::min; 161 162 ll dp(int now) { 163 if(deg[now] == 0) return min[now]; 164 ll cnt = 0; 165 for(edge* e = gv[now]; e; e = e->next) { 166 int to = e->to; 167 cnt += dp(to); 168 } 169 gv[now] = NULL; 170 deg[now] = 0; 171 return std::min (min[now], cnt); 172 } 173 } 174 175 inline void setting(){ 176 #ifndef ONLINE_JUDGE 177 freopen("_test.in", "r", stdin); 178 freopen("_test.me.out", "w", stdout); 179 #endif 180 return; 181 } 182 183 inline int main(){ 184 setting(); 185 read(n); 186 for(int i = 1; i < n; i++) { 187 int u, v, w; 188 read(u, v, w); 189 connect(u, v, w, g); 190 connect(v, u, w, g); 191 } 192 lcaWorker::min[1] = INF; 193 lcaWorker::dfs1(1, 0, 1); 194 lcaWorker::dfs2(1, 1); 195 int T; read(T); 196 while(T--) { 197 int k; read(k); 198 for(int i = 1; i <= k; i++) { 199 read(key[i]); 200 book[key[i]] = 1; 201 } 202 VTree::build(k); 203 printf("%lld\n", dpWorker::dp(1)); 204 edgeClearFlag = 1; 205 } 206 return 0; 207 } 208 209 } 210 211 signed main(){ return chiaro::main();}

浙公网安备 33010602011771号