\(\text{update 2024/6/9: }\) 文中提到了速度较快的 \(\text{DFS-SPFA}\),虽然速度较好,但是容易被卡,例如模板!
大家好,我是Weekoder!
今天要讲的是负环的一些习题与负环在题目中的应用。
一共有 \(3\) 个例题,分别为:
\(\color{#52C41A}\texttt{P2136}\),\(\color{#9D3DCF}\texttt{P2868}\),\(\color{#9D3DCF}\texttt{P3199}\)。(点击有 \(\text{Link}\))
\(\color{#52C41A}\texttt{P2136}\) 拉近距离
题面
在小明和小红的生活中,有 \(N\) 个关键的节点。有 \(M\) 个事件,记为一个三元组 \((S_i,T_i,W_i)\),表示从节点 \(S_i\) 有一个事件可以转移到 \(T_i\),事件的效果就是使他们之间的距离减少 \(W_i\)。
这些节点构成了一个网络,其中节点 \(1\) 和 \(N\) 是特殊的,节点 \(1\) 代表小明,节点 \(N\) 代表小红,其他代表进展的阶段。所有事件可以自由选择是否进行,但每次只能进行当前节点邻接的。请你帮他们写一个程序,计算出他们之间可能的最短距离。如果这个距离可以无限缩小,输出Forever love
。
分析
可以把题意理解为一个人在一条路径上不断往另一个人的方向走。
注意到题目中说距离会减少 \(W_i\),那么边权即为负。正常来说,跑一遍 SPFA 就可以了,答案是 \(dis_n\)。但是注意到不仅是小明可以去找小红,小红也可以去找小明,于是做两遍 SPFA,分别为 \(\text{spfa(1)}\) 和 \(\text{spfa(n)}\),最后取 \(\min\)。\(\texttt{Forever love}\) 就是判断有没有负环,除此之外和 SPFA 判负环没有区别。由于没有负环要输出最短路,所以考虑使用 BFS-SPFA。
\(\text{Code:}\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5;
struct Edge {
int to, w;
};
int n, m, dis[N], dp[N];
bool vis[N];
vector<Edge> nbr[N];
bool spfa(int s) {
queue<int> q;
memset(vis, 0, sizeof vis);
memset(dis, 0x3f, sizeof dis);
memset(dp, 0, sizeof dp);
dis[s] = 0;
vis[s] = 1;
q.push(s);
while (!q.empty()) {
int cur = q.front(); q.pop();
vis[cur] = 0;
for (auto nxt : nbr[cur]) {
int to = nxt.to, w = nxt.w;
if (dis[to] > dis[cur] + w) {
dis[to] = dis[cur] + w;
dp[to] = dp[cur] + 1;
if (dp[to] >= n) return 1;
if (!vis[to]) {
vis[to] = 1;
q.push(to);
}
}
}
}
return 0;
}
int main() {
cin >> n >> m;
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
nbr[u].emplace_back((Edge){v, -w});
}
if (spfa(1)) {
cout << "Forever love";
return 0;
}
int tmp = dis[n];
if (spfa(n)) {
cout << "Forever love";
return 0;
}
cout << min(dis[1], tmp);
return 0;
}
\(\color{#9D3DCF}\texttt{P2868}\) Sightseeing Cows G
题面
给你一张 \(n\) 点 \(m\) 边的有向图,第 \(i\) 个点点权为 \(F_i\),第 \(i\) 条边边权为 \(T_i\)。
找一个环,设环上的点组成的集合为 \(S\),环的边组成的集合为 \(E\),最大化 \(\dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}\)。
分析
考虑二分答案。
设二分答案 \(mid\),因为要找最大值,所以我们设还有一个更大值。这个值就是 \(\dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}\),所以假设我们有:\(\dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}>mid\)。如果成立,就猜大些,否则猜小些。但是这个不好判断,所以我们来推一下式子(在一个环中,点数 \(=\) 边数):
\[\begin{aligned} {} & \dfrac{\sum_{u\in S}F_u}{\sum_{e\in E}T_e}>mid \\ = & \sum_{u\in S}F_u>mid\cdot \sum_{e\in E}T_e \\ = & mid\cdot \sum_{e\in E}T_e-\sum_{u\in S}F_u<0 \\ = & \sum(mid\cdot T_e-F_u)<0 \end{aligned} \]推到这里,我们发现了一个有趣的结论:若想继续猜大些,则需要满足对于环上的边权之和,有 \(mid\cdot T_e-F_u<0\)。我们把 \(mid\cdot T_e-F_u\) 看作边权,那这不就变成了一个找负环的问题吗?没错,在简单的计算后,我们得到了一个结论:找负环。
在这里,就仅仅需要判断是否有负环并二分答案,推荐使用 DFS-SPFA,用时仅有 \(46\text{ms}\)。对于 BFS-SPFA,也能过,不过时间为 \(83\text{ms}\)。
\(\text{Code:}\)
\(\text{DFS-SPFA}\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5;
const double eps = 1e-6;
struct Edge {
int to;
double w;
};
int n, m;
double dis[N], f[N];
bool vis[N];
vector<Edge> nbr[N];
bool spfa(int cur, double X) {
vis[cur] = 1;
for (auto nxt : nbr[cur]) {
int to = nxt.to;
double w = nxt.w * X - f[cur];
if (dis[to] > dis[cur] + w) {
dis[to] = dis[cur] + w;
if (vis[to] || spfa(to, X)) return 1;
}
}
vis[cur] = 0;
return 0;
}
bool check(double X) {
memset(vis, 0, sizeof vis);
memset(dis, 0, sizeof dis);
for (int i = 1; i <= n; i++)
if (spfa(i, X))
return 1;
return 0;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> f[i];
double l = 1005, r = -1;
for (int i = 1, u, v; i <= m; i++) {
double w;
cin >> u >> v >> w;
nbr[u].emplace_back((Edge){v, w});
l = min(l, w);
r = max(r, w);
}
r++, l--;
while (r - l > eps) {
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%.2lf", l);
return 0;
}
\(\text{BFS-SPFA}\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5;
const double eps = 1e-6;
struct Edge {
int to;
double w;
};
int T, n, m, dp[N];
double dis[N], f[N];
bool vis[N];
vector<Edge> nbr[N];
bool spfa(double X) {
queue<int> q;
memset(vis, 0, sizeof vis);
memset(dis, 0, sizeof dis);
memset(dp, 0, sizeof dp);
for (int i = 1; i <= n; i++)
vis[i] = 1, q.push(i);
while (!q.empty()) {
int cur = q.front(); q.pop();
vis[cur] = 0;
for (auto nxt : nbr[cur]) {
int to = nxt.to;
double w = X * nxt.w - f[cur];
if (dis[to] > dis[cur] + w) {
dis[to] = dis[cur] + w;
dp[to] = dp[cur] + 1;
if (dp[to] >= n) return 1;
if (!vis[to]) {
vis[to] = 1;
q.push(to);
}
}
}
}
return 0;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> f[i];
double l = 1005, r = -1;
for (int i = 1, u, v; i <= m; i++) {
double w;
cin >> u >> v >> w;
nbr[u].emplace_back((Edge){v, w});
l = min(l, w);
r = max(r, w);
}
double tmp = r + 1;
r++, l--;
while (r - l > eps) {
double mid = (l + r) / 2;
if (spfa(mid)) l = mid;
else r = mid;
}
printf("%.2lf", l);
return 0;
}
\(\color{#9D3DCF}\texttt{P3199}\) 最小圈
题面
考虑带权有向图 \(G=(V,E)\) 以及 \(w:E\rightarrow \mathbb{R}\),每条边 \(e=(i,j)\)(\(i\neq j\),\(i, j\in V\))的权值定义为 \(w_{i,j}\)。设 \(n=|V|\)。
\(c=(c_1,c_2,\cdots,c_k)\)(\(c_i\in V\))是 \(G\) 中的一个圈当且仅当 \((c_i,c_{i+1})\)(\(1\le i<k\))和 \((c_k,c_1)\) 都在 \(E\) 中。称 \(k\) 为圈 \(c\) 的长度,同时记 \(c_{k+1}=c_1\),并定义圈 \(c=(c_1,c_2,\cdots,c_k)\) 的平均值为
\[\mu(c)= \frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}} \]即 \(c\) 上所有边的权值的平均值。设 \(\mu'(G)=\min_c\mu(c)\) 为 \(G\) 中所有圈 \(c\) 的平均值的最小值。
给定图 \(G=(V,E)\) 以及 \(w:E\rightarrow \mathbb{R}\),求出 \(G\) 中所有圈 \(c\) 的平均值的最小值 \(\mu'(G)\)。
题面概括
给定一个图,找一个环,最小化
\[\frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}} \]分析
考虑二分答案。
设二分答案 \(mid\),这次要找最小值,于是设 \(\frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}}<mid\)。继续推公式:
\[\begin{aligned} & \frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}}<mid \\ = & mid\cdot k>\sum\limits_{i=1}^{k}w_{c_i,c_{i+1}} \\ = & \sum\limits_{i=1}^{k}(mid-w_{c_i,c_{i+1}})>0 \\ = & \sum\limits_{i=1}^{k}(w_{c_i,c_{i+1}}-mid)<0 \\ \end{aligned} \]于是我们又得到了:将边权看作原边权 \(-mid\),判负环。
这个题需要用 DFS-SPFA,否则会 \(\color{#052242}\texttt{TLE}\) 挂 \(50\text{pts}\)。
\(\text{Code:}\)
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 5;
const double eps = 1e-9;
struct Edge {
int to;
double w;
};
int n, m;
double dis[N];
bool vis[N];
vector<Edge> nbr[N];
bool spfa(int cur, double X) {
vis[cur] = 1;
for (auto nxt : nbr[cur]) {
int to = nxt.to;
double w = nxt.w - X;
if (dis[to] > dis[cur] + w) {
dis[to] = dis[cur] + w;
if (vis[to] || spfa(to, X)) return 1;
}
}
vis[cur] = 0;
return 0;
}
bool check(double X) {
memset(dis, 0, sizeof dis);
memset(vis, 0, sizeof vis);
for (int i = 1; i <= n; i++)
if (spfa(i, X)) return 1;
return 0;
}
int main() {
cin >> n >> m;
double l = 1e8, r = -1e8;
for (int i = 1, u, v; i <= m; i++) {
double w;
cin >> u >> v >> w;
nbr[u].emplace_back((Edge){v, w});
l = min(l, w);
r = max(r, w);
}
r++, l--;
while (r - l > eps) {
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
printf("%.8lf", r);
return 0;
}
小结
以上就是关于负环的三个习题的内容。负环是一个判定型的算法,适合搭配二分答案等算法,发挥更大的用处。今天的习题都有一些难度,需要巩固练习。