负环
1. 负环原理
原理
- 求解图中的负环,是基于
spfa算法
(n表示图中顶点数):
(1)统计每个点入队的次数,如果某个点入队n次,则说明存在负环;
(2)统计当前每个点的最短路中所包含的边数,如果某点的最短路径所包含的边数大于等于n,则说明存在负环。
- 对于(1)来说,因为
spfa算法
是基于bellmanford算法
改进来的,对于bellmanford算法
来说,如果迭代了n此之后还有节点被更新的话,说明存在负环;对应到spfa算法
来说,某个点每入队一次,则说明其被更新了一次,因此如果某个点入队大于等于n次,则说明其被更新了大于等于n次,说明存在负环。 - 对于(2)来说,如果某点的最短路径所包含的边数大于等于n,则说明这条路径中至少存在n+1个点,因为只有n个点,根据抽屉原理,必定有两个节点相同,因为只有距离变小时才会更新,因此存在负环。
- 上面的两种方法最常用的是(2)。原因如下:
- spfa算法:
- spfa求负环:
spfa算法
求负环,有时候可能时间复杂度接近 O ( n × m ) O(n\times m) O(n×m),会超时,对于方法(2)这里有一个技巧:当所有的点入队的次数超过2n(或者3n)时,我们就认为图中有很大可能是存在负环的。
代码模板
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N]; // cnt[i]表示到虚拟源点的距离-1
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool spfa() {
queue<int> q;
for (int i = 1; i <= n; i++) { // 相当于有一个虚拟源点,所有的点都要加入
q.push(i);
st[i] = true;
}
while (q.size()) {
int t = q.front(); q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
2. AcWing上的负环题目
AcWing 904. 虫洞
问题描述
-
问题链接:AcWing 904. 虫洞
分析
- 本题有多组数据。
- 农田代表图中的点,路径代表双向边(其上的权值为正),虫洞代表单向边(其上的权值为负)。因此本问题就可以转换成:该图中是否存在负环?
- 直接使用
spfa算法
求解即可。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 5210;
int n, m, k; // n:顶点数; m:双向边数; k:单向边数
int h[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
int q[N]; // 循环队列
bool st[N]; // 代表点是否在队列中
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool spfa() {
// dist初始值无所谓
// st最终在队列为空时也会全部变为false
memset(cnt, 0, sizeof cnt);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++) {
q[tt++] = i;
st[i] = true;
}
while (hh != tt) {
int t = q[hh++];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j]) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main() {
int T;
cin >> T;
while (T--) {
memset(h, -1, sizeof h);
idx = 0;
cin >> n >> m >> k;
while (m--) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
while (k--) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, -c);
}
if (spfa()) puts("YES");
else puts("NO");
}
return 0;
}
AcWing 361. 观光奶牛
问题描述
-
问题链接:AcWing 361. 观光奶牛
分析
- 分析题目可知,此题让我们求解一个环,使得
∑ f i ∑ t i \frac{\sum f_i}{\sum t_i} ∑ti∑fi
取得最大值。
- 所有形如这样的问题,有一个统一的名称:01分数规划。这样的问题一般都可以采用二分来解决。
- 针对本题,因为 1 ≤ f [ i ] , t [ i ] ≤ 1000 1\le f[i],t[i] \le 1000 1≤f[i],t[i]≤1000,所以答案所在的范围是:(0, 1000],可以对该区间进行二分。
- 对于区间[l, r]以及mid=(l + r) / 2,如果有
∑ f i ∑ t i > m i d f i 是 环 上 的 点 的 权 值 , t i 是 环 上 边 的 权 值 \frac{\sum f_i}{\sum t_i} > mid \quad f_i是环上的点的权值,t_i是环上边的权值 ∑ti∑fi>midfi是环上的点的权值,ti是环上边的权值
则说明答案在[mid, r]之间,否则答案在[l, mid]之间,接着二分即可。
- 对上面的不等式进行变形:
∑ f i ∑ t i > m i d ∑ f i − m i d × ∑ t i > 0 ∑ ( f i − m i d × t i ) > 0 \frac{\sum f_i}{\sum t_i} > mid \\ \sum f_i - mid \times \sum t_i > 0 \\ \sum (f_i-mid \times t_i) > 0 ∑ti∑fi>mid∑fi−mid×∑ti>0∑(fi−mid×ti)>0
- 根据上面的变形,我们可以将点上的权值变到出边上去,即每条边的权值变为 f i − m i d × t i f_i-mid \times t_i fi−mid×ti,这样方便我们处理。
- 在变换边权之后,最终问题就转化成了:图中是否存在正环。
- 那么如何求解正环呢?可以将所有的边权取个反,问题就变成了是否存在负环;但是实际上我们不需要这样做,我们只需要求最长路径即可。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, M = 5010;
int n, m;
int wf[N]; // 每个点的权值
int h[N], e[M], wt[M], ne[M], idx;
double dist[N];
int cnt[N]; // cnt[i]表示到达i的最长路径经过的边数
int q[N]; // 循环队列
bool st[N]; // 代表点是否在队列中
void add(int a, int b, int c) {
e[idx] = b, wt[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(double mid) {
// 该函数会被调用多次,因此需要初始化cnt
memset(cnt, 0, sizeof cnt);
// dist不需要初始化,因为存在正环的话,到达该点的距离会被更新为+∞
// st不需要初始化,因为最终队列为空,st的值会全部变为false
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++) {
q[tt++] = i;
st[i] = true;
}
while (hh != tt) {
int t = q[hh++];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + wf[t] - mid * wt[i]) { // 求负环这里是>
dist[j] = dist[t] + wf[t] - mid * wt[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j]) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> wf[i];
memset(h, -1, sizeof h);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
double l = 0, r = 1010;
while (r - l > 1e-4) {
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%.2lf\n", l);
return 0;
}
AcWing 1165. 单词环
问题描述
-
问题链接:AcWing 1165. 单词环
分析
-
(1)首先我们需要考虑如何建图:一种直观的建图方式是顶点是各个字符串,如果字符串s2能接在s1的后面,那么连一条从s1指向s2的边。这样的话图中顶点的个数最多有 1 0 5 10^5 105个,可能是完全图,因此边的数量在 1 0 10 10^{10} 1010量级,会超时,因此这样的建图方式不可取。
-
我们换一种建图的方式:顶点表示一个字符串的前两个字母或者后两个字母,边表示字符串的长度。相当于将上面一种建图方式中的顶点变成了边,边变成了顶点。这样的话,图中顶点的个数为676( 26 × 26 26\times 26 26×26),边数最多 1 0 5 10^5 105个。
-
(2)另外我们还需要考虑一个问题,如何使得
∑ w i ∑ s i w i 表 示 环 上 字 符 串 长 度 , s i 恒 为 1 \frac{\sum w_i}{\sum s_i} \quad w_i表示环上字符串长度,s_i恒为1 ∑si∑wiwi表示环上字符串长度,si恒为1
最大。我们可以使用和上一题一样的思路:01分数规划,使用二分解决。
- 针对本题,因为 1 ≤ w [ i ] ≤ 1000 1\le w[i] \le 1000 1≤w[i]≤1000,所以答案所在的范围是:(0, 1000],可以对该区间进行二分。
- 对于区间[l, r]以及mid=(l + r) / 2,如果有
∑ w i ∑ 1 > m i d \frac{\sum w_i}{\sum 1} > mid ∑1∑wi>mid
则说明答案在[mid, r]之间,否则答案在[l, mid]之间,接着二分即可。
- 对上面的不等式进行变形:
∑ w i ∑ 1 > m i d ∑ w i − m i d × ∑ 1 > 0 ∑ ( w i − m i d × 1 ) > 0 \frac{\sum w_i}{\sum 1} > mid \\ \sum w_i - mid \times \sum 1 > 0 \\ \sum (w_i-mid \times 1) > 0 ∑1∑wi>mid∑wi−mid×∑1>0∑(wi−mid×1)>0
- 根据上面的变形,每条边的权值变为 w i − m i d w_i-mid wi−mid,在变换边权之后,最终问题就转化成了:图中是否存在正环。
- 那么如何求解正环呢?可以将所有的边权取个反,问题就变成了是否存在负环;但是实际上我们不需要这样做,我们只需要求最长路径即可。
- (3)另外题目可能没有解,如何判断这种情况呢?对于改变后的边权 w i − m i d w_i-mid wi−mid,这个值越大,越有可能有正环,如果mid取0时都没有正环,则说明没有解。特判一下即可,我们可以使用AcWing 361. 观光奶牛的做法解决这个问题。
- (4)如果单纯使用AcWing 361. 观光奶牛方法解决这个问题,会超时(TLE),因此需要使用上面原理中提到的技巧:当所有的点入队的次数超过2n(或者3n)时,我们就认为图中有很大可能是存在负环的。但是注意:本题的点数太少,不到700,但是边数非常多,最多有10000条,所以经验值不适用了,需要取一个更大的值。
- 另外对于求负环的问题,如果超时的话,我们还可以尝试将队列改为栈,有可能更快找到这个环,这样一般都是能解决TLE问题的。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 700, M = 100010;
int n; // 图中边数,有多少字符串就有多少条有向边
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int cnt[N]; // cnt[i]表示到达i的最长路径经过的边数
int q[N]; // 循环队列
bool st[N]; // 代表点是否在队列中
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(double mid) {
memset(cnt, 0, sizeof cnt);
int hh = 0, tt = 0;
for (int i = 0; i < 26 * 26; i++) {
q[tt++] = i;
st[i] = true;
}
int count = 0;
while (hh != tt) {
int t = q[hh++];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i] - mid) {
dist[j] = dist[t] + w[i] - mid;
cnt[j] = cnt[t] + 1;
if (cnt[j] >= N) return true; // n是边数,N才是点数
if (++count > N * 10) return true; // 经验上的trick
if (!st[j]) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main() {
char str[1010];
while (scanf("%d", &n), n) {
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < n; i++) {
scanf("%s", str);
int c = strlen(str);
if (c >= 2) {
int a = (str[0] - 'a') * 26 + str[1] - 'a';
int b = (str[c - 2] - 'a') * 26 + str[c - 1] - 'a';
add(a, b, c);
}
}
if (!check(0)) puts("No solution");
else {
double l = 0, r = 1000;
while (r - l > 1e-4) {
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%lf\n", r);
}
}
return 0;
}
#include <iostream>
#include <cstring>
using namespace std;
const int N = 700, M = 100010;
int n; // 图中边数,有多少字符串就有多少条有向边
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int cnt[N]; // cnt[i]表示到达i的最长路径经过的边数
int stk[N]; // 栈
bool st[N]; // 代表点是否在栈中
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(double mid) {
memset(cnt, 0, sizeof cnt);
int tt = 0;
for (int i = 0; i < 26 * 26; i++) {
stk[++tt] = i;
st[i] = true;
}
while (tt > 0) { // 此时tt指向栈顶元素
int t = stk[tt--];
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i] - mid) {
dist[j] = dist[t] + w[i] - mid;
cnt[j] = cnt[t] + 1;
if (cnt[j] >= N) return true; // n是边数,N才是点数
if (!st[j]) {
stk[++tt] = j;
st[j] = true;
}
}
}
}
return false;
}
int main() {
char str[1010];
while (scanf("%d", &n), n) {
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < n; i++) {
scanf("%s", str);
int c = strlen(str);
if (c >= 2) {
int a = (str[0] - 'a') * 26 + str[1] - 'a';
int b = (str[c - 2] - 'a') * 26 + str[c - 1] - 'a';
add(a, b, c);
}
}
if (!check(0)) puts("No solution");
else {
double l = 0, r = 1000;
while (r - l > 1e-4) {
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%lf\n", r);
}
}
return 0;
}