最小斯坦納樹 學習筆記


定義

摘自百度百科的定義:

斯坦納樹問題是組合優化問題,與 最小生成樹相似 ,是最短網絡的一種。最小生成樹是在給定的點集和邊中尋求最短網絡使所有點連通。而最小斯坦納樹允許在給定點外增加額外的點,使生成的最短網絡開銷最小。

可以這么理解:一個圖的生成樹是構造一棵樹把所有點給聯通,而斯坦納樹則是構造一棵樹把給定的幾個點聯通。如同生成樹有最小的一棵,斯坦納樹也有最小的。如何求最小斯坦納樹,是我們今天要探討的話題。

實現

  • 例題:Luogu P6192【模板】最小斯坦納樹

    給定一個包含 \(n\) 個結點和 \(m\) 條帶權邊的無向連通圖 \(G=(V,E)\)

    再給定包含 \(k\) 個結點的點集 \(S\),選出 \(G\) 的子圖 \(G'=(V',E')\),使得:

    1. \(S\subseteq V'\)
    2. \(G'\) 為連通圖;
    3. \(E'\) 中所有邊的權值和最小。

    你只需要求出 \(E'\) 中所有邊的權值和。

    \(n \leq 100, m \leq 500, k \leq 10\)

求最小斯坦納樹,我們使用的是 狀壓DP

首先非常顯然的是,這個選出來的子圖 \(G'\) 一定是個樹。

\(f_{i,S}\) 表示當前這個樹的根為 \(i\),選出的點的集合為 \(S\)(注意這里選出的點專指那 \(k\) 個點中的點),這里的 \(S\) 在 dp 中是被狀壓的。

  • 第一種轉移方式:

\[f_{i,S} = f_{i,T} + f_{i,S-T} \ \ \ (T \subseteq S) \]

這種轉移方式意義在於把一個根可能會連出多棵 \(S\) 互不相交的樹,該方程可以合並它們。此時這個根的度數 \(\geq 1\)。以下是一個示意圖,其中 \(5,6,7\) 三個點屬於目標的那 \(k\) 個點,\(3\) 是目前的 \(i\)

  • 第二種轉移方式:

\[f_{i,S} = f_{j,S} + w(i,j) \]

這種轉移方式意義在於把一個根的狀態轉移到與他相鄰的一個根上。此時根 \(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] 游覽計划

考慮把每個方格當成一個點,最小斯坦納樹搞它就完了。

需要注意的是,由於這道題從邊權變成了點權,因此需要把第一種轉移方式改成

\[f_{i,S} = f_{i,T} + f_{i,S-T} - w_i\ \ \ (T \subseteq S) \]

原因顯然,因為合並狀態時 \(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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM