提交入口:https://www.luogu.com.cn/problem/P6175

网上没找到满意的题解,大佬们微言大义,不是很能看懂。我在这里留下分析的大致思路,不保证正确,欢迎讨论。

题目描述

给定一张无向图,求图中一个至少包含 $3$ 个点的环,环上的节点不重复,并且环上的边的长度之和最小。该问题称为无向图的最小环问题。在本题中,你需要输出最小的环的边权和。若无解,输出 No solution.

输入格式

第一行两个正整数 $n,m$ 表示点数和边数。

接下来 $m$ 行,每行三个正整数 $u,v,d$,表示节点 $u,v$ 之间有一条长度为 $d$ 的边。

输出格式

输出边权和最小的环的边权和。若无解,输出 No solution.

样例 #1

样例输入 #1

1
2
3
4
5
6
7
8
5 7
1 4 1
1 3 300
3 1 10
1 2 16
2 3 100
2 5 15
5 3 20

样例输出 #1

1
61

提示

样例解释:一种可行的方案:$1-3-5-2-1$。

对于 $20\%$ 的数据,$n,m \leq 10$。

对于 $60\%$ 的数据,$m\leq 100$。

对于 $100\%$ 的数据,$1\le n\leq 100$,$1\le m\leq 5\times 10^3$,$1 \leq d \leq 10^5$。

无解输出包括句号。


分析 & 代码

错误思路

本题不存在优于$O(n^3)$很多的算法,请停止无谓的尝试。

不过在尝试的过程中我也理解了一些东西…改动的tarjan部分也许可以记一下,所以还是决定贴出来备忘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include<cstdio>
#include<cstring>
#define MIN(a,b) ((a)<(b)?(a):(b))
using namespace std;

const int maxn = 103;
const int maxm = 5003;
const int INF = 0x7fffffff;
int head[maxn],tot,depth[maxn],fa[maxn],fa2[maxn],dis[maxn][maxn];
int low[maxn],dfn[maxn],stac[maxn],num,ans,n,m,top,uset[maxn];
struct Edge{int v,w,next;}edge[maxm<<1];
bool vis[maxn],ins[maxn];

void makeset(int size){
for(int i=0;i<size;i++)uset[i]=i;
}

int find(int x){
if(x!=uset[x])uset[x]=find(uset[x]);
return uset[x];
}

void unionset(int x,int y){
x=find(x);
y=find(y);
if(x==y)return;
uset[x]=y;
}

inline void addedge(int x,int y,int z){
edge[++tot].v=y;
edge[tot].w=z;
edge[tot].next=head[x];
head[x]=tot;
}

void tarjan(int x){
low[x]=dfn[x]=++num;
stac[++top]=x; ins[x]=true;
for(int i=head[x];i;i=edge[i].next){
int y=edge[i].v;
if(!dfn[y]){
fa2[y]=x;
tarjan(y);
low[x]=MIN(low[x],low[y]);
}
else if(ins[y]&&y!=fa2[x]){
low[x]=MIN(low[x],dfn[y]);
}
}
if(dfn[x]==low[x]){
int y;
do{
y=stac[top--];
ins[y]=false;
unionset(x,y);
}while(x!=y);
}
}

void dfs(int x,int dep){ //可能有重边
vis[x]=true;
depth[x]=dep;
for(int i=head[x];i;i=edge[i].next){
int y=edge[i].v;
if(!vis[y]){
fa[y]=x;
dfs(y,depth[x]+dis[x][y]);
}
else if(y!=fa[x]&&(depth[x]-depth[y]>0)&&(find(x)==find(y))){
ans=MIN(ans,dis[x][y]+depth[x]-depth[y]);
}
}
return;
}

int main(){
scanf("%d%d",&n,&m);
ans=INF;
makeset(n+1);
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
if(x==y)continue;
if(z<dis[x][y]||(!dis[x][y])){
addedge(x,y,z);
addedge(y,x,z);
dis[x][y]=dis[y][x]=z;
}
}

for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);

for(int s=1;s<=n;s++){
memset(fa,0,sizeof(fa));
memset(vis,false,sizeof(vis));
memset(depth,0,sizeof(depth));
dfs(s,0); int i=s+1;
while(i<=n){
if(!vis[i])dfs(i,0);
i++;
}
int j=1;
while(j<s){
if(!vis[j])dfs(j,0);
j++;
}
}

if(ans==INF)printf("No solution.");
else printf("%d",ans);
return 0;
}

以下数据将得不到正确答案:

1
2
3
4
5
6
7
8
9
10
11
5 10
4 2 3
5 2 5
3 4 2
5 3 4
2 3 3
1 5 5
2 1 1
5 4 2
1 4 2
1 3 4

