• 【图论】SPFA求负环


    算法提高课笔记

    基础知识

    负环:环上权值之和是负数

    求负环的常用方法 基于SPFA

    1. 统计每个点入队次数,如果某个点入队n次,则说明存在负环(完全等价于Bellman-Ford算法)
    2. 统计当前每个点的最短路中所包含的边数,如果某点的最短路包含的边数大于等于n,则说明存在负环

    玄学结论:当所有点的入队次数超过2n时,就认为图中有很大肯存在负环

    例题

    虫洞

    原题链接

    农夫约翰在巡视他的众多农场时,发现了很多令人惊叹的虫洞。

    虫洞非常奇特,它可以看作是一条 单向 路径,通过它可以使你回到过去的某个时刻(相对于你进入虫洞之前)。

    农夫约翰的每个农场中包含 N 片田地,M 条路径(双向)以及 W 个虫洞。

    现在农夫约翰希望能够从农场中的某片田地出发,经过一些路径和虫洞回到过去,并在他的出发时刻之前赶到他的出发地。

    他希望能够看到出发之前的自己。

    请你判断一下约翰能否做到这一点。

    下面我们将给你提供约翰拥有的农场数量 F,以及每个农场的完整信息。

    已知走过任何一条路径所花费的时间都不超过 10000 秒,任何虫洞将他带回的时间都不会超过 10000 秒。

    输入格式

    第一行包含整数 F,表示约翰共有 F 个农场。

    对于每个农场,第一行包含三个整数 N,M,W。

    接下来 M 行,每行包含三个整数 S,E,T,表示田地 S 和 E 之间存在一条路径,经过这条路径所花的时间为 T。

    再接下来 W 行,每行包含三个整数 S,E,T,表示存在一条从田地 S 走到田地 E 的虫洞,走过这条虫洞,可以回到 T 秒之前。

    输出格式

    输出共 F行,每行输出一个结果。

    如果约翰能够在出发时刻之前回到出发地,则输出 YES,否则输出 NO。

    数据范围

    1 ≤ F ≤ 5 1≤F≤5 1F5
    1 ≤ N ≤ 500 , 1≤N≤500, 1N500,
    1 ≤ M ≤ 2500 , 1≤M≤2500, 1M2500,
    1 ≤ W ≤ 200 , 1≤W≤200, 1W200,
    1 ≤ T ≤ 10000 , 1≤T≤10000, 1T10000,
    1 ≤ S , E ≤ N 1≤S,E≤N 1S,EN

    输入样例

    2
    3 3 1
    1 2 2
    1 3 4
    2 3 1
    3 1 3
    3 2 1
    1 2 3
    2 3 4
    3 1 8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出样例

    NO
    YES
    
    • 1
    • 2

    题意

    负环板题

    思路

    跑一遍spfa

    代码

    #include 
    
    using namespace std;
    
    const int N = 510, M = 5210;
    
    int n, m1, m2;
    int h[N], ne[M], e[M], w[M], idx;
    int dist[N];
    int cnt[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()
    {
        memset(dist, 0, sizeof dist);
        memset(cnt, 0, sizeof cnt);
        memset(st, 0, sizeof st);
    
        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; // 更新次数一旦超过n就说明有负环
                    if (!st[j])
                    {
                        q.push(j);
                        st[j] = true;
                    }
                }
            }
        }
        return false;
    }
    
    int main()
    {
        ios::sync_with_stdio(false);
        cin.tie(0), cout.tie(0);
    
        int T;
        cin >> T;
        while (T -- )
        {
            cin >> n >> m1 >> m2;
            memset(h, -1, sizeof h);
            idx = 0;
    
            for (int i = 0; i < m1; i ++ )
            {
                int a, b, c;
                cin >> a >> b >> c;
                add(a, b, c), add(b, a, c);
            }
            for (int i = 0; i < m2; i ++ )
            {
                int a, b, c;
                cin >> a >> b >> c;
                add(a, b, -c);
            }
    
            if (spfa()) puts("YES");
            else puts("NO");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85

    观光奶牛

    原题链接

    给定一张 L 个点、P 条边的有向图,每个点都有一个权值 f[i],每条边都有一个权值 t[i]。

    求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。

    输出这个最大值

    注意:数据保证至少存在一个环。

    输入格式

    第一行包含两个整数 L 和 P。

    接下来 L 行每行一个整数,表示 f[i]。

    再接下来 P 行,每行三个整数 a,b,t[i],表示点 a 和 b 之间存在一条边,边的权值为 t[i]。

    输出格式

    输出一个数表示结果,保留两位小数。

    数据范围

    2 ≤ L ≤ 1000 , 2≤L≤1000, 2L1000,
    2 ≤ P ≤ 5000 , 2≤P≤5000, 2P5000,
    1 ≤ f [ i ] , t [ i ] ≤ 1000 1≤f[i],t[i]≤1000 1f[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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    输出样例

    6.00
    
    • 1

    题意

    给出点权和边权,求一个环使得点权之和除以边权之和最大

    思路

    求比值最大的问题 -> 01分数规划 -> 二分

    首先确定边界值,答案一定在[0, 1000) 之间

    每次取中点mid,如果图中存在一个环使得比值大于mid,则答案在mid和r之间,反之亦然

    现在问题变成了如何判断是否存在一个比值大于mid的环?

    判断: ∑ f i ∑ t i > M i d \frac{\sum f_i}{\sum t_i}>Mid tifi>Mid
    化简: ∑ f i − M i d ∗ ∑ t i > 0 \sum f_i-Mid*\sum t_i>0 fiMidti>0

    在环上的点权我们可以任意加到边权上,对环不会有影响,假设我们将所有点权加到出边上,化简: ∑ ( f i − M i d ∗ t i ) > 0 \sum (f_i-Mid*t_i)>0 (fiMidti)>0

    现在将边权全看成 f i − M i d ∗ t i f_i-Mid*t_i fiMidti

    问题转换成了:图中是否存在一个正环

    求最长路即可

    代码

    #include 
    
    using namespace std;
    
    const int N = 1010, M = 5010;
    
    int n, m;
    int wf[N];
    int h[N], e[M], ne[M], wt[M], idx;
    double dist[N];
    int cnt[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)
    {
        memset(dist, 0, sizeof dist);
        memset(st, 0, sizeof st);
        memset(cnt, 0, sizeof cnt);
    
        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] + 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; // 更新次数超过n就说明有正环
                    if (!st[j])
                    {
                        q.push(j);
                        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);
        for (int j = 0; j < m; j ++ )
        {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, c);
        }
    
        double l = 0, r = 1e6;
        while (r - l > 1e-4) // 误差
        {
            double mid = (l + r) / 2;
            if (check(mid)) l = mid;
            else r = mid;
        }
    
        printf("%.2lf\n", l);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    单词环

    原题链接

    我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。

    如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。

    我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。

    如下例:

    ababc
    bckjaca
    caahoynaab
    
    • 1
    • 2
    • 3

    第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5+7+10=22(重复部分算两次),总共使用了 3 个串,所以平均长度是 223≈7.33。

    输入格式

    本题有多组数据。

    每组数据的第一行,一个整数 n,表示字符串数量;

    接下来 n 行,每行一个长度小于等于 1000 的字符串。

    读入以 n=0 结束。

    输出格式

    若不存在环串,输出”No solution”,否则输出最长的环串的平均长度。

    只要答案与标准答案的差不超过 0.01,就视为答案正确。

    数据范围

    1 ≤ n ≤ 105 1≤n≤105 1n105

    输入样例

    3
    intercommunicational
    alkylbenzenesulfonate
    tetraiodophenolphthalein
    0
    
    • 1
    • 2
    • 3
    • 4
    • 5

    输出样例

    21.66
    
    • 1

    题意

    n个字符串,如果a的末尾两个字符和b的开头相同则能相连,求能构成的环的平均长度最大值

    思路

    把每个字符串的首位两个字母看做一个点,比如说样例可以这样建图:
    在这里插入图片描述
    答案就变成了求 ∑ w i ∑ 1 \frac{\sum w_i}{\sum 1} 1wi的最大值

    答案在(0, 1000)之内,二分做,类似观光奶牛

    最终判断式为: ∑ w i − M i d ∗ ∑ 1 > 0 \sum w_i - Mid*\sum 1>0 wiMid1>0

    判断有没有解直接把 Mid = 0 代入即可,因为如果等于0都不能满足的话大于0就更不会满足了

    于是成功TLE,用一下玄学优化

    代码

    #include 
    
    using namespace std;
    
    const int N = 700, M = 100010;
    
    int n;
    int h[N], e[M], ne[M], w[M], idx;
    double dist[N];
    int cnt[N];
    bool st[N];
    
    void add(int a, int b, int c)
    {
        e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ;
    }
    
    bool check(double mid)
    {
        memset(st, 0, sizeof st);
        memset(cnt, 0, sizeof cnt);
    
        queue<int> q;
        for (int i = 0; i < 676; i ++ ) // 所有点存入队列
        {
            q.push(i);
            st[i] = true;
        }
    
        int count = 0;
        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] - mid)
                {
                    dist[j] = dist[t] + w[i] - mid;
                    cnt[j] = cnt[t] + 1;
                    if (++ count > 10000) return true; // 次数过多直接返回(玄学可能失败
                    if (cnt[j] >= N) return true; // 这个是保证一定正确的
                    if (!st[j])
                    {
                        q.push(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 ++ )
            {
                cin >> str;
                int len = strlen(str);
                if (len >= 2)
                {
                    // 用类似于26进制的方法存储
                    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 = 1000;
                while (r - l > 1e-4) // 误差
                {
                    double mid = (l + r) / 2;
                    if (check(mid)) l = mid;
                    else r = mid;
                }
                printf("%lf\n", r);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
  • 相关阅读:
    这些论文的作者居然是猫、狗、仓鼠……
    小说接龙小程序
    leaflet+vue2实现地图交互
    java服务器调试指南
    Excel多条件计数——COUNTIFS【获奖情况统计】
    nodejs+vue教室管理系统的设计与实现-微信小程序-安卓-python-PHP-计算机毕业设计
    【C++】模板基础 + STL
    HarmonyOS开发实例:【分布式手写板】
    4. 条件查询
    Qt出现假死冻结现象
  • 原文地址:https://blog.csdn.net/dhxbshbdjzxy/article/details/132791585