最小斯坦纳树

你离开我真会死。 提交于 2020-03-27 12:16:41

最小斯坦纳树

问题描述:

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

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

  1. \(S\subseteq V'\)

  2. \(G′\) 为连通图;

  3. \(E′\) 中所有边的权值和最小。

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

分析:

\(dp[i][s]\) 表示 以 i 为根的一棵树,包含集合 S 中所有点的最小代价 \(i\) 号点不一定在 \(s\)

  1. \(w[i][j] + dp[j][s] -> dp[i][s]\)

  2. \(dp[i][T] + dp[i][S-T] -> dp[i][S]\quad(T \subseteq S)\)

第二类转移可以用枚举子集来转移,第一类转移是一个三角不等式,可以用\(spfa\)或者\(dijkstra\)

第一类转移复杂度为\(O(m\log m \times 2^k)\)

第二类转移复杂度为\(O(n\times 3^n)\) ,子集枚举的时间复杂度可以用二项式定理求出

代码:

const int N = 100 + 5;
const int M = 1010;
int n, m, k, x, y, z, tot;
int head[N], ver[M], nxt[M], edge[M], dp[N][4200];
int p[N], vis[N];
priority_queue<pair<int,int>> q;
void add(int x, int y, int z){
    ver[++tot] = y; edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}
void dijkstra(int s){
    memset(vis, 0, sizeof vis);
    while(q.size()){
        auto t = q.top();q.pop();
        int x = t.second;
        if(vis[x]) continue;
        vis[x] = 1;
        for(int i=head[x];i;i=nxt[i]){
            int y = ver[i];
            if(dp[y][s] > dp[x][s] + edge[i]){
                dp[y][s] = dp[x][s] + edge[i];
                q.push(make_pair(-dp[y][s], y));
            }
        }
    }
}
int main(){
    scanf("%d%d%d", &n, &m, &k);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d", &x, &y, &z);
        add(x, y, z);add(y, x, z);
    }
    memset(dp, 0x3f, sizeof dp);
    for(int i=1;i<=k;i++){
        scanf("%d", &p[i]);
        dp[p[i]][1<<(i-1)] = 0;
    }
    for(int s = 1; s < (1 << k); s++){
        for(int i=1; i <= n; i++){
            for(int subs = (s-1)&s; subs; subs = s & (subs-1)){
                dp[i][s] = min(dp[i][s], dp[i][subs]+dp[i][s^subs]);
            }
            if(dp[i][s]!=inf) q.push(make_pair(-dp[i][s], i));
        }
        dijkstra(s);
    }
    printf("%d\n", dp[p[1]][(1<<k)-1]);

    return 0;
}

例题 2019南昌邀请赛A

给出一张图,求让 \(4\) 对点相互可以到达的最小边权值。仅要求一对之间,一对与另外一对可到达也可不到达。

题目链接

先对于8个点求出最小斯坦纳树,然后求出\(res[s] = \min_{i\in [1,n]} dp[i][s]\) ,表示点集为 \(s\) 的最小斯坦纳树路径和。有些 \(s\) 是不合法的,因为它不能完整的包含4对点中的某一对(如果包含某个点,则必须包含该点的配对点,否则就不包含)。去除这些不合法的集合 \(s\), 在合法的集合之间进行状压\(dp\)转移即可。

const int N = 30 + 5;
const int M = 2000;
int n, m, k, tot, cnt;
int head[N], ver[M], nxt[M], edge[M], dp[N][(1<<8)], res[1<<8];
int p[N], vis[N];
map<string,int> mp;
priority_queue<pair<int,int>> q;
void add(int x, int y, int z){
    ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}
int getId(string str){
    if(mp.count(str))return mp[str];
    return mp[str] = ++cnt;
}
void dijkstra(int s){
    memset(vis, 0, sizeof vis);
    while(q.size()){
        auto t = q.top();q.pop();
        int x = t.second;
        if(vis[x]) continue;
        vis[x] = 1;
        for(int i=head[x];i;i=nxt[i]){
            int y = ver[i];
            if(dp[y][s] > dp[x][s] + edge[i]){
                dp[y][s] = dp[x][s] + edge[i];
                q.push(make_pair(-dp[y][s], y));
            }
        }
    }
}
bool check(int s){
    for(int i=0;i<8;i+=2){
        if((s >> i & 1) != (s >> (i+1) & 1)) return false;
    }
    return true;
}
int main(){
    scanf("%d%d", &n, &m);
    for(int i=1;i<=n;i++){
        string str;cin >> str;
        getId(str);
    }
    for(int i=1;i<=m;i++){
        int x, y, z;
        string s, t;
        cin >> s >> t >> z;
        x = getId(s);
        y = getId(t);
        add(x, y, z);add(y, x, z);
    }
    for(int i=1;i<=4;i++){
        string s, t;
        cin >> s >> t;
        p[i*2-1] = getId(s);
        p[i*2] = getId(t);
    }
    k = 8;
    memset(dp, 0x3f, sizeof dp);
    for(int i=1;i<=k;i++){
        dp[p[i]][1<<(i-1)] = 0;
    }
    for(int S = 1; S < (1 << k); S++){
        for(int i=1; i <= n; i++){
            for(int subs = (S-1)&S; subs; subs = S & (subs-1)){
                dp[i][S] = min(dp[i][S], dp[i][subs]+dp[i][S^subs]);
            }
            if(dp[i][S]!=inf) q.push(make_pair(-dp[i][S], i));
        }
        dijkstra(S);
    }
    //以上为斯坦纳树模板
    memset(res, 0x3f, sizeof res);
    for(int s = 1; s < (1<<k);s++){
        if(!check(s)) continue; // 不合法 s 直接跳过
        for(int i=1;i<=n;i++){
            res[s] = min(res[s], dp[i][s]); //更新得到res[s]
        }
    }
    for(int s = 1; s < (1 << k); s++){
        for(int subs=(s-1)&s;subs;subs=(subs-1)&s){
            res[s] = min(res[s], res[subs] + res[s^subs]); //不合法的s保证不会发生转移,因为提前赋值为inf
        }
    }
    printf("%d\n", res[(1<<k)-1]);
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!