AcWing 算法提高课 SPFA求负环 专题

负环

原理复习

  1. 统计每个点的入队次数,如果某个点入队 n 次,则有负环
  2. 统计当前每个点的最短路中包含的边数,如果某点的最短边所包含的边数 \(\geq n\),有负环 (点重合)

建一个虚拟源点,向每个点连一条权值为0的边,这样所有的点都能到达且可初始化为0

Trick: SPFA太多次被卡,即 当所有点入队次数超过2*n(注:这个值可能上下浮动,还是得靠经验所以很玄学)时,就认为有很大可能存在负环

虫洞

题目

思路

Code

时间变小了也就相当于存在负环,把值变小,所以转化为判断是否存在负环

要注意是循环队列!

细节: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

思路

Code

实数二分 + SPFA

01分数规划:求\(\frac{\sum f_i}{\sum t_i}\)最大 (点权\(f_i\),边权\(t_i\)

做法:二分

\[\frac{\sum f_i}{\sum t_i}>mid\rightarrow \sum f_i>mid\,\sum t_i\rightarrow \sum f_i-mid\sum t_i>0\rightarrow \sum (f_i-mid\,t_i)>0 \]

然后可以把边权和点权一起处理,即:\(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);
    }
}
posted @ 2022-04-28 17:00  Sakana~  阅读(32)  评论(0编辑  收藏  举报