AcWing 算法提高课 SPFA求负环 专题
负环
- 统计每个点的入队次数,如果某个点入队 n 次,则有负环
- 统计当前每个点的最短路中包含的边数,如果某点的最短边所包含的边数 \(\geq n\),有负环 (点重合)
建一个虚拟源点,向每个点连一条权值为0的边,这样所有的点都能到达且可初始化为0
Trick: SPFA太多次被卡,即 当所有点入队次数超过2*n(注:这个值可能上下浮动,还是得靠经验所以很玄学)时,就认为有很大可能存在负环
虫洞
思路
时间变小了也就相当于存在负环,把值变小,所以转化为判断是否存在负环
要注意是循环队列!
细节:1. h和idx记得每一组都要初始化;2. M边数是5000+200
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 505, M = 5205; //注意边的数量是5000+200
int t, n, m1, m2;
int h[N], e[M], ne[M], w[M], idx;
int dis[N], cnt[N];
bool vis[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 = 0; i <= n; i ++) {
q.push (i), vis[i] = true;
dis[i] = 0, cnt[i] = 0;
}
while (!q.empty()) {
int t = q.front();
q.pop();
vis[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dis[j] > dis[t] + w[i]) {
dis[j] = dis[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n)
return true;
if (!vis[j]) //未加入过
q.push(j), vis[j] = true;
}
}
}
return false;
}
int main () {
cin >> t;
while (t --) {
memset (h, -1, sizeof h), idx = 0; //初始化要做全套啊
cin >> n >> m1 >> m2;
while (m1 --) {
int a, b, c;
cin >> a >> b >> c;
add (a, b, c), add (b, a, c);
}
while (m2 --) {
int a, b, c;
cin >> a >> b >> c;
add (a, b, -c);
}
if (spfa())
cout << "YES" << endl;
else
cout << "NO" << endl;
}
}
//时间变小了也就相当于存在负环,把值变小
//转化为判断是否存在负环
//要注意是循环队列!
观光奶牛
题目
给定一张 L 个点、P 条边的有向图,每个点都有一个权值 f[i],每条边都有一个权值 t[i]。
求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。
输出这个最大值。
注意:数据保证至少存在一个环。
输入格式
第一行包含两个整数 L 和 P。
接下来 L 行每行一个整数,表示 f[i]
再接下来 P 行,每行三个整数 a,b,t[i],表示点 a 和 b 之间存在一条边,边的权值为 t[i]。
输出格式
输出一个数表示结果,保留两位小数。
数据范围
\(2≤L≤1000, 2≤P≤5000, 1≤f[i],t[i]≤1000\)
输入样例:
5 7
30
10
10
5
10
1 2 3
2 3 2
3 4 5
3 5 2
4 5 5
5 1 3
5 2 2
输出样例:
6.00
时/空限制:1s / 64MB
思路
实数二分 + SPFA
01分数规划:求\(\frac{\sum f_i}{\sum t_i}\)最大 (点权\(f_i\),边权\(t_i\))
做法:二分
然后可以把边权和点权一起处理,即:\(i\) 到 \(j\) 权值为 \(t_k\),可以把边权变成 \(f_i-mid\,t_k\)
等价为找找图中是否存在正环
\(Tip:\) 保留多少位小数就把 \(eps\) 设置为多少位 + 2
- 补充一点:\(r\) 为什么要定为 \(1e6\),因为\(\frac{max(f_i)}{min(t_i)} * L = 1e6\)
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1005, M = 5005;
const double eps = 1e-4;
int n, m;
int h[N], e[M], ne[M], idx;
double f[N], w[M], dis[N];
int cnt[N];
bool vis[N];
void add (int a, int b, double c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool spfa (double x) {
queue <int> q;
memset (vis, true, sizeof vis);
memset (cnt, 0, sizeof cnt);
for (int i = 1; i <= n; i ++)
q.push (i);
while (!q.empty()) {
int t = q.front();
q.pop();
vis[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dis[j] < dis[t] + f[t] - x * w[i]) { //说明f[t] - x * w[i] > 0
dis[j] = dis[t] + f[t] - x * w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n)
return true;
if (!vis[j])
vis[j] = true, q.push (j);
}
}
}
return false;
}
int main () {
memset (h, -1, sizeof h);
cin >> n >> m;
for (int i = 1; i <= n; i ++)
cin >> f[i];
while (m --) {
int a, b;
double c;
cin >> a >> b >> c;
add (a, b, c);
}
double l = 0, r = 1e6; //实数二分
while (l + eps < r) {
double mid = (l + r) / 2;
if (spfa (mid))
l = mid;
else
r = mid;
}
cout << fixed << setprecision (2) << l << endl;
}
单词环
题目
我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。
如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。
我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
如下例:
ababc
bckjaca
caahoynaab
第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5+7+10=22(重复部分算两次),总共使用了 3 个串,所以平均长度是 223≈7.33。
输入格式
本题有多组数据。
每组数据的第一行,一个整数 n,表示字符串数量;
接下来 n 行,每行一个长度小于等于 1000 的字符串。
读入以 n=0 结束。
输出格式
若不存在环串,输出”No solution”,否则输出最长的环串的平均长度。
只要答案与标准答案的差不超过 0.01,就视为答案正确。
数据范围:\(1≤n≤105\)
输入样例:
3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0
输出样例:
21.66
思路
建图方式:鬼才建图法
把一个串当作一条边,最左边两个 和 最右边两个 当作点,边长是该串的长度
则图上的节点最多只有\(26∗26\)个节点(两个字母之间,26 * 26个组合),最多只有\(1e5\)条边(字符串个数)
然后在该图上做01分数规划(点权均为1)
玄学优化:统计所有点被更新的总次数,如果该次数大于 10000,就存在负环
(这太玄学了)
- 注意二分的边界(\(r\)的设置)
Code
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int N = 700, M = 1e5 + 5;
const double eps = 1e-4;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
double dis[N];
int cnt[N];
bool vis[N];
void add (int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool spfa (double x) { //这里又忘了写double
queue <int> q;
memset (dis, 0, sizeof dis);
memset (vis, true, sizeof vis);
memset (cnt, 0, sizeof cnt);
for (int i = 0; i < 676; i ++) //注意n不是点数,这里要写676
q.push (i);
int ct = 0; //迭代次数
while (!q.empty()) {
int t = q.front();
q.pop();
vis[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dis[j] < dis[t] + w[i] - x) { //点权是1
dis[j] = dis[t] + w[i] - x;
cnt[j] = cnt[t] + 1;
if (cnt[j] >= 676)
return true;
if (++ ct > 10000)
return true; //玄学优化
if (!vis[j])
q.push (j), vis[j] = true;
}
}
}
return false;
}
int main () {
IOS;
while (cin >> n, n) {
memset (h, -1, sizeof h);
idx = 0;
while (n --) {
string s;
cin >> s;
int len = s.size();
if (len >= 2) {
int a = (s[0] - 'a') * 26 + (s[1] - 'a');
int b = (s[len - 2] - 'a') * 26 + (s[len - 1] - 'a'); //注意顺序
add (a, b, len);
}
}
if (!spfa(0)) {
cout << "No solution" << endl;
continue;
}
double l = 0, r = 1001;
while (l + eps < r) {
double mid = (l + r) / 2;
if (spfa(mid))
l = mid;
else
r = mid;
}
cout << fixed << setprecision(2) << l << endl; //算出来是21.6666 (但是用cout就会变成21.67...)
//printf ("%.2lf\n", l);
}
}