1. 程式人生 > 其它 >[POJ1734]Sightseeing 觀光之旅 題解

[POJ1734]Sightseeing 觀光之旅 題解

無向圖的最小環問題,前置芝士:\(\text{Floyd}\)

傳送門

題目描述

給定一張無向圖,求圖中一個至少包含 \(3\) 個點的環,環上的節點不重複,並且環上的邊的長度之和最小。

你需要輸出最小環的方案,若最小環不唯一,輸出任意一個均可。

想法

拿到題會想到找出所有的環,然後進行統計。

可是對於無向圖而言,要找到具體的環路不是一件易事。

於是考慮如何不找環。

思路

看到資料範圍:

\(1≤N≤100, 1≤M≤10000\)

點數這麼少,相當於直接明示用 \(O(n^3)\)\(\text{Floyd}\) 演算法。

整個圖裡跑完一遍

先看 \(\text{Floyd}\) 的核心程式碼:

for(int k = 1; k <= n; k ++)
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            f[i][j] = min(f[i][k], f[k][j]);

這個演算法以 \(k\) 為中繼點,\(i, j\) 作為附加狀態,進行 \(dp\) 地狀態轉移。

可以發現,在最外層的迴圈剛開始的時候,f[i][j] 表示在經過編號 \(k - 1\) 的點的情況下,\(i \rightarrow j\) 的最小值。

我們不妨把它的過程畫出來,一下是某張圖的一部分。

那麼在不經過編號 \(\geq k\) 的點的情況下,\(f[i][j]\) 一定就指的是下面的 \(i \rightarrow ... \rightarrow j\) 的路徑長度。

於是,發現 \(\min\limits_{1\leq i < j < k} f[i][j] + g[i][k] + g[k][j]\) 即滿足以下條件的一個最小環的長度:

  • 經過的點編號都不超過 \(k\)
  • 經過點 \(i, j, k\),且 \(k\) 處於 \(i, j\) 中間並只經過一條邊。

對於任意的 \(k\in [1,n]\),都可以在 \(O(n^2)\) 的時間內計算出他所對應的所有環的長度,於是我們只需要取一個最小值即可。

而對於輸出答案的限制,我們可以用一個數組 pos[i][j] 來儲存在 \(\text{Floyd}\) 過程中 \(i\)\(j\) 最短路徑的中繼點 \(k\)

於是我們可以進行遞迴處理。

程式碼

#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f

using namespace std;

const int N = 110, M = 2e4 + 10;

int d[N][N], g[N][N], pos[N][N];

vector<int> path;
int n, m;

void init()
{
    memset(pos, 0, sizeof pos);
    memset(g, 0x3f, sizeof g);
    for(int i = 1; i <= n; i ++) g[i][i] = 0;
    cin >> n >> m;
    for(int i = 1; i <= m; i ++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = g[b][a] = min(g[a][b], c); // 避免重邊
    }
    memcpy(d, g, sizeof g);
}

void get(int x, int y)
{
    if(x == y || !pos[x][y]) return ; // pos[x][y] == 0表示 x, y 之間沒有任何點
    get(x, pos[x][y]); // 遞迴左邊
    path.push_back(pos[x][y]); // 中間(當前點)
    get(pos[x][y], y); // 右邊
}

signed main()
{
    init();

    int ans = INF;
    for(int k = 1; k <= n; k ++)
    {
        for(int i = 1; i < k; i ++)
        {
            for(int j = i + 1; j < k; j ++)
            {
                if(ans > (long long)d[i][j] + g[i][k] + g[k][j]) // 列舉k的鄰點,如果i或j沒有到達k的邊,那麼右式必然大於INF,不會考慮
                {
                    ans = d[i][j] + g[i][k] + g[k][j];
                    path.clear();
                    path.push_back(i); // 按照i -> ... -> j -> k的順序存環,這題有spj,別急
                    get(i, j);
                    path.push_back(j);
                    path.push_back(k);
                }
            }
        }
        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= n; j ++)
                if(d[i][j] > d[i][k] + d[k][j]) // 更新f陣列
                    d[i][j] = d[i][k] + d[k][j],
                    pos[i][j] = k; // 記錄i, j的中繼點k
    } 

    if(ans == INF) puts("No solution."); // 無解情況
    else 
    {
        for(auto i : path) // 輸出方案
            cout << i << ' ';
    }

    return 0;
}

雙倍經驗,輸出 ans 即可。