负环
有向图或者无向图中, 存在环路, 使得一圈的权值之和 < 0, 称为负环
负环为什么要单独处理, 比如最短路中, 如果一个点能走到负环上, 那么就能在这个点上转无穷多次, 每转1次, 距离就会小一点, 因此图形中, 某些点的最短路就会变成
−
∞
-\infty
−∞, 会影响最短路算法的正确性
求负环的常用方法, 基于SPFA
(1) : 统计每个点入队的次数, 如果某个点入队n次(起点不算, 其他点入队n次, 增加n条边), 说明存在负环
实际上就是bellman-ford算法:
bellman-ford算法说迭代n次, 还有点被更新的话, 说明存在负环
bellman-ford算法, 每更新1次, 最短路径中边的数量就会+1, 更新n次, 最短路径中边数 >= n. 如果1条路径中边的数量 >= n, 一共n条边, n + 1个点, 总共只有n个点, 那么必定有两个点重合, 必然存在一个环, 只有在边的距离不断变小才会更新, 更新这两个点形成的环总和一定 < 0.
转移到spfa里, 某个点入队1次, 说明被更新了1次, 入队>=n, 说明被更新>=n次
(2) : 统计当前每个点最短路中包含的边数, 如果某个点的最短路所包含的边数 >= n, 则也说明存在负环 yxc:一般来说喜欢这种方式
同样n条边, n + 1个点, 总共n个点, 必然存在环, 并且路径可以减小, 负环
算法竞赛进阶指南: Bellman-ford算法判断负环时间复杂度为O(nm), 因为每次转1圈 迭代m条边, 更新1条边,需要迭代n次, 更新O(nm)次, 才能找到负环.SPFS稍微块一些, 但最坏O(nm), 因此一般用SPFA
spfa O(m)
实际上 时间复杂度O(nm)比较慢, 虽然理论来说来只有当边数 >= n的时候, 才存在负环, 很多情况下这样会超时, 但spfa陷入漩涡🌀,不停地在更新, 我们就可以认为存在负环
(yxc:比较trick的做法,
玄学做法):比方说所有点入队次数超过某个定值的时候, 就可以说存在负环.
当所有点的入队次数超过2n, 我们就认为图中有很大可能, 是存在负环的.
(yxc: 注意, 这个不一定正确, 肯定有可以构造出来不正确的情况, 但是呢, 这个实际上能取得不错的运行效果, 但我们做一个题目, 一直不停的超时, 不妨试一下这个做法.)(Trick!)
AcWing 904. 虫洞
分析
他希望能够看到出发之前的自己。
请你判断一下约翰能否做到这一点。
就是裸的SPFA求负环问题
数据M = 2500* 2 = 无向边5000 + 虫洞200条边 = 5200, N = 500, 5200 * 500 ~= 10^7
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 5210;
int h[N], e[M], ne[M], w[M], idx;
int n, m1, m2;
bool st[N];
int q[N];
int cnt[N];
int dist[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool spfa(){
memset(dist, 0, sizeof dist);
memset(st, 0, sizeof st);
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;
scanf("%d%d%d", &n, &m1, &m2);
while (m1 --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
while (m2 --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, -c);
}
if (spfa()) puts("YES");
else puts("NO");
}
return 0;
}
AcWing 361. 观光奶牛
分析
f
i
f_i
fi:点的权值
t
i
t_i
ti:边的权值
求环上使得
∑
f
i
∑
t
i
\frac{\sum f_i}{\sum t_i}
∑ti∑fi最大
在图论问题, 求形如
∑
f
i
∑
t
i
\frac{\sum f_i}{\sum t_i}
∑ti∑fi的问题, 叫做0-1分数规划, 所有形如0-1分数规划的问题, 一般用二分来做
最小值0, 最大值(分子都取1000, 分母都取1), 因此分数值在(0, 1000]之间.
取一个mid, 然后取判断一下图中是否存在一个环, 使得
∑
f
i
∑
t
i
>
m
i
d
\frac{\sum f_i}{\sum t_i} > mid
∑ti∑fi>mid, 假设可以写一个函数判断
∑
f
i
∑
t
i
>
m
i
d
\frac{\sum f_i}{\sum t_i} > mid
∑ti∑fi>mid, 那么我们要找的值在[mid, R]之间, L = mid; 如果判断出来没有任何一个环
∑
f
i
∑
t
i
>
m
i
d
\frac{\sum f_i}{\sum t_i} > mid
∑ti∑fi>mid, 说明答案在[0, mid - 1], R = mid - 1;
因此可以根据判断的结果, 每次可以将答案的区间每次缩小一般, 最终可以找到答案
如果判断图中是否存在一个环使得
∑
f
i
∑
t
i
>
m
i
d
\frac{\sum f_i}{\sum t_i} > mid
∑ti∑fi>mid?
由于所有分母是整数,
∑
f
i
>
m
i
d
∗
∑
t
i
∑
f
i
−
m
i
d
∗
∑
t
i
>
0
\sum f_i > mid * \sum t_i\\ \sum f_i - mid * \sum t_i > 0
∑fi>mid∗∑ti∑fi−mid∗∑ti>0
这里既有点权, 又有边权, 不是很方便, 如果求的是最短距离的话, 可以把点权放到边上去,如果是有向边的话, 点权可以放到出边/入边上, 都是等价的.
为什么点权可以放到边上去呢?
比如说把点权f1放到边上, 边权变成了f1 + t1. 现在要证明的是把点权放到出边上的是等价的
那么对于任何一个环来说, 点权 + 边权 = ∑ f i + ∑ t i \sum f_i + \sum t_i ∑fi+∑ti, 同样道理, 把点权放到出边上, 总共的权值之和 = ∑ f i + ∑ t i \sum f_i + \sum t_i ∑fi+∑ti, 结果是一样的
∑
(
f
i
−
m
i
d
∗
t
i
)
>
0
\sum (f_i - mid * t_i) > 0
∑(fi−mid∗ti)>0
即: 将原本
i
→
j
i \to j
i→j上的权重由
t
k
t_k
tk 看成是
f
i
−
m
i
d
∗
t
k
f_i - mid * t_k
fi−mid∗tk
那么问题等价于 图形中是否存在环, 使得所有边权之和 > 0, 也就等价于 图中是否存在正环
正环的, 把所有边权值取一个负号, 实际中, 不用真的取负号, 而将求最短路改成求最长路, 然后统计下每个路径包含的边数即可
最终: 用二分->判断不等式成不成立, 成立的话, 答案落在右半区间; 否则, 答案在左半区间.
数据范围: 点数*边数 = 5000, 000不是很大
code
注意dist 换成double, double l, r
;
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, M = 5010;
int h[N], e[M], wt[M], ne[M], idx;
int n, m;
int wf[N];
bool st[N];
double dist[N];
int q[N], cnt[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){
memset(st, 0, sizeof st);
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] + 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;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ ){
scanf("%d", &wf[i]);
}
while (m --){
int a, b, c;
scanf("%d%d%d", &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;
}
1165. 单词环
分析
需要考虑3个问题
- 建图
- ∑ w i ∑ s i \frac{\sum w_i}{\sum s_i} ∑si∑wi 最大
- 优化
建图: 可以把每个字符串看成1个点, 1后面可以接2, 就建一条
1
→
2
1 \to 2
1→2, 2后面接3, 建立
2
→
3
2 \to 3
2→3, 然后
3
→
1
3 \to 1
3→1, 这样建图是可以, 但是数据会过大, 题目
1
0
5
10^5
105个点, 假设字符串都是字母aaaaaaa…aa, 那么每个字符串都可以连接, 需要建
(
1
0
5
)
2
=
1
0
10
(10^5)^2 = 10^{10}
(105)2=1010, 爆空间/时间
可以看成一个pair的形式, 将第1个字符串ababc
看成ab
⟶
5
\stackrel{5}{\longrightarrow}
⟶5bc
对于第2字符串bckjaca
来说, 相当于bc
⟶
7
\stackrel{7}{\longrightarrow}
⟶7ca
第3个字符串caahoynaab
, 相当于ca
⟶
10
\stackrel{10}{\longrightarrow}
⟶10ab
一共
1
0
5
10^5
105条边, 因为题目要求接龙2个字母才可以接, 26* 26 = 676个点
这样的建图方式, 这样的图中的环 是否和题目中的环是等价的呢?
这样的图中, 将任何一个点换成原来的字符串后, 等价于图中的定义的字符串的环.
任意的字符串的环, 将每个字符串看成一条边, 字符串的开头和结尾看成两个点, 就可以对应图里的一个环
6.7
∗
1
0
7
6.7 * 10^7
6.7∗107 复杂度
剩下的问题变成
w
i
w_i
wi: 边权, 点权: 单词权重为1
求
∑
w
i
∑
1
\frac{\sum w_i}{\sum 1}
∑1∑wi最大
先看下答案区间, 分子, 分母 >0, 因此最小值0, 最大值的话, 每条边权最大1000, 单词权重1, 因此最大1000
无解的话, 也需要判断, 有解的话一定在(0, 1000]内, 如果在范围内找不到, 表示无解.
每次二分一个中点M , 判断图中是否存在环 使得左边等式 > M
∑
w
i
∑
1
>
M
⇔
∑
w
i
>
M
∗
∑
1
⇔
∑
w
i
−
M
∑
1
>
0
⇔
∑
(
w
i
−
M
∗
1
)
>
0
\frac{\sum w_i}{\sum 1} > M\\ \Leftrightarrow \sum w_i > M * \sum 1\\ \Leftrightarrow \sum w_i - M \sum 1 > 0 \\ \Leftrightarrow \sum (w_i - M * 1) > 0
∑1∑wi>M⇔∑wi>M∗∑1⇔∑wi−M∑1>0⇔∑(wi−M∗1)>0
所以, 这里没有点权, 1也是在单词的边权上, 我们可以重新定义我们的边权为
w
i
−
M
∗
1
⇔
w_i - M * 1 \Leftrightarrow
wi−M∗1⇔图中是否存在正环
可以发现
0
<
M
≤
1000
0 < M \leq 1000
0<M≤1000, 但M = 0的时候如果都没有正环, 那么M > 0的时候, 边权会减小, 那就更没有可能有正环了, 因此判断的时候, 直接将M = 0 代入, 来判断
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 700, M = 1e5 + 10;
int n;
int h[N], e[M], w[M], ne[M], idx;
int q[N], cnt[N];
double dist[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(st, 0, sizeof st);
memset(cnt, 0 ,sizeof cnt);
int hh = 0, tt = 0;
for (int i = 0; i < 676; 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 (++ count > 10000) return true; // 10000 是经验值
if (cnt[j] >= N) return true;
if (!st[j]){
st[j] = true;
q[tt ++ ] = j;
if (tt == N) tt = 0;
}
}
}
}
return false;
}
int main(){
char str[1010];
while (scanf("%d", &n), n){
idx = 0;
memset(h, -1, sizeof h);
for (int i = 0; i < n; i ++ ){
scanf("%s", str);
int len = strlen(str);
if (len >= 2){
int left = (str[0] - 'a') * 26 + (str[1] - 'a');
int right = (str[len - 2] - 'a') * 26 + (str[len - 1] - 'a');
add(left, right, len);
}
}
if (!check(0)) puts("No solution");
else {
double l = 0, r = 1010;
while (r - l > 1e-4){
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%lf\n", r);
}
}
return 0;
}