【图论】—— SPFA判负环 + 01分数规划


 

关于SPFA

【图论】—— Bellman-Ford算法和SPFA算法_玄澈_的博客-CSDN博客

负环

给定一张有向图(无向图的每条边可以看做是两条方向相反的有向边,从而按照有向图处理),每条边都有一个权值(长度)。若一条边的长度是负数,则称它是负权边。若存在一个环,环上各边的权值之和是负数,则称这个环为“负环”

             算法名称               能否处理负权边           时间复杂度
Dijlstra不能,负权可能造成当前最小的dist[x]以后不一定最小O(n^{2})
Heap-Dijkstra不能,该算法仅当每一个点第一次从堆中取出时执行,等价于选最小的dist[x]O(m logn)
Bellman-ford能,无负环时,最短路包含的边数 < n, n - 1 轮迭代后一定收敛O(mn)
SPFA能,本质是队列优化的Bellman-ford,负环只会增加入队次数O(km)-O(nm)

 如果图中存在负环,那么直观的表现是:无论经历多少轮迭代,总存在有向边 (x,y,z) 使得dist[y] > dist[x] + z , BL算法和SPFA算法永远都不能结束。

根据抽屉原理,若存在一个 dist[x] ,从起点1到结点x的最短路包含 \geq n 条边,则这条路径必然重复经过了某个 节点p 。换言之,这条最短路上存在一个环,环上各点的值都能更新下一个点的 dist 值。 p 绕这条环一圈,最终能更新它自己。因此,这个环的长度是负数。每饶一圈,最短路的长度只会越来越小,不可能收敛到每条边都满足三角不等式的状态。


SPFA判负环 

设 cnt[x] 表示从1到 x 的最短路径所包含的边数, cnt[1] = 0 。当执行更新 dist[y] = dist[x] + z 时, 同样更新 cnt[y] = cnt[x] + 1 。若此时发现 cnt[y] \geqslant n ,则图中有负环。若算法正常结束,则图中没有负环。

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N],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(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] + 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()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    if(spfa()) puts("Yes");
    else puts("No");
    
    return 0;
}


 01分数规划

例题:AcWing 361. 观光奶牛 

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

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

输出这个最大值。

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

输入格式

第一行包含两个整数 LL 和 PP。

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

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

输出格式

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

数据范围

2≤L≤10002≤L≤1000,
2≤P≤50002≤P≤5000,
1≤f[i],t[i]≤10001≤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

解题思路:

 


AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;

const int N = 1010, M = 5010;

int n, m;
int wf[N];
int h[N], e[M], wt[M], ne[M], idx;
double dist[N];
bool st[N];
int cnt[N];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], wt[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 = 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;
                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 a, b, c;
        cin >> 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", r);
    
    return 0;
}

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是SPFA算法记录路径的C++代码模板: ```c++ #include <iostream> #include <cstring> #include <queue> #include <vector> using namespace std; const int MAXN = 1005; const int INF = 0x3f3f3f3f; struct Edge { int to, w; }; vector<Edge> edges[MAXN]; // 存储图的邻接表 int dist[MAXN]; // 存储源点到各个点的最短距离 int pre[MAXN]; // 存储路径上每个点的前驱节点 bool inQueue[MAXN]; // 标记每个点是否在队列中 void SPFA(int s, int n) { queue<int> q; memset(dist, INF, sizeof(dist)); memset(inQueue, false, sizeof(inQueue)); memset(pre, -1, sizeof(pre)); dist[s] = 0; pre[s] = s; inQueue[s] = true; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); inQueue[u] = false; for (int i = 0; i < edges[u].size(); i++) { int v = edges[u][i].to; int w = edges[u][i].w; if (dist[v] > dist[u] + w) { dist[v] = dist[u] + w; pre[v] = u; // 更新前驱节点 if (!inQueue[v]) { inQueue[v] = true; q.push(v); } } } } } void printPath(int s, int t) { vector<int> path; for (int i = t; i != s; i = pre[i]) { // 从终点往回找前驱节点 path.push_back(i); } path.push_back(s); reverse(path.begin(), path.end()); // 反转路径,使其从起点到终点 for (int i = 0; i < path.size(); i++) { cout << path[i] << " "; } cout << endl; } int main() { int n, m, s, t; cin >> n >> m >> s >> t; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; edges[u].push_back({v, w}); edges[v].push_back({u, w}); // 无向图 } SPFA(s, n); if (dist[t] == INF) { cout << "No path!" << endl; } else { cout << "Shortest path: " << dist[t] << endl; cout << "Path: "; printPath(s, t); } return 0; } ``` 在SPFA算法中,每个节点的前驱节点存储在pre数组中。当算法执行完毕后,可以使用pre数组从终点往回找前驱节点,直到找到起点为止,即可得到最短路径。这里使用一个vector来存储路径上的节点,最后反转vector中的元素,使其从起点到终点输出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玄澈_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值