斯坦纳树学习笔记

abcdeffa

2020-03-14 20:40:04

Personal

## Brief introduction > 斯坦纳树问题是组合优化问题,与最小生成树相似,是最短网络的一种。最小生成树是在给定的点集和边中寻求最短网络使所有点连通,而最小斯坦纳树允许在选定点外增加额外的点,使生成的最短网络开销最小。$^{[1]}$ 斯坦纳树问题即给出你一个有 $n$ 个点 $m$ 条边的有权无向图,然后选定 $k$ 个点,让你求出使这 $k$ 个点联通所需的最小代价,其实最小生成树是最小斯坦纳树的一种特殊情况。$^{[2]}$ 在一秒内可以跑过的范围大概是 $1 \leq k \leq 10$,$1 \leq n, m \leq 10^3$,因为我们要状压,所以 $k$ 一般来说不会很大。 ## Algorithm flow 首先我们首先可以发现一个结论:答案的子图 **一定** 是树,因为如果答案存在环,则删去环上任意一条边,则可以使代价变小。$^{[3]}$ 解决这类问题,我们考虑设 $f_{i, S}$ 表示以 $i$ 为根,包含选定点的状态为 $S$ 的情况下所需要的代价的最小值,其中当 $S$ 在二进制下的第 $i$ 位为 1 时表示包含了第 $i$ 个关键点,为 0 则表示不包含。 那么我们可以得出我们的第一个转移式: $$f_{i, S} = \min\{ f_{i, S'} + f_{i, S \oplus S'} \}$$ 这条转移式就是将 $S$ 拆分成 $S'$ 和 $(S \oplus S')$ 两个状态,这里的 $(S \oplus S')$ 也可以写成 $(S - S \land S')$,一般来说习惯用 $(S \oplus S')$,这里的 $S'$ 是 $S$ 状态的一个子集。 对于枚举 $S'$,我们采用下面的方式: ```cpp for(int SS = (S - 1) & S;SS;SS = (SS - 1) & S) ``` 这样就可以优化复杂度了。 我们已经解决了自己更新自己的问题了,那么我们怎么解决新加一条边的问题呢? 我们又可以想到下面的这条转移式: $$f_{i, S} = \min(f_{j, S} + w)$$ 其中 $i$ 和 $j$ 之间有一条边权为 $w$ 的边,其中若 $i$ 或 $j$ 是关键点,则 $S$ 就要包含它们。 这个怎么转移呢? 我们发现当 $f_{j, S}$ 的值比当前的 $f_{i, S}$ 的值优的话,那么就要满足 $f_{j, S} + w < f_{i, S}$,是不是像极了你做 SPFA 时的那个转移式? 于是我们考虑使用 SPFA 来做转移。 初始时 $f_{i, S}$ 均为正无穷,$f_{a_j, 2^{j - 1}} = 0$,其中 $a_j$ 是第 $j$ 个选定点的编号。 第二条转移式具体怎用 SPFA 来转移? 我们先从小到大枚举 $S$,在对所有的 $f_{i, S}$ 执行完第一条转移式 $f_{i, S} = \min\{ f_{i, S'} + f_{i, S \oplus S'} \}$ 后,把所有满足 $f_{i, S}$ 不为正无穷的 $i$ 丢进队列里,然后跑 SPFA 更新就好啦。 SPFA 部分的代码如下: ```cpp void bfs (int S) { while(tou != wei) { int x = dl[tou]; for(int i = h[x];i;i = b[i].g) { int y = b[i].y; if(f[x][S] + b[i].c < f[y][S]) { f[y][S] = f[x][S] + b[i].c; if(!vis[y]) { vis[y] = 1; dl[wei] = y; wei++; if(wei >= maxN) { wei = 1; } } } } vis[x] = 0; tou++; if(tou >= maxN) { tou = 1; } } } ``` 结合这两条转移式,我们就可以得出下面的代码了。 ```cpp for(int S = 0;S <= ma; S++) { tou = wei = 1; for(int SS = (S - 1) & S;SS;SS = (SS - 1) & S) { for(int i = 1;i <= n; i++) { f[i][S] = min(f[i][S], f[i][SS] + f[i][S ^ SS]); } } for(int i = 1;i <= n; i++) { if(f[i][S] < INF) { vis[i] = 1; dl[wei++] = i; } } bfs(S); } ``` 其中函数 $bfs()$ 的内容已在上面给出。 最后的答案就是 $\min_{i = 1}^p f_{i, 2^p - 1}$,其中 $p$ 是给定点的个数。 ## Example 1 题目来源:Luogu P6192 【模板】最小斯坦纳树 给定一个包含 $n$ 个结点和 $m$ 条带权边的无向连通图 $G = (V,E)$。 再给定包含 $k$ 个结点的点集 $S$,选出 $G$ 的子图 $G'=(V', E')$,使得 $S\subseteq V'$,$G'$ 为连通图,且$E'$ 中所有边的权值和最小。你只需要求出 $E'$ 中所有边的权值和即可。 $$1 \leq n \leq 100, 1 \leq m \leq 500, 1 \leq k \leq 10$$ 保证答案和输入的所有数均小于 $2^{31}$。 ## Solution 1 这道题目其实就是 **斯坦纳树** 的模板,直接用 **斯坦纳树** 来做就好了。 ## Code 1 ```cpp #include <cstdio> #include <cstring> #define maxP 10 #define maxN 3010 const int INF = 707406378; struct edge{ int x, y, c, g; } b[maxN << 1]; int len = 0, tou = 1, wei = 1; int f[maxN][1 << maxP]; int color[maxP], num[maxP]; int vis[maxN], dl[maxN], h[maxN]; int min (int x, int y) { return x < y ? x : y; } void ins (int x, int y, int c) { len++; b[len].x = x; b[len].y = y; b[len].c = c; b[len].g = h[x]; h[x] = len; } void bfs (int S) { while(tou != wei) { int x = dl[tou]; for(int i = h[x];i;i = b[i].g) { int y = b[i].y; if(f[x][S] + b[i].c < f[y][S]) { f[y][S] = f[x][S] + b[i].c; if(!vis[y]) { vis[y] = 1; dl[wei] = y; wei++; if(wei >= maxN) { wei = 1; } } } } vis[x] = 0; tou++; if(tou >= maxN) { tou = 1; } } } int main () { int n = 0, m = 0, p = 0; scanf("%d %d %d", &n, &m, &p); for(int i = 1;i <= m; i++) { int x = 0, y = 0, c = 0; scanf("%d %d %d", &x, &y, &c); ins(x, y, c); ins(y, x, c); } memset(f, 127 / 3, sizeof(f)); for(int i = 1;i <= p; i++) { scanf("%d", &num[i]); f[num[i]][1 << (i - 1)] = 0; } const int ma = (1 << p) - 1; for(int S = 0;S <= ma; S++) { tou = wei = 1; for(int SS = (S - 1) & S;SS;SS = (SS - 1) & S) { for(int i = 1;i <= n; i++) { f[i][S] = min(f[i][S], f[i][SS] + f[i][S ^ SS]); } } for(int i = 1;i <= n; i++) { if(f[i][S] < INF) { vis[i] = 1; dl[wei++] = i; } } bfs(S); } int ans = INF; for(int i = 1;i <= n; i++) { ans = min(ans, f[i][ma]); } printf("%d", ans); return 0; } ``` ## Example 2 题目来源:Luogu P3264 [JLOI2015]管道连接 小铭铭最近进入了某情报部门,该部门正在被如何建立安全的通道连接困扰。该部门有 $n$ 个情报站,用 $1$ 到 $n$ 的整数编号。给出 $m$ 对情报站 $u_i;v_i$ 和费用 $w_i$,表示情报站 $u_i$ 和 $v_i$ 之间可以花费 $w_i$ 单位资源建立通道。 如果一个情报站经过若干个建立好的通道可以到达另外一个情报站,那么这两个情报站就建立了通道连接。形式化地,若 $u_i$ 和 $v_i$ 建立了通道,那么它们建立了通道连接;若 $u_i$ 和 $v_i$ 均与 $t_i$ 建立了通道连接,那么 $u_i$ 和 $v_i$ 也建立了通道连接。 现在在所有的情报站中,有 $p$ 个重要情报站,其中每个情报站有一个特定的频道。小铭铭面临的问题是,需要花费最少的资源,使得任意相同频道的情报站之间都建立通道连接。 现在请你求出最小的花费是多少。 ## Solution 2 如果我们设答案的状态 $S$ 是由一颗或多棵斯坦纳树的状态 $S_1$、$S_2$、...、$S_k$,其中 $1 \leq k \leq p$,那么 $S_i(1 \leq i \leq k)$ 必定满足如果它在二进制下的某一位是 1,那么这个状态肯定包含了所有和这个选定点的频道一样的点,那么我们对所有这样的状态求出它的斯坦纳树,然后状压 DP 一下就可以求出答案了。 状压 DP 的话就是设 $g_S$ 表示当前让状态为 $S$ 的选定点联通且 **在满足题目要求的情况下** 所需的最小代价,然后转移即可。 我卡了常,又多交了几遍才在洛谷上过了这题...... ## Code 2 ```cpp #include <cstdio> #include <cstring> #define maxP 10 #define maxN 3010 #define min(x, y) ((x) < (y) ? (x) : (y)) const int INF = 707406378; struct edge{ int x, y, c, g; } b[maxN << 1]; int len = 0, tou = 1, wei = 1; int f[maxN][1 << maxP]; int vis[maxN], dl[maxN], h[maxN]; int abl[1 << maxP], g[1 << maxP]; int color[maxP], count[maxP], num[maxP], dy[maxP]; int read () { char c; int x = 0; c = getchar(); while(c < '0' || c > '9') { c = getchar(); } while(c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = getchar(); } return x; } void ins (int x, int y, int c) { len++; b[len].x = x; b[len].y = y; b[len].c = c; b[len].g = h[x]; h[x] = len; } void bfs (int S) { while(tou != wei) { int x = dl[tou]; for(register int i = h[x];i;i = b[i].g) { int y = b[i].y; if(f[x][S] + b[i].c < f[y][S]) { f[y][S] = f[x][S] + b[i].c; if(!vis[y]) { vis[y] = 1; dl[wei] = y; wei++; if(wei >= maxN) { wei = 1; } } } } vis[x] = 0; tou++; if(tou >= maxN) { tou = 1; } } } int main () { int n = 0, m = 0, p = 0; n = read(); m = read(); p = read(); for(register int i = 1;i <= m; i++) { int x = 0, y = 0, c = 0; x = read(); y = read(); c = read(); ins(x, y, c); ins(y, x, c); } memset(f, 127 / 3, sizeof(f)); memset(g, 127 / 3, sizeof(g)); int cnt = 0; for(register int i = 1;i <= p; i++) { color[i] = read(); num[i] = read(); if(!dy[color[i]]) { dy[color[i]] = ++cnt; } f[num[i]][1 << (i - 1)] = 0; count[dy[color[i]]] += (1 << (i - 1)); } const int ma = (1 << p) - 1; for(register int S = 0;S <= ma; S++) { bool flag = true; for(register int i = 1;i <= cnt; i++) { int now = (S & count[i]); if(now && now != count[i]) { flag = false; break; } } abl[S] = flag; } for(register int S = 0;S <= ma; S++) { tou = wei = 1; for(register int SS = (S - 1) & S;SS;SS = (SS - 1) & S) { for(register int i = 1;i <= n; i++) { f[i][S] = min(f[i][S], f[i][SS] + f[i][S ^ SS]); } } for(register int i = 1;i <= n; i++) { if(f[i][S] < INF) { vis[i] = 1; dl[wei++] = i; } } bfs(S); if(abl[S]) { for(register int i = 1;i <= n; i++) { g[S] = min(g[S], f[i][S]); } } } for(register int S = 0;S <= ma; S++) { for(register int SS = (S - 1) & S;SS;SS = (SS - 1) & S) { g[S] = min(g[S], g[SS] + g[S ^ SS]); } } printf("%d", g[ma]); return 0; } ``` ## Reference [1] 百度百科-斯坦纳树(https://baike.baidu.com/item/%E6%96%AF%E5%9D%A6%E7%BA%B3%E6%A0%91/12796694?fr=aladdin) [2] Coco_T_ 的 CSDN 博客中的文章“斯坦纳树 Steiner Tree”(https://blog.csdn.net/wu_tongtong/article/details/78992913?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task) [3] ix35_ 的洛谷博客中的文章“题解 P6192 【【模板】最小斯坦纳树】”(https://www.luogu.com.cn/blog/ix-35/solution-p6192)