【图论】环问题(最小环、最大环、环计数)
关于图论中环的一些常见问题,包括最小环、最大环、环计数
最小环
有向带权图最小环
这里不考虑自环,若存在自环,答案与所有自环取最小值即可。
算法
解法一:dijkstra枚举边,复杂度 O ( M ( N + M ) l o g N ) O(M(N+M)logN) O(M(N+M)logN)。对环上一条有向边 v → u v \rightarrow u v→u,从 u u u 到 v v v 的最短路 d d d 加上边长 w w w 就是包含该边的最小环长,因此枚举所有边即可。一种剪枝优化是可以比较队头与答案大小,若不优显然无需继续跑当前最短路。
解法二:dijkstra枚举顶点,复杂度 O ( N ( N + M ) l o g N ) O(N(N+M)logN) O(N(N+M)logN),优于前者。当源点 s s s 的所有邻点都被松弛后,重新将 s s s 设置为未访问,则 d [ s ] d[s] d[s] 就是 s s s 所在最小环长度。该方法不能在无向图中使用,否则会直接走回源点 s s s。
例题
题意:一人有代价地选择原图中一些边得到新图,一人在新图中进行若干轮删边操作,每轮所删边集不能含有环。显然新图中有环时必须删两轮,否则一轮删去所有边,若新图无边则无需删除。前者希望后者轮数尽可能多,于是转换为有向图最小环问题。
参考代码(解法一):
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 2005;
using PII = pair<int, int>;
vector<PII> G[MAXN];
int d[MAXN], mn = 1e9;
bitset<MAXN> vis;
void dijkstra(int s, int t)
{
memset(d, 0x3f, sizeof(d));
d[s] = 0;
vis.reset();
priority_queue<PII, vector<PII>, greater<PII>> q;
q.emplace(0, s);
while (!q.empty())
{
int v = q.top().second;
q.pop();
if (d[v] >= mn || v == t)
break;
if (vis[v])
continue;
vis[v] = 1;
for (auto [w, u] : G[v])
{
if (!vis[u] && d[v] + w < d[u])
{
d[u] = d[v] + w;
q.emplace(d[u], u);
}
}
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m, c, ans = 0;
cin >> n >> m >> c;
for (int i = 1; i <= m; i++)
{
int x, y, w;
cin >> x >> y >> w;
G[x].emplace_back(w, y);
if (w <= c)
ans = 1;
}
if (ans == 0)
{
cout << "0\n";
return 0;
}
for (int v = 1; v <= n; v++)
{
for (auto [w, u] : G[v])
{
dijkstra(u, v);
mn = min(mn, d[v] + w);
}
}
if (mn <= c)
ans = 2;
cout << ans << endl;
return 0;
}
参考代码(解法二):
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 2005;
using PII = pair<int, int>;
vector<PII> G[MAXN];
int d[MAXN];
bitset<MAXN> vis;
void dijkstra(int s)
{
memset(d, 0x3f, sizeof(d));
d[s] = 0;
vis.reset();
int flg = 1;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.emplace(0, s);
while (!q.empty())
{
int v = q.top().second;
q.pop();
if (!flg && v == s)
break;
if (vis[v])
continue;
vis[v] = 1;
for (auto [w, u] : G[v])
{
if (!vis[u] && d[v] + w < d[u])
{
d[u] = d[v] + w;
q.emplace(d[u], u);
}
}
if (flg)
{
vis[s] = 0, d[s] = 0x3f3f3f3f;
flg = 0;
}
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m, c, ans = 0;
cin >> n >> m >> c;
for (int i = 1; i <= m; i++)
{
int x, y, w;
cin >> x >> y >> w;
if (w <= c)
G[x].emplace_back(w, y), ans = 1;
}
if (ans == 0)
{
cout << "0\n";
return 0;
}
int mn = 0x3f3f3f3f;
for (int v = 1; v <= n; v++)
{
dijkstra(v);
mn = min(mn, d[v]);
}
if (mn <= c)
ans = 2;
cout << ans << endl;
return 0;
}
无向带权图最小环
算法
解法一:dijkstra枚举边,复杂度 O ( M ( N + M ) l o g N ) O(M(N+M)logN) O(M(N+M)logN)。注意在无向图中是双向边,必须屏蔽反边,否则会直接走回前一个点。这可以通过成对建边,并屏蔽反边编号来做到。此外,当我们走过一条边之后,无需再走它的反边,这可以保证复杂度不会因为双向边而增大,是非常重要的。
解法二:floyd,复杂度 O ( N 3 ) O(N^3) O(N3)。最经典的解法,在大部分情况下都适用。
例题
题意:求无向图中至少有3个点的最小环长度。这就意味着如果使用dijkstra,不仅要屏蔽反边,还要屏蔽所有与反边平行的重边,也就是说,对于当前枚举边 v → u v \rightarrow u v→u,进行dijkstra处在起点 u u u 时,无论如何不能走回 v v v。
参考代码(dijkstra):
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 2005, MAXM = MAXN << 3;
int head[MAXN], tot = 1;
struct Edge
{
int to, next, w;
} e[MAXM];
void add(int u, int v, int w)
{
e[++tot].to = v, e[tot].w = w, e[tot].next = head[u];
head[u] = tot;
}
using PII = pair<int, int>;
int d[MAXN], mn = 0x3f3f3f3f;
bool used[MAXM];
bitset<MAXN> vis;
void dijkstra(int s, int t)
{
memset(d, 0x3f, sizeof(d));
d[s] = 0;
vis.reset();
priority_queue<PII, vector<PII>, greater<PII>> q;
q.emplace(0, s);
while (!q.empty())
{
int v = q.top().second;
q.pop();
if (d[v] >= mn || v == t)
break;
if (vis[v])
continue;
vis[v] = 1;
for (int i = head[v]; i; i = e[i].next)
{
int u = e[i].to, w = e[i].w;
if (v == s && u == t)
continue;
if (!vis[u] && d[v] + w < d[u])
{
d[u] = d[v] + w;
q.emplace(d[u], u);
}
}
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int x, y, w;
cin >> x >> y >> w;
add(x, y, w), add(y, x, w);
}
for (int v = 1; v <= n; v++)
{
for (int i = head[v]; i; i = e[i].next)
{
if (used[i])
continue;
int u = e[i].to, w = e[i].w;
dijkstra(u, v);
mn = min(mn, d[v] + w);
used[i] = used[i ^ 1] = 1;
}
}
if (mn == 0x3f3f3f3f)
cout << "No solution." << endl;
else
cout << mn << endl;
return 0;
}
参考代码(floyd):
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 105, INF = 0x3f3f3f3f;
int n, m, d[MAXN][MAXN], a[MAXN][MAXN], ans = INF;
void floyd()
{
for (int k = 1; k <= n; k++)
{
for (int i = 1; i < k; i++)
for (int j = i + 1; j < k; j++)
if (a[i][k] != INF && a[k][j] != INF && d[i][j] + a[j][k] + a[k][i] < ans)
ans = d[i][j] + a[j][k] + a[k][i];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (d[i][k] + d[k][j] < d[i][j])
d[i][j] = d[i][k] + d[k][j];
}
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
memset(a, 0x3f, sizeof(a));
cin >> n >> m;
for (int i = 1; i <= n; i++)
a[i][i] = 0;
int x, y, w;
for (int i = 1; i <= m; i++)
cin >> x >> y >> w, a[y][x] = a[x][y] = min(a[x][y], w);
memcpy(d, a, sizeof(a));
floyd();
if (ans == INF)
cout << "No solution.\n";
else
cout << ans << endl;
return 0;
}
无权图最小环
算法
上述所有做法都可对应使用,其中dijkstra不需要堆,复杂度降掉一个log,变为普通bfs。
例题
不需要参考代码了吧
Codeforces Round 869 (Div.1) B. Fish Graph
题意:找到一个环,环上有一个点能够往环外岔出两条边。
思路:枚举度数不小于4的点作为环点,跑bfs求最小环,若有,答案加入另外两条边即可。
参考代码:
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 2e3 + 5;
int pre[MAXN], deg[MAXN];
vector<int> G[MAXN];
bitset<MAXN> vis;
int bfs(int s, int t)
{
memset(pre, 0, sizeof(pre));
vis.reset();
queue<pair<int, int>> q;
q.emplace(s, 0);
while (!q.empty())
{
auto [v, p] = q.front();
q.pop();
if (vis[v])
continue;
vis[v] = 1, pre[v] = p;
if (v == t)
return t;
for (auto u : G[v])
if (!vis[u] && !(v == s && u == t))
q.emplace(u, v);
}
return 0;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
cin >> t;
while (t--)
{
int n, m;
cin >> n >> m;
while (m--)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v), ++deg[v], ++deg[u];
}
int u = 0;
for (int i = 1; i <= n && !u; i++)
{
if (deg[i] < 4)
continue;
for (auto v : G[i])
if (u = bfs(v, i))
break;
}
if (!u)
cout << "NO\n";
else
{
cout << "YES\n";
int v = u;
vector<pair<int, int>> e;
while (pre[v])
{
e.emplace_back(v, pre[v]);
v = pre[v];
}
e.emplace_back(v, u);
int cnt = 0;
for (auto w : G[u])
{
if (w != pre[u] && w != v)
e.emplace_back(u, w), ++cnt;
if (cnt == 2)
break;
}
cout << e.size() << "\n";
for (auto [v, u] : e)
cout << v << " " << u << "\n";
}
for (int i = 1; i <= n; i++)
G[i].clear(), deg[i] = 0;
}
return 0;
}
最大环
无权图最大环
算法
如果图没有特殊性质,则这个问题是NPC的(最坏情况下是一个哈密顿环),需要通过dfs暴力枚举所有路径找到答案。
若每个点的出度最多为1,说明没有环嵌套的情况,这时图中每个大小不为1的强连通分量都是一个环,使用tarjan算法求出强连通分量即可找到最大环,同样也可求最小环。当然也可以跑拓扑排序把所有的非环边去除,再枚举所有环。
例题
不需要参考代码了吧
环计数
通常而言,环计数是针对简单无向图而言,没有重边和自环
对于简单有向图,可以依然按无向图方法操作,再判断所需的边是否在原图中存在
三元环计数
算法
解法一:根号分治,复杂度 M M M\sqrt{M} MM。根据根号分治的规则,将无向图按照以下方法变为有向图:
- 边由度数大的点指向度数小的点
- 若度数相等,边由编号小的点指向编号大的点
这之后,所得的有向图是无环的,三元环
(
u
,
v
,
w
)
(u,v,w)
(u,v,w) 在新图中的边一定是:
u
→
v
,
v
→
w
,
u
→
w
u \rightarrow v,v \rightarrow w,u \rightarrow w
u→v,v→w,u→w。
我们可以标记
u
u
u 的所有出点,再遍历它们,如果出点
v
v
v 能到达标记点
w
w
w,则找到一个环。
解法二:bitset优化枚举, 复杂度 O ( N M w ) O(\cfrac{NM}{w}) O(wNM),稠密图中优于根号分治。对点 v v v,设其出点集合为 o u t [ v ] out[v] out[v],入点集合为 i n [ v ] in[v] in[v],在无向图情况下 o u t [ v ] = = i n [ v ] out[v]==in[v] out[v]==in[v]。枚举所有边 v → u v \rightarrow u v→u,则 i n [ v ] ∩ o u t [ u ] in[v] \cap out[u] in[v]∩out[u] 就是可能的第三个环点构成的集合,暴力求交集是 O ( N ) O(N) O(N) 的,使用bitset优化。注意,所得的最终结果是答案的3倍,因为一个环被每条边计算了三次。
例题
参考代码:
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
int deg[MAXN], vis[MAXN];
vector<int> G[MAXN];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
vector<pair<int, int>> e(m);
for (auto &[v, u] : e)
cin >> v >> u, ++deg[v], ++deg[u];
for (auto [v, u] : e)
{
if (deg[v] < deg[u] || deg[v] == deg[u] && v > u)
swap(v, u);
G[v].push_back(u);
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
for (auto u : G[i])
vis[u] = i;
for (auto u : G[i])
for (auto w : G[u])
ans += (vis[w] == i);
}
cout << ans << endl;
return 0;
}
参考代码:
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1505;
bitset<MAXN> out[MAXN], in[MAXN];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
freopen("triatrip.in", "r", stdin);
freopen("triatrip.out", "w", stdout);
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
string s;
cin >> s;
for (int j = 1; j <= n; j++)
if (s[j - 1] == '+')
out[i][j] = 1, in[j][i] = 1;
}
int64_t ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (out[i][j])
ans += (out[j] & in[i]).count();
cout << ans / 3 << endl;
return 0;
}
题意:求出共用一条边的三元环个数。
思路:每当找到一个环时,给环的三条边计数加1。最后遍历所有边,如果该边计数大于1,则对答案贡献为 C c n t 2 C_{cnt}^2 Ccnt2。
参考代码:
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
int deg[MAXN], vis[MAXN];
vector<int> G[MAXN];
inline pair<int, int> mk(int x, int y) { return make_pair(min(x, y), max(x, y)); }
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
while (cin >> n >> m)
{
map<pair<int, int>, int> mp;
vector<pair<int, int>> e(m);
for (int i = 0; i < m; i++)
{
int &v = e[i].first, &u = e[i].second;
cin >> v >> u;
++deg[v], ++deg[u];
}
for (int i = 0; i < m; i++)
{
int v = e[i].first, u = e[i].second;
if (deg[v] < deg[u] || deg[v] == deg[u] && v > u)
swap(v, u);
G[v].push_back(u);
}
for (int i = 1; i <= n; i++)
{
for (auto u : G[i])
vis[u] = i;
for (auto u : G[i])
for (auto w : G[u])
if (vis[w] == i)
++mp[mk(i, u)], ++mp[mk(u, w)], ++mp[mk(i, w)];
}
int64_t ans = 0;
for (const auto &p : mp)
if (p.second > 1)
ans += 1ll * p.second * (p.second - 1) / 2;
cout << ans << "\n";
for (int i = 1; i <= n; i++)
G[i].clear(), vis[i] = 0, deg[i] = 0;
}
return 0;
}
四元环计数
算法
沿用根号分治建图,并根据建图规则给每个点分配唯一
r
a
n
k
rank
rank 排名(度数越大、编号越小越靠前)。
原四元环
(
u
,
v
,
w
,
x
)
(u,v,w,x)
(u,v,w,x) 在有向图中必然存在两条边
u
→
v
,
u
→
x
u \rightarrow v, u \rightarrow x
u→v,u→x。一个四元环唯一的表示为:
u
−
v
→
w
,
u
−
x
→
w
u-v\rightarrow w,u-x\rightarrow w
u−v→w,u−x→w 且
r
a
n
k
[
u
]
<
r
a
n
k
[
v
]
<
r
a
n
k
[
w
]
rank[u]<rank[v]<rank[w]
rank[u]<rank[v]<rank[w],这保证一个四元环不被重复计算。
枚举点
u
u
u,枚举其在原图的出点
v
v
v,枚举
v
v
v 在有向图的所有出点
w
w
w,这其中隐含
r
a
n
k
[
v
]
<
r
a
n
k
[
w
]
rank[v]<rank[w]
rank[v]<rank[w],因此现在只需要满足
r
a
n
k
[
u
]
<
r
a
n
k
[
w
]
rank[u]<rank[w]
rank[u]<rank[w],即为一条有效路径,我们令
c
n
t
[
w
]
cnt[w]
cnt[w] 代表
w
w
w 上的有效路径数,只要有两条以上,就能成环,因此答案加上
c
n
t
[
w
]
cnt[w]
cnt[w],再令
c
n
t
[
w
]
+
+
cnt[w]++
cnt[w]++。每当更换
u
u
u 时,所有点计数要清空。
例题
Gym - 104197F F*** 3-Colorable Graphs
题意:给出一个连通二分图,两侧各有 n n n 个点,共 m m m 条边。求最少添加多少条边,能使得整张图无法只用3种颜色染色。
思路:如果一张图无法只用3种颜色染色,一定存在如图所示的结构。由于是连通二分图,显然最多只需要添加3条边就能产生所示结构,故答案不超过3。注意到,若图中存在四元环,这是最佳情况,此时只需要添加同侧边,答案为2。于是本题转换为判断图中是否存在四元环,可以直接套用四元环计数。当然因为是判断,也可直接枚举
u
u
u,枚举
v
v
v,标记
w
w
w 颜色为
u
u
u,每次判断是否能找到同色的
w
w
w 即可。
参考代码:
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 2e4 + 5;
int deg[MAXN], cnt[MAXN];
vector<int> G[MAXN], T[MAXN];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
while (m--)
{
int v, u;
cin >> v >> u;
G[v].push_back(u), G[u].push_back(v);
++deg[v], ++deg[u];
}
for (int v = 1; v <= n + n; v++)
for (auto u : G[v])
if (deg[v] > deg[u] || (deg[v] == deg[u] && v < u))
T[v].push_back(u);
int ans = 0;
for (int v = 1; v <= n + n; v++)
{
for (auto u : G[v])
for (auto w : T[u])
if (deg[v] > deg[w] || (deg[v] == deg[w] && v < w))
ans += cnt[w]++;
for (auto u : G[v])
for (auto w : T[u])
if (deg[v] > deg[w] || (deg[v] == deg[w] && v < w))
cnt[w] = 0;
}
cout << (ans ? "2\n" : "3\n");
return 0;
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)