定義
摘自百度百科的定義:
斯坦納樹問題是組合優化問題,與 最小生成樹相似 ,是最短網絡的一種。最小生成樹是在給定的點集和邊中尋求最短網絡使所有點連通。而最小斯坦納樹允許在給定點外增加額外的點,使生成的最短網絡開銷最小。
可以這么理解:一個圖的生成樹是構造一棵樹把所有點給聯通,而斯坦納樹則是構造一棵樹把給定的幾個點聯通。如同生成樹有最小的一棵,斯坦納樹也有最小的。如何求最小斯坦納樹,是我們今天要探討的話題。
實現
-
例題:Luogu P6192【模板】最小斯坦納樹
給定一個包含 \(n\) 個結點和 \(m\) 條帶權邊的無向連通圖 \(G=(V,E)\)。
再給定包含 \(k\) 個結點的點集 \(S\),選出 \(G\) 的子圖 \(G'=(V',E')\),使得:
- \(S\subseteq V'\)
- \(G'\) 為連通圖;
- \(E'\) 中所有邊的權值和最小。
你只需要求出 \(E'\) 中所有邊的權值和。
\(n \leq 100, m \leq 500, k \leq 10\)。
求最小斯坦納樹,我們使用的是 狀壓DP。
首先非常顯然的是,這個選出來的子圖 \(G'\) 一定是個樹。
令 \(f_{i,S}\) 表示當前這個樹的根為 \(i\),選出的點的集合為 \(S\)(注意這里選出的點專指那 \(k\) 個點中的點),這里的 \(S\) 在 dp 中是被狀壓的。
- 第一種轉移方式:
這種轉移方式意義在於把一個根可能會連出多棵 \(S\) 互不相交的樹,該方程可以合並它們。此時這個根的度數 \(\geq 1\)。以下是一個示意圖,其中 \(5,6,7\) 三個點屬於目標的那 \(k\) 個點,\(3\) 是目前的 \(i\)。
- 第二種轉移方式:
這種轉移方式意義在於把一個根的狀態轉移到與他相鄰的一個根上。此時根 \(i\) 的度數 \(=1\)。示意圖如下,其中 \(i = 1,j=3\),\(S=\{5,6\}\),橙色虛線代表待擴展的邊 \((i,j)\)。
考慮 dp 順序,顯然 \(S\) 從小到大枚舉即可。
對於第一種轉移方式,只需枚舉 \(S\) 的子集 \(T\)。對於第二種轉移方式,注意到這玩意兒是個 三角不等式,聯想到最短路也是如此——沒錯,用最短路跑一遍就行了。
參考代碼
int n, m, k, f[Maxn][1 << Maxk];
struct Edge {
int next, to, dis;
}
edge[Maxm * 2];
int head[Maxn], edge_num;
void add_edge(int from, int to, int dis) {
edge[++edge_num].next = head[from];
edge[edge_num].to = to;
edge[edge_num].dis = dis;
head[from] = edge_num;
}
queue <int> Q; int dist[Maxn]; bool inq[Maxn];
void SPFA(int S) {
memset(inq, 0, sizeof(inq));
for(int i = 1; i <= n; ++i) {
dist[i] = f[i][S];
if(dist[i] != 1061109567) Q.push(i), inq[i] = 1;
}
while(Q.size()) {
int u = Q.front();
Q.pop();
inq[u] = 0;
for(int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if(dist[v] > dist[u] + edge[i].dis) {
dist[v] = dist[u] + edge[i].dis;
if(!inq[v]) {
inq[v] = 1;
Q.push(v);
}
}
}
}
for(int i = 1; i <= n; ++i) f[i][S] = dist[i];
}
int main() {
n = read(); m = read(); k = read();
int u, v, w;
for(int i = 1; i <= m; ++i) {
u = read(); v = read(); w = read();
add_edge(u, v, w);
add_edge(v, u, w);
}
memset(f, 63, sizeof(f));
for(int i = 1; i <= k; ++i) {
u = read();
f[u][1 << (i - 1)] = 0;
}
for(int i = 1; i <= n; ++i) f[i][0] = 0;
for(int S = 0; S < (1 << k); ++S) {
for(int i = 1; i <= n; ++i)
for(int T = S & (S - 1); T; T = S & (T - 1))
f[i][S] = min(f[i][S], f[i][T] + f[i][T ^ S]);
SPFA(S);
}
int ans = 2e9;
for(int i = 1; i <= n; ++i) ans = min(ans, f[i][(1 << k) - 1]);
cout << ans << endl;
return 0;
}
例題
- [WC2008] 游覽計划
考慮把每個方格當成一個點,最小斯坦納樹搞它就完了。
需要注意的是,由於這道題從邊權變成了點權,因此需要把第一種轉移方式改成
原因顯然,因為合並狀態時 \(i\) 的點權被算了兩次,需要減去一次。
另外,這道題需要輸出方案。一個解決方法是記錄前驅、回溯解決。怎么 dp 過來,就怎么找回去,如果某個狀態和上個狀態滿足 dp 方程,說明這個狀態是從上個狀態轉移過來的,從而可以計算出哪些點是選了的。具體請參見代碼的 dfs 部分。
代碼:
int w, h, n, cnt, root, val[Maxn], dp[Maxn][1 << Maxk], pre[Maxn][1 << Maxk];
struct Edge {
int next, to;
}
edge[Maxm];
int head[Maxn], edge_num;
inline void add_edge(int from, int to) {
edge[++edge_num].next = head[from];
edge[edge_num].to = to;
head[from] = edge_num;
}
inline int f(int i, int j) {
return h * (i - 1) + j;
}
queue <int> Q; bool inq[Maxn];
void SPFA(int S) {
for(int i = 1; i <= n; ++i) if(dp[i][S] != 1061109567) Q.push(i), inq[i] = 1;
while(Q.size()) {
int u = Q.front();
Q.pop();
inq[u] = 0;
for(rg int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if(dp[v][S] > dp[u][S] + val[v]) {
dp[v][S] = dp[u][S] + val[v];
pre[v][S] = u;
if(!inq[v]) inq[v] = 1, Q.push(v);
}
}
}
}
bool ans[Maxn], vis[Maxn][1 << Maxk];
void dfs(int u, int S) {
if(!S || vis[u][S]) return;
vis[u][S] = 1;
if(pre[u][S] && dp[pre[u][S]][S] + val[u] == dp[u][S]) dfs(pre[u][S], S), ans[u] = 1;
for(rg int T = S & (S - 1); T; T = S & (T - 1))
if(dp[u][T] + dp[u][S ^ T] - val[u] == dp[u][S]) dfs(u, T), dfs(u, S ^ T);
}
int main() {
w = read(); h = read(); n = w * h;
for(rg int i = 1; i <= w; ++i)
for(rg int j = 1; j <= h; ++j) {
val[f(i, j)] = read();
if(i > 1) add_edge(f(i, j), f(i - 1, j));
if(j > 1) add_edge(f(i, j), f(i, j - 1));
if(i < w) add_edge(f(i, j), f(i + 1, j));
if(j < h) add_edge(f(i, j), f(i, j + 1));
}
memset(dp, 63, sizeof(dp));
for(rg int i = 1; i <= n; ++i) {
if(!val[i]) dp[i][1 << (cnt++)] = 0, root = i;
dp[i][0] = 0;
}
for(rg int S = 0; S < (1 << cnt); ++S) {
for(rg int i = 1; i <= n; ++i)
for(rg int T = S & (S - 1); T; T = S & (T - 1))
dp[i][S] = min(dp[i][S], dp[i][T] + dp[i][S ^ T] - val[i]);
SPFA(S);
}
dfs(root, (1 << cnt) - 1);
cout << dp[root][(1 << cnt) - 1] << endl;
for(rg int i = 1; i <= w; ++i) {
for(rg int j = 1; j <= h; ++j) {
int pos = f(i, j);
if(!val[pos]) printf("x");
else if(ans[pos]) printf("o");
else printf("_");
}
printf("\n");
}
return 0;
}