SPFA找负环

求负环的常用方法,基于 SPFA

  1. 统计每个点入队的次数,如果某个点入队 n n n 次,则说明存在负环
  2. 统计当前每个点的最短路中所包含的边数,如果某个点的最短路所包含的边数大于等于 n n n,则也说明存在环

904. 虫洞

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

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

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

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

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

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

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

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

输入格式

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

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

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

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

输出格式

输出共 F F 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
输出样例:
NO
YES

这是一道寻找负环的裸题

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 510, M = 5210;
int t, n, m1, m2, h[N], e[M], w[M], ne[M], idx;
int dist[N], q[N], cnt[N];
bool st[N];

void add(int x, int y, int z){
    e[idx] = y;
    w[idx] = z;
    ne[idx] = h[x];
    h[x] = idx++;
}

bool spfa(){
    memset(dist, 0, sizeof dist);
    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] + 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(){
    cin >> t;
    while(t--){
        cin >> n >> m1 >> m2;
        memset(h, -1, sizeof h);
        idx = 0;
        while(m1--){
            int x, y, z;
            cin >> x >> y >> z;
            add(x, y, z);
            add(y, x, z);
        }
        while(m2--){
            int x, y, z;
            cin >> x >> y >> z;
            add(x, y, -z);
        }
        if(spfa()) puts("YES");
        else puts("NO");
    }
    return 0;
}

361. 观光奶牛

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

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

输出这个最大值。

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

输入格式

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

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

再接下来 P P P 行,每行三个整数 a , b , t [ i ] a,b,t[i] abt[i] 表示点 a a a b b b 之间存在一条边,边的权值为 t [ i ] t[i] 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
输出样例:
6.00

思路:

  • 设答案为 a n s ans ans

  • 二分答案,设当前二分值为 m i d mid mid

  • 设一个环 S S S 的边权为 t 1 , t 2 , t 3 … t_1,t_2,t_3… t1,t2,t3 点权为 f 1 , f 2 , f 3 … f_1,f_2,f_3… f1,f2,f3

    m i d < = a n s mid<=ans mid<=ans,即存在一个环 S S S 使得 m i d < = ∑ f i ∑ t i mid<=\frac{∑f_i}{∑t_i} mid<=tifi,变换一下: ∑ ( f i − m i d ∗ t i ) > = 0 ∑(f_i-mid∗t_i)>=0 (fimidti)>=0

    否则,则 m i d > a n s mid>ans mid>ans

  • 我们可以把点权添加至以该有向边为起点的对应边上

  • 每次 c h e c k check check 的时候,一条 u u u 指向 v v v,边权为 w w w 的边权变为:

    f u − w ∗ m i d f_u-w∗mid fuwmid。我们只需检查这个图是否存在正环即可。

  • 如何检查是否存在正环

    方法一:可以将所有边取负号,然后检查这个图是否存在负环即可

    方法二:将 SPFA 中求最短路径改为求最大路径,同时 c n t cnt cnt 计数,就变成了检查是否存在正环

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 1010, M = 5010;
int n, m, wf[N], h[N], e[M], ne[M], w[M], idx;
double dist[N];
int cnt[N];
bool st[N];

void add(int x, int y, int z){
    e[idx] = y;
    w[idx] = z;
    ne[idx] = h[x];
    h[x] = idx++;
}

bool check(double mid){
    memset(dist, 0, sizeof dist);
    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 * w[i]){
                dist[j] = dist[t] + wf[t] - mid * 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(){
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++) cin >> wf[i];
    while(m--){
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    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", r);
    return 0;
}

1165. 单词环

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

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

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

如下例:

ababc
bckjaca
caahoynaab

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

输入格式

本题有多组数据。

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

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

读入以 n = 0 n=0 n=0 结束。

输出格式

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

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

数据范围

1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105

输入样例:
3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0
输出样例:
21.66

难点在于?:

  1. 如何去建图
  2. 如何使得平均长度最大
  3. 数据较大,如何进行优化

建图:由于只关注这个字符串的最后两位和下一个字符串的前两位字符,因此,可以将一个字符串的前两位和最后两位作为一条边的顶点,字符串长度作为边的权值大小进行建图,这样可以将点的数量降低至 26 ∗ 26 26*26 2626,例如字符串ababc、bckjaca、caahoynaab

在这里插入图片描述

同样,使用上个题目的二分进行推理:

在这里插入图片描述

在上述公式中,当 $M = 0 $ 时 ∑ ( w i − M ) ∑(w_i- M) (wiM) 有最大值,因此先带入 M = 0 M = 0 M=0 检测最大值是否大于 0 0 0,如果最大值都小于 0 0 0,则说明不存在正环。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 700, M = 100010;
int n, h[N], e[M], w[M], ne[M], idx;
double dist[N];
int cnt[N];
bool st[N];

void add(int x, int y, int z){
    e[idx] = y;
    w[idx] = z;
    ne[idx] = h[x];
    h[x] = idx++;
} 

bool check(double mid){
    memset(dist, 0, sizeof dist);
    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; // 经验上的trick
                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++){
            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 = 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;
}

if(++count > 10000) return true; 为一种比较取巧的优化:当求最长路时,经过的点大于某一个数时,我们就可以武断地认为当前图中存在一个正环。

上述两道题目都使用了 01 01 01 分数规划

  • 即求 ∑ f ∑ w \frac{∑f}{∑w} wf 的最大值 v v v
  • 这种形式的问题被称为 01 01 01 分数规划,通常使用二分法来求解:
  • 如果 v > m i d v>mid v>mid ,那么取二分区间的右边,否则取左边。
  • v > m i d v>mid v>mid 进行变换,得到
  • ∑ ( f i − m i d ∗ w i ) > 0 ∑(f_i−mid∗w_i)>0 (fimidwi)>0
  • 进而有 ∑ ( m i d ∗ w i − f i ) < 0 ∑(mid∗w_i−f_i)<0 (midwifi)<0
  • 也就是转化为判断图中有没有负环,其中边权指的是 m i d ∗ w i − f i mid∗w_i−f_i midwifi
  • 接下来就是基本操作了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值