最小斯坦纳树

1.1 最小斯坦纳树模型

是一个经典模型:给定一张图和图上的一些关键点,图上边有非负边权,要求选择一些边使得所有关键点联通并且边权和最小。

关键点数一般很小,假设点数、边数、关键点数分别为 \(n, m, k\),那么算法的时间复杂度是 \(O(2^k m \log n + 3^k n)\)

P6192 【模板】最小斯坦纳树

题意同上。

首先显然最终的生成子图一定是一棵树。我们对这棵隐树做类似树形 dp 的东西。先看看式子:

\(dp_{i,S}\) 表示第 \(i\) 个节点为根的子树,包含 \(S\) 关键点集合的最小边权和。

有两个转移:

  • 对于 \(i-j\)\(dp_{i, S} \leftarrow dp_{j, S} + w_{i, j}\)
  • 对于 \(T \subseteq S\)\(dp_{i,S} \leftarrow dp_{i,T} + dp_{i,S-T}\)

初始 \(dp_{*, 0} = 0\),如果 \(i \in \mathrm{keynodes}\) 那么 \(dp_{i,i} = 0\)

其中 \(\leftarrow\)\(\mathrm{checkmin}\)

我们分类讨论证明这个转移是正确的:

首先如果 \(j\)\(i\) 的父亲,那么会白白多算,是不可能真正转移到的。

然后如果 \(i\) 有若干个儿子,其可以从每一个儿子上转移而来,再通过类似树上背包的方式合并。

考虑转移顺序。显然有第二维度从低到高的拓扑结构。枚举第二维度,可以先做第二个转移,对于每一个 \(i\),枚举子集耗费 \(3^k\)。然后做第一个转移,这个转移式子是 \(dp_{j} \leftarrow dp_{i} + w_{i,j}\),发现很像 dijkstra 结构。于是可以用稍微变形的 dijkstra 做。

时间复杂度是 \(O(2^k m \log n + 3^k n)\)

GYM 100722 D Ticket to Ride

【题意】
给定 \(n\)\(m\) 边的图,给定 \(4\) 个节点对,要求选出一些边使得这 \(4\) 对节点两两联通。

【分析】

可以考虑先 \(2^4\) 枚举哪些对节点是连在一起的,然后求最小斯坦纳树,再合并。

也可以去掉 \(2^4\):发现 dp 的时候其实做了所有关键点集合的答案,再做一次 dp 合并即可。

map<string, int> mp; int n, m; int fa[33]; 
int get(int x) {if(fa[x] == x) return x; else return fa[x] = get(fa[x]);}
void merge(int x, int y) {x = get(x); y = get(y); fa[x] = y; }
vector<pii> g[33];  int key[4][2]; int dp[33][1<<8]; bool vis[33]; 
int ans[1<<4]; 
int getans(int state) {
    vector<int> pt; 
    f(i, 0, 3) if((state >> i) & 1) {
        pt.push_back(key[i][0]); pt.push_back(key[i][1]); 
    }
    sort(pt.begin(), pt.end()); 
    int cnt = unique(pt.begin(), pt.end()) - pt.begin() - 1; 
    f(i, 1, n) f(j, 0, (1 << (cnt + 1)) - 1) dp[i][j] = inf;
    f(i, 0, cnt) dp[pt[i]][(1<<i)] = 0;
    f(i, 1, n) dp[i][0] = 0; 
    f(i, 0, (1 << (cnt + 1)) - 1) {
        f(j, 1, n) {
            for(int t = i; t >= 0; t = (t - 1) & i) {
                cmin(dp[j][i], dp[j][t] + dp[j][i - t]); 
                if(t == 0) break;
            }
        }
        priority_queue<pii> q; 
        int mn = 0; 
        f(j, 1, n) vis[j] = 0; 
        f(j, 1, n)if(mn == 0 || dp[j][i] < dp[mn][i]) {mn = j;}
        q.push({-dp[mn][i], mn}); 
        while(!q.empty()) {
            pii now = q.top(); q.pop(); 
            if(vis[now.second]) continue;
            vis[now.second] = 1; cmin(dp[now.second][i], -now.first); 
            for(pii j : g[now.second]) {
                if(vis[j.first]) continue;
                q.push({-dp[now.second][i] - j.second, j.first}); 
            }
        }
    }
    int ret = inf; 
    f(i, 1, n) cmin(ret, dp[i][(1 << (cnt + 1)) - 1]); 
    return ret; 
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    //freopen();
    //freopen();
    //time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    while(cin >> n >> m && n != 0) {
        mp.clear(); f(i, 1, n) {string s; cin >> s; mp[s] = i; g[i].clear(); }
        f(i, 1, m) {
            string s, t; cin >> s >> t; int w; cin >> w; 
            g[mp[s]].push_back({mp[t], w}); 
            g[mp[t]].push_back({mp[s], w}); 
        }
        f(i, 0, 3) f(j, 0, 1) {
            string u; cin >> u; key[i][j] = mp[u]; 
        }
        f(i, 0, 15) ans[i] = getans(i);
        f(i, 0, 15) for(int j = i; ; j = (j - 1) & i) {
            cmin(ans[i], ans[j] + ans[i - j]); if(j == 0) break;
        }
        cout << ans[15] << endl; 
    }
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
posted @ 2023-05-12 22:29  OIer某罗  阅读(179)  评论(0编辑  收藏  举报