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