前言:
今天做题的时候,碰到一个有向图求最小环问题,发现除了经典的 Floyd求最小环 之外,熟知的求最短路问题的 dijkstra算法 也可以求最小环。
有向图有以下三种实现方式,而无向图只能用第一种实现方式。
实现方式1:删边求最短距离
对于一条有向边 ( u , v ) (u, v) (u,v),可以删掉这条边(不用这条边更新)跑从 v 到 u 的最短距离,加上这条边的权值便是该边所在的最小环。
对于每条边都求一次最短路,所以时间复杂度: O ( m ∗ m l o g n ) O(m*mlogn) O(m∗mlogn)。
这种实现方法有向图无向图都可用。
有向图 Code:
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
const int N = 50010, mod = 1e9+7;
int T, n, m, k;
int a[N], c;
int dist[N], f[N];
vector<PII> e[N];
int mina = 1e9; //最小环权值
struct node{
int x, y, w;
}b[N];
int dij(int st, int en)
{
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
for(int i=1;i<=n;i++) dist[i] = 1e10, f[i] = 0;
dist[st] = 0;
while(que.size())
{
int x = que.top().se;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(auto it : e[x])
{
int tx = it.fi, w = it.se;
if(dist[tx] > dist[x] + w)
dist[tx] = dist[x] + w, que.push({dist[tx], tx});
}
}
}
signed main(){
Ios;
cin>>n>>m;
int ans = 0;
for(int i=1;i<=m;i++)
{
int x, y, z;cin>>x>>y>>z;
e[x].pb({y, z});
b[i] = {x, y, z};
}
for(int i=1;i<=m;i++)
{
int x = b[i].x, y = b[i].y, w = b[i].w;
dij(y, x);
mina = min(mina, dist[x] + w); //从y到x的最短距离加上从x到y的这条边就是这个边所在的最小环
}
cout << mina;
return 0;
}
无向图 Code:
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
const int N = 50010, mod = 1e9+7;
int T, n, m, k;
int a[N], c;
int dist[N], f[N];
vector<PII> e[N];
int pre[N];
struct node{
int x, y, w;
}b[N];
int dij(int st, int en)
{
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
for(int i=1;i<=n;i++) dist[i] = 1e10, f[i] = 0, pre[i] = 0;
dist[st] = 0;
while(que.size())
{
int x = que.top().se;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(auto it : e[x])
{
int tx = it.fi, w = it.se;
if(x == st && tx == en || x == en && tx == st) continue; //st-en这条边不能走
if(dist[tx] > dist[x] + w)
dist[tx] = dist[x] + w, que.push({dist[tx], tx}), pre[tx] = x;
}
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x, y, z;cin>>x>>y>>z;
e[x].pb({y, z});
e[y].pb({x, z});
b[i] = {x, y, z};
}
stack<int> stk;
int mina = 1e9;
for(int i=1;i<=m;i++)
{
int x = b[i].x, y = b[i].y, w = b[i].w;
dij(y, x);
if(dist[x] + w < mina){ //记录最小环中的所有点
mina = dist[x] + w;
while(stk.size()) stk.pop();
int t = x;
while(t) stk.push(t), t = pre[t];
}
}
cout << mina << endl;
if(!stk.size()) cout<<"No solution.";
while(stk.size()) cout << stk.top() <<" ", stk.pop();
return 0;
}
有向图实现方式2:回到起点构成环
将每一个点都作为起点,跑 dijkstra。
对于每个点,从该点出发判断是否能够再回到起点,如果能,说明从该点出发能构成环。
因为是跑最短路求得的,所以这个环是 所有从起点出发回到起点的环中 权值最小的一个。
将所有点作为起点所得的最小环的最小值 便是 整张图的最小环。
每个点都跑一遍 dij,所以 时间复杂度: O ( n ∗ m l o g n ) O(n*mlogn) O(n∗mlogn)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define PII pair<int,int>
#define pb push_back
const int N = 200010, mod = 1e9+7;
int T, n, m, k;
int a[N], c;
int dist[N], f[N];
vector<PII> e[N];
int mina = 1e18; //最小环大小
void dij(int st)
{
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
for(int i=1;i<=n;i++) dist[i] = 1e9, f[i] = 0;
dist[st] = 0;
while(que.size())
{
int x = que.top().se;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(auto it : e[x])
{
int tx = it.first, w = it.second;
if(tx == st) mina = min(mina, dist[x] + w); //下一个节点是起点,那么环的大小就是起点到当前点距离+到起点这条边的权值。
if(dist[tx] > dist[x] + w)
dist[tx] = dist[x] + w, que.push({dist[tx], tx});
}
}
}
signed main(){
Ios;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x, y, z;cin>>x>>y>>z;
e[x].pb({y, z});
}
cout << mina;
return 0;
}
有向图实现方法3:任意两点间的最短距离
对于每个点都跑一遍最短路,便得到任意两点间的最短距离。
那么从 x 点到 y 点的最短距离 + 从 y 点到 x 点的最短距离 便构成了一个最小环。
二重遍历所有组合,取所有环中的最小值便是整张图的最小环。
每个点都求一次最短路,所以时间复杂度: O ( n ∗ m l o g n ) O(n * mlogn) O(n∗mlogn)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
const int N = 5010, mod = 1e9+7;
int T, n, m, k;
int a[N], c;
int dist[N][N], f[N];
vector<PII> e[N];
int dij(int st)
{
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
for(int i=1;i<=n;i++) dist[st][i] = 1e10, f[i] = 0;
dist[st][st] = 0;
while(que.size())
{
int x = que.top().se;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(auto it : e[x])
{
int tx = it.fi, w = it.se;
if(dist[st][tx] > dist[st][x] + w)
dist[st][tx] = dist[st][x] + w, que.push({dist[st][tx], tx});
}
}
}
signed main(){
Ios;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x, y, z;cin>>x>>y>>z;
e[x].pb({y, z});
}
for(int i=1;i<=n;i++) dij(i);
int mina = 1e9;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(i!=j) mina = min(mina, dist[i][j] + dist[j][i]);
}
cout << mina;
return 0;
}
例题
最后附上这道题目~
题意:
A 和 B 正在玩一个游戏,给出一张图,初始只有 n 个点,没有边。
- 起初 A 可以购买一些有向边加到图中,A 有总钱数 C C C,一共 m 条边,每条边有价格 w i w_i wi;
- 接下来到 B 操作,B 可以通过若干轮删边将所有边都删掉,每轮删边要满足:删掉的这些边在图中不构成环。
A 想要 B 删边的轮数尽量多,而 B 想删边的轮数尽量少。两者都采取最佳策略,问 B 要删边多少轮?
2
≤
n
≤
2000
,
1
≤
m
≤
5000
,
1
≤
c
≤
1
0
9
2≤n≤2000 , 1≤m≤5000, 1≤c≤10^9
2≤n≤2000,1≤m≤5000,1≤c≤109
1
≤
u
i
,
v
i
≤
n
,
u
i
≠
v
i
,
1
≤
w
i
≤
100000
1≤u_i,v_i≤n, u_i≠v_i, 1≤w_i≤100000
1≤ui,vi≤n,ui=vi,1≤wi≤100000
分析:
如果若干条边能够构成环的话,B 就需要两次操作才能将这个环删除:一次操作取若干条边破坏环,第二次操作删掉剩余边。
接下来就想错了,就想着 A 要使得图中的环尽可能多,然后 B 的操作次数就是 环数 + 1,然后就想着如何选择能够构成环最多,想到缩点等等。。整个思路就偏了。
无论是一个环 还是 无数个环,B 只需要两次操作就能将所有边删掉:一次操作取所有环中的若干条边,破坏所有的环,第二次操作删除所有剩余边。
所以:只要有环,B 都只需要 2 次操作。
如果没有环,只有边,那么 B 只需要 1 次操作;
如果没有边,那么 B 需要 0 次操作。
所以,如果 A 所拥有的钱数能够购买一个环,就能使得 B 的操作次数为 2。
可以将所有边都先加到图中,判断图中价格最小的一个环的价格,看是否不超过 C。
也就是,求有向图的最小环。
那么就可以用有向图的三种实现方法。
但是,对于第一种实现方法,时间复杂度为 O(m*mlogn),在这道题中会超时。
只能用第二种或者第三种实现方法。
实现二:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<int,int> mp;
const int N = 200010, mod = 1e9+7;
int T, n, m, k;
int a[N], c;
int dist[N], f[N];
vector<PII> e[N];
int mina = 1e18;
void dij(int st)
{
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
for(int i=1;i<=n;i++) dist[i] = 1e9, f[i] = 0;
dist[st] = 0;
while(que.size())
{
int x = que.top().se;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(auto it : e[x])
{
int tx = it.first, w = it.second;
if(tx == st) mina = min(mina, dist[x] + w);
if(dist[tx] > dist[x] + w)
dist[tx] = dist[x] + w, que.push({dist[tx], tx});
}
}
}
signed main(){
Ios;
cin>>n>>m>>c;
int ans = 0;
for(int i=1;i<=m;i++)
{
int x, y, z;cin>>x>>y>>z;
e[x].pb({y, z});
if(z <= c) ans = 1;
}
for(int i=1;i<=n;i++) dij(i);
if(mina <= c) ans = 2;
cout << ans;
return 0;
}
实现三:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<int,int> mp;
const int N = 5010, mod = 1e9+7;
int T, n, m, k;
int a[N], c;
int dist[N][N], f[N];
vector<PII> e[N];
int dij(int st)
{
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
for(int i=1;i<=n;i++) dist[st][i] = 1e10, f[i] = 0;
dist[st][st] = 0;
while(que.size())
{
int x = que.top().se;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(auto it : e[x])
{
int tx = it.fi, w = it.se;
if(dist[st][tx] > dist[st][x] + w)
dist[st][tx] = dist[st][x] + w, que.push({dist[st][tx], tx});
}
}
}
signed main(){
Ios;
cin>>n>>m>>c;
int ans = 0;
for(int i=1;i<=m;i++)
{
int x, y, z;cin>>x>>y>>z;
e[x].pb({y, z});
if(z <= c) ans = 1;
}
for(int i=1;i<=n;i++) dij(i);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(i!=j && dist[i][j] + dist[j][i] <= c) ans = 2;
}
cout << ans;
return 0;
}
以前只知道 Floyd求最小环,没想到 dijkstra求最小环 的时间效率甚至更优。
如果哪里有问题的话欢迎留言讨论~