笔记:最小斯坦纳树

最小斯坦纳树

定义

摘自百度百科的定义:

斯坦纳树问题是组合优化问题,与 最小生成树相似 ,是最短网络的一种。最小生成树是在给定的点集和边中寻求最短网络使所有点连通。而最小斯坦纳树允许在给定点外增加额外的点,使生成的最短网络开销最小。

实现

例题:P6192 【模板】最小斯坦纳树 - 洛谷

题目描述

给定一个包含 n 个结点和 m 条带权边的无向连通图 G=(V,E)

再给定包含 k 个结点的点集 S,选出 G 的子图 G=(V,E),使得:

  1. SV

  2. G 为连通图;

  3. E 中所有边的权值和最小。

你只需要求出 E 中所有边的权值和。

数据范围

对于 100% 的数据,1n100,  1m500,  1k10,  1u,vn,  1w106

保证给出的无向图连通,但 可能 存在重边和自环。

分析

要解决最小斯坦纳树,我们需要使用最短路算法 + 状压 DP。

首先,很显然的一点是:G 是一棵树。

证明:若 G 中包含环,那么将环中边权最大的边删去后,G 仍连通且边权和变得更小,与前面假设的边权和最小矛盾,故 G 不含环。

然后,我们发现 k 很小,可以往状压方面去想。设 f[i][S] 表示以 i 为根的树,选择的点集为 S(题目里面说的 k 个点)时的最小边权和,则可得到方程:

f[i][S]=f[i][ST]+f[i][T](TS)f[i][S]=f[j][S]+w(i,j)

  • 第一个式子可将 S 的两个互为补集的子集的状态合并。

  • 第二个式子可将 i 的状态转移到另一个相邻节点。注意这个式子,像不像最短路?都是三角不等式,我们只需在状压时对每个状态跑一次最短路即可。

考虑 DP 的顺序:不难想到按 S 从小到大枚举即可。

考虑答案的产生:iSf[i][S] 均为答案。

  • 证明:因为 f[i][S] 表示以 i 为根结点且包含 S 的树的最小边权和,而最小斯坦纳树显然包括 S,所以 S 中的任意一点为根都可以作为答案。

代码

#include <bits/stdc++.h>
using namespace std;

const int maxn = 105, maxm = 505, maxk = 10;
int n, m, k;
int head[maxn];
struct Edge {
    int to, weight, next;
} edge[maxm << 1];
struct Node {
    int id, dis;
    bool operator < (const Node &rhs) const {
        return dis > rhs.dis;
    }
};
int f[maxn][1 << maxk];

inline void insertEdge(int u, int v, int w) {
    static int edgecnt;
    edge[++edgecnt].to = v;
    edge[edgecnt].weight = w;
    edge[edgecnt].next = head[u];
    head[u] = edgecnt;
}

inline void dijkstra(int S) {
    priority_queue<Node> que;
    for (int i = 1; i <= n; i++) {
        if (f[i][S] == 0x3f3f3f3f) continue;
        que.push(Node{ i, f[i][S] }); // 只有已经松弛过的点才加入优先队列
    }
    while (!que.empty()) {
        int u = que.top().id;
        que.pop();
        for (int i = head[u]; i; i = edge[i].next) {
            int v = edge[i].to, w = edge[i].weight;
            if (f[v][S] > f[u][S] + w) {
                f[v][S] = f[u][S] + w; // 和最短路是一样的
                que.push(Node{ v, f[v][S] });
            }
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        insertEdge(u, v, w);
        insertEdge(v, u, w);
    }
    memset(f, 0x3f, sizeof(f)); // 记得初始化为正无穷
    int vertex;
    for (int i = 1; i <= k; i++) {
        scanf("%d", &vertex);
        f[vertex][1 << (i - 1)] = 0; // 当集合中只包含一个点的时候,边权和显然为 0
    }
    for (int i = 1; i <= n; i++) {
        f[i][0] = 0; // 一个点都没有显然也是 0
    }
    for (int S = 1; S < (1 << k); S++) {
        for (int i = 1; i <= n; i++) { // 以每个点为根都跑一遍
            for (int j = S & (S - 1); j; j = S & (j - 1)) { // 枚举当前 S 的子集(不含空集和 S 本身,原因自己想)
                f[i][S] = min(f[i][S], f[i][S ^ j] + f[i][j]);
            }
        }
        dijkstra(S); // 最短路
    }
    printf("%d", f[vertex][(1 << k) - 1]); // S 中的每个点都可作为答案,所以就干脆直接用最后读入的 vertex
    return 0;
}

最小斯坦纳树森林

[[P3264 JLOI2015 管道连接]]
[[Peach Blossom Spring]]
这两道题 基本相当于双倍经验 都有一个共同特点,就是钦定的关键点被分为了几个集合,可能形成斯坦纳树森林。(详情看题)

对于这种题,除了 f[i][S] 表示以 i 为根,关键点点集为 S 时的最小边权和,还有一个 g[S],表示所选关键点点集为 S 的时候所构成的斯坦纳树森林的最小边权和,并且这个 g[S] 在不同的题目中会有不同的前置条件,只有所有前置条件满足的时候,才可以通过 f 转移。

g[S]=min(f[i][S])

然后,再通过状压枚举子集转移。

g[S]=minTS(g[S],g[ST]+g[T])

(说的不一定对,若有误,请指正。)

posted @   jxyanglinus  阅读(92)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示