[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\) 的最小值。

我们不妨把它的过程画出来,一下是某张图的一部分。

image

那么在不经过编号 \(\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 即可。

posted @ 2022-12-10 19:29  MoyouSayuki  阅读(18)  评论(0编辑  收藏  举报
:name :name