[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 }
View Code

复杂度:$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();}
View Code

 

posted @ 2020-04-23 09:24  Chiaro  阅读(218)  评论(0)    收藏  举报