最小斯坦纳树
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;
}