最小斯坦纳树打印方案
阅读本文需要完全掌握上一篇文章《动态规划求解最小斯坦纳树》。
我们以求解“最小边权和”斯坦纳树为例进行演示。
【前情回顾】
动态规划求解最小斯坦纳树的过程,主要依靠两个步骤:合并子树,强行连通。
除了最初始的 d p [ 1 < < ( i − 1 ) ] [ t e r m i n a l [ i ] ] dp[1<<(i-1)][terminal[i]] dp[1<<(i−1)][terminal[i]]之外,其余的所有 d p dp dp 状态都是靠合并子树 + 强行连通产生的。
如何去求一个 d p [ S ] [ t e r m i n a l [ i ] ] dp[S][terminal[i]] dp[S][terminal[i]] 所表示的具体的最小斯坦纳树?只要遵循下面步骤:
1.求出 d p [ S ] [ t e r m i n a l [ i ] ] dp[S][terminal[i]] dp[S][terminal[i]] 是被谁转移的? 这个和 D i j k s t r a Dijkstra Dijkstra 求最短路时记录前驱结点相同。对于每个结点 i , i ∈ V i,i\in V i,i∈V,我们需要在强行连通操作中记录 i i i 是被谁转移的?将答案存在 f a [ S ] [ i ] fa[S][i] fa[S][i] 中。
if (dp[s][v] > dp[s][u] + w(u,v)) {
fa[s][v] = u;
q.push({dp[s][v],v});
}
我们一直回溯下去(假设 v v v 是由 u u u 转移而来, u u u 是由 t t t 转移而来, ⋯ \cdots ⋯,最终一定存在一个点 x x x, d p [ S ] [ x ] dp[S][x] dp[S][x] 一定是由其自身的子树合并而来,即 f a [ S ] [ x ] = x fa[S][x]=x fa[S][x]=x。)当回溯到由自身的子树合并而来的值的时候,我们终止循环。
int v = terminal[1];
while (fa[S][v] != v) {
v = fa[S][v];
ans.push_back(v); //ans 是最终的最小斯坦纳树点集
}
接下来怎么操作?现在 v v v 已经是由自身的子树合并而来的,所以接下来我们要在合并子树操作中记录前驱值(子树所包含的集合)。
记录 d p [ S ] [ v ] dp[S][v] dp[S][v] 合并了自身的哪两个子树?
设 m e r g e [ S ] [ v ] [ 0 ] = S ′ , m e r g e [ S ] [ v ] [ 1 ] = S − S ′ merge[S][v][0]=S',merge[S][v][1]=S-S' merge[S][v][0]=S′,merge[S][v][1]=S−S′(用两个数组记录 d p [ S ] [ v ] dp[S][v] dp[S][v] 是合并了哪两个子树 d p [ S ′ ] [ v ] , d p [ S − S ′ ] [ v ] dp[S'][v],dp[S-S'][v] dp[S′][v],dp[S−S′][v])。为了方便起见通常令 m e r g e [ S ] [ v ] [ 0 ] merge[S][v][0] merge[S][v][0] 存较小的集合, m e r g e [ S ] [ v ] [ 1 ] merge[S][v][1] merge[S][v][1] 存较大的集合。
for (int t = s; t != 0; t = (t - 1) & s) {
for (int i = 0; i < N; i++) {
if(dp[s][i] > dp[t][i] + dp[s ^ t][i]) {
dp[s][i] = dp[t][i] + dp[s ^ t][i];
merge[s][i][0] = min(t, s ^ t);
merge[s][i][1] = max(t, s ^ t);
}
}
}
接下来,只要求解 d p [ S ′ ] [ v ] dp[S'][v] dp[S′][v] 和 d p [ S − S ′ ] [ v ] dp[S-S'][v] dp[S−S′][v] 的前驱结点即可。不难发现这是一个递归的过程,而递归的终点就是 我们递归到了初始化时的值。
//以任意的S中的一个点为起点。
int v = terminal[1];
map<int,int> ans; //用集合去重
work(S,v);
----------------------------------------
//写一个递归函数
void work(int S,int v) {
while (fa[S][v] != v) {
ans[v] = 1;
v = fa[S][v];
}
int cnt = 0;
for (int i = 0; i < k; i++) {
if (S & (1 << i)) {
cnt++;
}
}
if (cnt == 1) return; //强行连通操作完了之后,如果|S|是1,说明已经到了初始值,直接退出循环
int mask1 = merge[S][v][0], mask2 = merge[S][v][1];
work(mask1, v); work(mask2,v);
}
-----------------------------------------
//最终答案就是map中的点。
【模版】
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int K = 11;
const int N = 2000;
vector<PII> g[N]; //邻接表存图
int n, m, k;
map<int,int> ans;
int terminal[K], fa[1 << K][N], Merge[1 << K][N][2];
int bnt = 0;
void work(int S,int v) {
bnt++;
if(bnt > 1000) return;
while (fa[S][v] != v) {
ans[v] = 1;
v = fa[S][v];
ans[v] = 1;
}
//强行连通操作完了之后,如果|S|是1,说明已经到了初始值,直接退出循环
int cnt = 0;
for (int i = 0; i < k; i++) if (S & (1 << i)) cnt++;
if (cnt == 1) return;
int mask1 = Merge[S][v][0], mask2 = Merge[S][v][1];
work(mask1, v); work(mask2,v);
}
void add (int u, int v, int w) { g[u].push_back({v,w}); }
void printSteinerTree(){
work((1 << k) - 1, terminal[1]);
for (auto i : ans) cout << i.first << ' ';
cout << endl;
}
void slove () {
cin >> n >> k >> m;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add (u, v, w), add(v, u, w);
}
vector<vector<int>> dp(1 << k, vector<int>(n + 1, INF));
for (int i = 1; i <= k; i++) {
cin >> terminal[i];
dp[1 << (i - 1)][terminal[i]] = 0;
}
for (int S = 1; S < (1 << k); S++) {
for (int t = S; t; t = (t - 1) & S ) {
for (int u = 1; u <= n; u++) {
if(dp[S][u] > dp[t][u] + dp[S ^ t][u]) {
dp[S][u] = dp[t][u] + dp[S ^ t][u];
Merge[S][u][0] = min(t, S ^ t);
Merge[S][u][1] = max(t, S ^ t);
}
}
}
priority_queue<PII, vector<PII>, greater<PII>> q;
for (int i = 1; i <= n; i++) {
fa[S][i] = i; // 初始化 i是由自身转移而来的
if (dp[S][i] != INF) q.push({dp[S][i], i});
}
while (q.size()) {
auto [d,u] = q.top();
q.pop();
if (d != dp[S][u]) continue;
for (auto [v,w] : g[u]) {
if (dp[S][v] > dp[S][u] + w) {
dp[S][v] = dp[S][u] + w;
fa[S][v] = u;
q.push({dp[S][v], v});
}
}
}
}
printSteinerTree();
cout << dp[(1 << k) - 1][terminal[1]] << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}