斯坦纳树 学习笔记
斯坦纳树问题是组合优化问题1 ,其中最常见的是最小斯坦纳树问题。
我们一般使用状压dp来求解最小斯坦纳树的问题,当然也有不用状压dp的题啦(
最小斯坦纳树
题目链接:P6192 【模板】最小斯坦纳树
题意:
给定一个包含 $n$ 个结点和 $m$ 条带权边的无向连通图 $G=(V,E)$。
再给定包含 $k$ 个结点的点集 $S$,选出 $G$ 的子图 $G^{\prime}=(V^{\prime},E^{\prime})$,使得:
$S\subseteq V^{\prime}$;
$G^{\prime}$ 为连通图;
$E^{\prime}$ 中所有边的权值和最小。
你只需要求出 $E^{\prime}$ 中所有边的权值和。
数据范围:
$1\leq n\leq 100,~1\leq m\leq 500,~1\leq k\leq 10,~1\leq u,v\leq n,~1\leq w\leq 10^6$。
保证给出的无向图连通,但 可能 存在重边和自环。
$\mathcal{Part\ 1}$
最小斯坦纳树问题,就是要花费最小的代价,连通给定的 $k$ 个关键点。
称答案为关键图,并称关键点构成的集合为关键集合。显然这个关键图是一棵树(最小斯坦纳树)。
考虑状压dp。设 $f_{i,S}$ 表示包含关键子集 $S$ 中所有点,且钦定 $i$ 为树根时的最小代价。
注意,这里的 $S$ 是关键子集,也就是我们并不关心非关键点选了哪些。
并且容易发现在关键图连出环的时候一定是不优的,因此我们不会算重答案。
但是为什么要钦定树根呢? 因为不钦定根时的复杂度反倒会更高,因为不好快速转移。
$\mathcal{Part\ 2}$
考虑如何不重不漏地转移。(下文中使用 $a \downarrow b+c$ 表示 $a \gets \min\{a,~b+c\}$ )
一棵以 $i$ 为根的树,有两种情况,第一种是 $i$ 的度数为 $1$ ,第二种是 $i$ 的度数大于 $1$ 。
对于 $i$ 的度数为 $1$ 的情况,我们可以枚举它在树上与 $i$ 相邻的点 $j~(j \in S)$ ,则
对于 $i$ 的度数大于 $1$ 的情况,我们可以划分为若干个子树考虑,即( $\subset$ 表示真包含)
$\mathcal{Part\ 3}$
枚举顺序是斯坦纳树的最巧妙之处。
对于第二种转移,我们只要顺序枚举 $S$ ,然后枚举子集即可。
对于第一种转移,注意到只有 $f_{i,S} > f_{j,S} + w(j,i)$ 时才会更新 $f_{i,S}$ ,可以想到三角不等式。
因此对于每个 $S$ ,我们先枚举一遍 $T$ ,然后跑一遍 spfa 。
这里可以用 spfa 是因为它即使被卡,也不会成为复杂度瓶颈,并且一般情况下常数小于 Dijkstra。
注意,在这道题里不会成为复杂度瓶颈,是因为 $\mathcal{O}(nm)$ 较小,不代表所有此类题中均可用 spfa。
同时,在实现的时候,最好把状态交换一下,即记为 $\mathtt{f[S][i]}$ ,因为数组的连续访问会比较快。
时间复杂度 $\mathcal{O}(nm\times 2^k + n\times 3^k)$ ,但是平均复杂度比这个要低一些。
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
void up(int &x,int y) { x < y ? x = y : 0; }
void down(int &x,int y) { x > y ? x = y : 0; }
#define N 105
#define K 10
#define M 515
int n,m,k,pos=1,head[N],f[(1<<K)+5][N];
struct Edge{ int u,v,w,next; } e[M * 2];
void addEdge(int u,int v,int w)
{
e[++pos] = {u,v,w,head[u]};
head[u] = pos;
}
bitset<N> vis; queue<int> q;
void spfa(int S)
{
int *d = f[S]; while(!q.empty()) q.pop();
for(int i=0; i<n; i++) if(d[i] != INF){ q.push(i); vis[i] = 1; }
while(!q.empty())
{
int u = q.front(); q.pop();
vis[u] = 0;
for(int i=head[u]; i; i=e[i].next)
{
int v = e[i].v, w = e[i].w;
if(d[v] > d[u] + e[i].w)
{
d[v] = d[u] + e[i].w;
if(!vis[v]) { q.push(v); vis[v] = 1; }
}
}
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
// freopen("check.in","r",stdin);
// freopen("check.out","w",stdout);
cin >> n >> m >> k;
for(int i=1,u,v,w; i<=m; i++)
{
cin >> u >> v >> w; --u; --v;
addEdge(u,v,w); addEdge(v,u,w);
}
memset(f,0x3f,sizeof(f));
for(int i=0,x; i<k; i++)
{ cin >> x; --x; f[1 << i][x] = 0; }
int mx = (1 << k) - 1;
for(int S=1; S<=mx; S++)
{
for(int T = S&(S-1); T; T = (T - 1) & S)
for(int i=0; i<n; i++) down(f[S][i], f[T][i] + f[S ^ T][i]);
spfa(S);
}
int res = INF;
for(int i=0; i<n; i++) down(res, f[mx][i]);
cout << res << '\n';
return 0;
}
斯坦纳树例题
P4294 [WC2008]游览计划 (网格图中的最小斯坦纳树)
P3264 [JLOI2015]管道连接 (最小斯坦纳森林,
然后正解跑的比暴力慢)- P3638 [APIO2013] 机器人 (
忘记了) - zoj 3613 Wormhole Transport (有空写,可见link )
参考文献:
[1] https://www.luogu.com.cn/blog/ix-35/solution-p6192
[2] https://www.luogu.com.cn/blog/xyf007/solution-p6192
[3] https://oi-wiki.org/graph/steiner-tree/
[4] https://zh.wikipedia.org/wiki/%E7%BB%84%E5%90%88%E4%BC%98%E5%8C%96
1. 在应用数学和理论计算机科学的领域中,组合优化是在一个有限的对象集中找出最优对象的一类课题。在很多组合优化的问题中,穷举搜索/枚举法是不可行的。组合优化的问题的特征是可行解的集是离散或者可以简化到离散的,并且目标是找到最优解。常见的例子有旅行商问题和最小生成树。 ↩