27-1.jpg

原因在于,上面那个算法假了,想要穷尽所有情况,复杂度还得上一维,且常数巨大。

正确思路

定义: 设 $R_{i,j,k}$ 为经过编号为$i$,$j$,$k$ 节点的最小环。

$R_{i,j,k}$ 可以不存在。

考虑集合 $Ring=\{ R_{i,j,k}|1\leqslant i<j<k \leqslant n \}$ ,我们指出,符合题目要求的最小环 $r\in Ring$ 。

证明: $r$ 的节点数大于等于3,任取其中3个$a$,$b$,$c$ ,不妨设$a<b<c$。因为 $r$ 是全图中最小的环,所以 $r$ 也是经过 $a$,$b$,$c$ 的最小环。则 $r=R_{a,b,c}\in Ring$ ,证毕。

于是在算法上,只需要枚举集合 $Ring$ ,并不断更新ans。这提示我们从$floyd$考虑。简述$floyd$算法如下:

设$D_{i,j,k}$为 $i$ 到 $j$ 的只以 $(1…k)$ 集合中的节点为中间节点的最短路长度。
若最短路经过点 $k$,则$D_{i,j,k}=D_{i,k,k-1}+D_{k,j,k-1}$
若最短路不经过点 $k$,则$D_{i,j,k}=D_{i,j,k-1}$
因此,$D_{i,j,k}=\mathrm{min}(D_{i,j,k-1},D_{i,k,k-1}+D_{k,j,k-1})$

先给出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <cstdio>
#define MIN(a,b) ((a)<(b)?(a):(b))
using LL = long long;
using namespace std;

const LL INF = 1e13+3;
LL n,m,ans=INF;
LL dis[103][103],mcopy[103][103];

int main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=j)dis[i][j]=mcopy[i][j]=INF;

for(int i=1;i<=m;i++){
LL x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
dis[x][y]=dis[y][x]=MIN(dis[x][y],z);
mcopy[x][y]=mcopy[y][x]=dis[x][y];
}

for(int k=1;k<=n;k++){
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++) //k=2时也不会执行
ans=MIN(ans,dis[i][j]+mcopy[j][k]+mcopy[k][i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=dis[j][i]=MIN(dis[i][j],dis[i][k]+dis[k][j]);
}

if(ans==INF)printf("No solution.");
else printf("%lld",ans);

return 0;
}

这看起来有点奇怪。记住我们的目标是枚举集合$Ring$,这里解释以下核心代码:

1
2
3
4
5
6
7
8
9
10
//for循环首次不满足条件便退出
//奇怪的现象,不知道自己为什么会为此产生疑问
for(int k=1;k<=n;k++){
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++) //k=2时也不会执行
ans=MIN(ans,dis[i][j]+mcopy[j][k]+mcopy[k][i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=dis[j][i]=MIN(dis[i][j],dis[i][k]+dis[k][j]);
}

首先,由枚举顺序,已经有 $1\leqslant i<j<k \leqslant n$ 。

在Floyd枚举k的时候,已经得到了 $i,j (1\leqslant i\ne j \leqslant n)$ 只以 $\{ 1,…,k-1\}$ 集合中的节点为中间节点的最短路长度 $D_{i,j,k-1}$,它在代码中就是dis[i][j]

为什么第6行dis[i][j]加的是mcopy[][]

从生成的角度去理解它:k要在环中,必然要从k中伸出边,设伸出的两个边连的点是 i 和 j 。那么 $D_{i,j,k-1}$ 加上两个边的长度就是环的长度。而这两个边是原始数据输入的长度。

一个可能的疑惑:$D_{i,j,k-1}$ 不是 $D_{i,j,n}$ ,为什么要这样,直觉上用$D_{i,j,n}$ 不是更好吗?

答案是不能且不必要:

  • 若 $D_{i,j}$ 经过 k 点,不成环,使用$D_{i,j,n}$ 会得到错误答案。因此不能。
  • 若 $D_{i,j}$ 不经过 k 及编号以后的点,$D_{i,j,n}=D_{i,j,k-1}$,
  • 若 $D_{i,j}$ 经过 k 以后的点,考虑下图:27-2.png 这里需要反过来考虑:设 $p$ 为 $D_{i,j}$ 经过的最大的节点编号,则就长度而言 $R_{i,j,k}\geqslant R_{i’,j’,p}$ (这里$R_{i’,j’,p}$的存在性已经保证),因为 $D_{i’,j’}\leqslant \mathrm{len}\{i’,i,k,j,j’ \}$ 。而在后面的计算中,$R_{i’,j’,p}$ 会被计算到,如果合适,它会作为更优解更新ans

顺带一提,集合 $Ring$ 中所有元素都会被枚举到。