最小斯坦纳树
概念
对于无向图 \(G = (V, E)\),记其关键点集合为 \(S\)。试在图中选出一边集 \(E^{\prime}\),使得图 \(G^{\prime} = (V^{\prime}, E^{\prime})\)(其中 \(S \in V^{\prime}\))为连通图且 \(\sum\limits_{(i, j) \in E^{\prime}} w_{i, j}\) 最小。易看出满足条件的图 \(G^{\prime}\) 为一棵树(反之删去环上任意一边边权总和更小),符合以上性质的生成树称为最小斯坦纳树。
思路
当 \(|S|\) 较小时,最小斯坦纳树可以通过状压 dp 求解。
考虑令 \(f[i][s]\) 表示以 \(i\) 为生成树的树根,生成树中的关键点集合为 \(s\) 时的最小边权总和。分类讨论:
当 \(i\) 有多棵子树时,考虑枚举最后添加的一棵子树,有 \(\forall s^{\prime} \in s, f[i][s] = \min(f[i][s - s^{\prime}] + f[i][s^{\prime}])\)。直接枚举子集的复杂度是 \(O(3^n)\)
当 \(i\) 只有一棵子树时,易知 \(\forall (i, j) \in E, f[i][s] = \min(f[j][s] + w_{i, j})\)。联想到最短路,容易看出这个式子实际上可以通过松弛转移(以 \(f[][s]\) 为距离数组)。据说用 SPFA 会比 Dijkstra 快(不知道什么原理
时间复杂度是 \(O(\sum\limits_{i = 0}^{ 2^{|S|} } 3^i + n \log n)\)
代码
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn = 1e2 + 5;
const int maxm = 1e3 + 5;
const int maxk = 15;
const int maxs = (1 << 10);
const int inf = 0x3f3f3f3f;
struct node
{
int to, nxt, w;
} edge[maxm];
int n, m, k, cnt;
int s[maxk];
int head[maxn], f[maxn][maxs];
bool in_queue[maxn];
queue<int> q;
void add_edge(int u, int v, int w)
{
cnt++;
edge[cnt].to = v;
edge[cnt].nxt = head[u];
edge[cnt].w = w;
head[u] = cnt;
}
void spfa(int st)
{
while (!q.empty())
{
int u = q.front();
q.pop();
in_queue[u] = false;
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (f[v][st] > f[u][st] + edge[i].w)
{
f[v][st] = f[u][st] + edge[i].w;
if (!in_queue[v])
{
q.push(v);
in_queue[v] = true;
}
}
}
}
}
int main()
{
int u, v, w;
memset(f, 0x3f, sizeof(f));
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= m; i++)
{
scanf("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
add_edge(v, u, w);
}
for (int i = 1; i <= k; i++)
{
scanf("%d", &s[i]);
f[s[i]][(1 << (i - 1))] = 0;
}
for (int i = 0; i < (1 << k); i++)
{
for (int j = 1; j <= n; j++)
for (int l = i & (i - 1); l; l = (l - 1) & i)
f[j][i] = min(f[j][i], f[j][i ^ l] + f[j][l]);
for (int j = 1; j <= n; j++)
{
if (f[j][i] != inf)
{
q.push(j);
in_queue[j] = true;
}
}
spfa(i);
}
printf("%d\n", f[s[1]][(1 << k) - 1]);
return 0;
}
变式
最小斯坦纳树森林
每个关键点有其类型,要求同一类型的关键点连通即可。
可以考虑分别考虑每种类型,处理出使该类型的关键点全部连通的最小代价。令 \(w_s\) 表示令集合 \(s\) 中的关键点全部连通的最小代价,其中 \(s\) 为显然有 \(\forall 1 \leq i \leq n, w_s = \min(f[i][s])\)
最后考虑合并代价。令 \(g_s\) 表示使集合 \(s\) 中的关键点全部连通的最小代价,且 \(s\) 满足:若 \(s\) 包含某一类型的关键点,则该类型的所有关键点均在 \(s\) 中。有:\(\forall s^{\prime} \in s, g_s = \min(g_{s - s^{\prime}} + w_{s^{\prime}})\)
由于上面考虑了所有情况的最优解,因此不会重复选边。
最终答案为 \(g_{2^k - 1}\)
点权最小斯坦纳树
要求 \(\sum\limits_{u \in V^{\prime}} w_u\) 最小。
状态照旧,考虑转移。
当 \(u\) 有多棵子树时,直接转移会重复累加 \(w_u\),所以方程为
\(\forall s^{\prime} \in s, f[u][s] = \min(f[u][s - s^{\prime}] + f[u][s^{\prime}] - w_u)\)
当 \(u\) 有一棵子树时,有
\(\forall (u, v) \in E, f[u][s] = \min(f[v][s] + w_u)\)
构造最小斯坦纳树
考虑直接记录更新当前状态的状态,然后递归处理。