[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
即可。