Loading

最小斯坦纳树

概念

P6192 【模板】最小斯坦纳树

对于无向图 \(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;
}

变式

最小斯坦纳树森林

P3264 [JLOI2015]管道连接

每个关键点有其类型,要求同一类型的关键点连通即可。

可以考虑分别考虑每种类型,处理出使该类型的关键点全部连通的最小代价。令 \(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}\)

点权最小斯坦纳树

P4294 [WC2008]游览计划

要求 \(\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)\)

构造最小斯坦纳树

考虑直接记录更新当前状态的状态,然后递归处理。

posted @ 2022-07-06 21:30  kymru  阅读(121)  评论(0编辑  收藏  举报