浅谈最短路和差分约束

差分约束系统

n 个未知量 x1, x2 ⋯ xnm 个不等式条件

这样的一组不等式就是一个差分约束系统,差分约束系统的求解可以巧妙的转化为最短路问题

和最短路的关系

先移项,得到 ,可以发现和最短路中的三角不等式非常相似,。因而可以转化成最短路问题,具体来说,先对不等式建图,每个不等式都可看成边,让 ji 连一条权为 c 的边,此图上跑最短路,得到的 disi 就是一组满足条件的解。

转化的充要性证明

限制条件转化为有向边,跑最短路得到的 dis 满足 ,把 disu, disv 看成 xu, xv 的话,也就是 ,因而当最短路有解,也就是没有负环,原不等式就有解,且解就为 dis必要性成立

思考一下变量关系,若是关系无环比如说是一条链,容易想到,此时的不等式和最短路都是有解的。若是变量关系成环,设存在一个有向环 ,对应的边权依次为

由每条边对应的约束,有

将上述不等式逐项代入相消可得

若不等式组有解,则必有

否则会出现矛盾,而这也意味着图中不存在负环,所以此时最短路也是有解的,充分性成立

因此,不等式组有解等价于图中的最短路有解,这个转化是充分且必要的

最小解和最大解

若是 ,这样从 u 跑到 v ,得到 ,可以发现 xv 的解正好取等,即最大解,也可推出其他变量的解都为最大解。若我们想要最小解,那么让 ,这样从 u 跑到 v,不就有最小解了吗。但是,这样还是跑最短路吗?再观察 ,回想一下我们前面的证明,可以发现最短路的关键性质,,和此不等式不再契合。然而有没有什么满足 这个性质呢?仔细一看,这不正是最长路的性质吗,对的,我们直接跑最长路就可以了,相似的,无解条件为存在正环

因此, 这样 建边跑最短路能得到最大解, 这样 建边跑最长路能得到最小解

负权图最短路

有负权边后,dijkstra 就不再适用了,因为 dijkstra 基于贪心思想,每走一步期望的 dis 都应该是单调不减的。所以我们需要可以在负权图上跑最 短/长 路,且能判 负/正 环的算法

Bellman-Ford O(n ⋅ m)

此算法暴力的进行 n − 1 轮,每轮都从所有的点向所有方向走,这样最短路一定可以延展出来,因为一条简单路最多 n − 1 条边嘛。正是因为它非常之暴力,所以能处理 dijkstra 处理不了的负权问题。还能检测负环,可以额外跑一轮,若还有 dis 可以更新的话,那只能是存在负环了

MYCODE


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;

constexpr ll inf = 1e18;

void solve(){
    int n, m;
    cin >> n >> m;
    vector<vector<pii>> gra(n + 1);
    for(int i=1; i<=m; i++){
        int u, v, w;
        cin >> u >> v >> w;
        if(w >= 0){
            gra[u].push_back({v, w});
            gra[v].push_back({u, w});
        }
        else gra[u].push_back({v, w});
    }

    vector<ll> dis(n + 1, inf);
    dis[1] = 0;
    for(int k=0; k<n-1; k++){ // 尝试 n - 1 轮松弛
        for(int i=1; i<=n; i++){
            if(dis[i] == inf) continue;
            for(auto [v, w] : gra[i]){
                dis[v] = min(dis[v], dis[i] + w);
            }
        }
    }

    bool neg = false; // 额外跑一轮 // neg 为 ture 则存在负环
    for(int i=1; i<=n; i++){
        if(dis[i] == inf) continue;
        for(auto [v, w] : gra[i]){
            neg |= dis[i]+w < dis[v];
        }
    }
    
    cout << (neg? "YES":"NO") << '\n';
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t = 1; cin >> t;
    while(t--) solve();
    
    return 0;
}

SPFA O(k ⋅ m)

Bellman-Ford 中,最短路还没延展到的点的遍历是多余的。所以可以用一个队列,存已经延展的点,从这些点去扩展。spfa 非常类似换掉 pqdijkstra,它是 Bellman-Ford 算法的队列优化版,复杂度为 O(k ⋅ m) ~ O(n ⋅ m)。此外,也有启发式的 spfa,不过比较玄学,启发的大致思想是,更容易松弛别人的点就放队列前,在此不过多赘述

MYCODE

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;

constexpr ll inf = 1e18;

void solve(){
    int n, m;
    cin >> n >> m;
    vector<vector<pii>> gra(n + 1);
    for(int i=1; i<=m; i++){
        int u, v, w;
        cin >> u >> v >> w;
        if(w >= 0){
            gra[u].push_back({v, w});
            gra[v].push_back({u, w});
        }
        else gra[u].push_back({v, w});
    }

    bool neg = false;
    int s = 1; // 源点 // 可能要建超级源点
    vector<bool> inq(n + 1);
    vector<int> cnt(n + 1); // 当前到 u 的最短路的边数
    vector<ll> dis(n + 1, inf);
    dis[s] = 0, inq[s] = true;
    queue<int> q;
    q.push(s);
    while(!q.empty() && !neg){
        int u = q.front(); q.pop();
        inq[u] = false;
        for(auto [v, w] : gra[u]){
            if(dis[u] + w < dis[v]){
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1;
                if(!inq[v]) q.push(v), inq[v]=true;
                if(cnt[v] >= n){
                    neg = true;
                    break;
                }
            }
        }
    }
    
    cout << (neg? "YES":"NO") << '\n';
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t = 1; cin >> t;
    while(t--) solve();
    
    return 0;
}

题目链接: 【模板】负环

差分约束例题

【模板】差分约束

我们先建一个超级源点 s,其对所有点建一条权为 0 的边,主要目的是让图连通,方便处理,也相当于加了限制条件 ,而且这样操作是不影响可解性的,因为若有解,则整体平移后也是一个解。建好图,从 s 开始跑一下最短路就可以了。

注意 : 加源点后图中就是 n + 1 个节点了 , 要判断 cnt[v] ≥ n + 1

MYCODE

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;

constexpr ll inf = 3e18;

// 差分约束 就是 负权图最短路
void solve(){
    int n, m;
    cin >> n >> m;
    vector<vector<pii>> gra(n + 1);
    for(int i=1; i<=m; i++){
        int u, v, w;
        cin >> v >> u >> w;
        gra[u].push_back({v, w});
    }
    for(int i=1; i<=n; i++){
        gra[0].push_back({i, 0});
    }

    bool neg = false;
    int s = 0;
    vector<bool> inq(n + 1);
    vector<int> cnt(n + 1);
    vector<ll> dis(n + 1, inf);
    dis[s] = 0, inq[s] = true;
    queue<int> q;
    q.push(s);
    while(!q.empty() && !neg){
        int u = q.front(); q.pop();
        inq[u] = false;
        for(auto [v, w] : gra[u]){
            if(dis[u] + w < dis[v]){
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1;

                if(!inq[v]) q.push(v), inq[v]=true;

                if(cnt[v] >= n+1){ // 有源点, 所以是 n+1 !
                    neg = true;
                    break;
                }
            }
        }
    }
    if(neg){
        cout << "NO" << '\n';
        return;
    }

    for(int i=1; i<=n; i++){
        cout << dis[i] << " \n"[i == n];
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t = 1; //cin >> t;
    while(t--) solve();
    
    return 0;
}

SP116 - Intervals

题意

n 个区间 [ai, bi],每个区间至少选 ci 个整数。满足这 n 条限制的情况下,至少要取多少个整数

解析

每条限制可转化成 ,很容易想到差分约束。关键是如何建图去描述这个问题,思考 pre 数组的特点,大概为 ,这两个条件已经足够描述出合法的前缀和数组,我们将此额外限制加入图中,这样就可以得到合法的解

整理一下 :

这题需要求最小解,我们要跑最长路。此外,这题值域有 0,我们简单平移一下即可

MYCODE

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;

constexpr ll inf = 1e18;

// 差分约束 , 最长路 , 求最小解
/*
  好像不需要判正环

*/
const int MX = 5e4+1;
void solve(){
    int n; cin >> n;
    vector<vector<pii>> gra(MX + 1);
    for(int i=1; i<=n; i++){
        int u, v, c;
        cin >> u >> v >> c;
        u++, v++, c;
        gra[u-1].push_back({v, c});
    }
    for(int i=1; i<=MX; i++){
        gra[i-1].push_back({i, 0}); // 0 <= pi - pi-1 <= 1 // 两者都要
        gra[i].push_back({i-1, -1});
    }

    int s = 0;
    vector<bool> inq(MX + 1);
    vector<ll> dis(MX + 1, -inf);
    dis[s] = 0, inq[s] = true;
    queue<int> q;
    q.push(s);
    while(!q.empty()){
        int u = q.front(); q.pop();
        inq[u] = false;
        for(auto [v, w] : gra[u]){
            if(dis[u] + w > dis[v]){ // 改这里即为最长路
                dis[v] = dis[u] + w;

                if(!inq[v]) q.push(v);

                inq[v] = true;
            }
        }
    }
    cout << dis[MX] << '\n';
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t = 1; cin >> t;
    while(t--) solve();
    
    return 0;
}

A - 2019 CCPC Harbin

和上面那道题比较相似,但多了一个二分,限制条件要想好,注意 spfa 中的剪枝,否则会 tle

题意

n 个立方体一排,涂色,有两种限制,分别有 M1M2 个。第一种:[Li, Ri] 中的立方体涂色个数  ≥ k,第二种:[Li, Ri] 外的立方体涂色个数  ≥ k。求满足限制的最小染色立方体数

(n ≤ 3000, 0 ≤ M1, M2 ≤ 3000)

解析

和上面的题比较相似。不同点在于限制二是 ,多一个变量,容易想到二分 pren 去掉一个变量,这样就转化成了差分约束问题。我们二分 pren 设为 ,因为前缀和的性质,我们还有

限制条件即为 :

要注意的是,为了解的正确性,我们至少将所有的限制边都遍历过一遍,而此题仅从超级源点出发不是很能保证满足这个条件。因为 ,可能会有 ,这导致点 n 不会入队,也就会使得点 n 相关的限制边没有遍历到,从而导致错解。解决办法为,不建超级源点 s,而是直接将每个点都加入队列中,这样一定可以保证每个限制边都遍历过

另,此人竟然提交了惊人的 77 发之后 ? ? 终于算是过掉了这题 ? ? QAQ

( 这人也太菜了,我无言以对 )

MYCODE

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;

constexpr ll inf = 3e18;

// 差分约束 , 最长路 , 求最小解
/*
  单调性 
  pn 越大 , 条件越宽松 , 更容易有解
  容易证明, 无解有解对应的 pn 区间连续

*/

void solve(){
    int n, m1, m2;
    cin >> n >> m1 >> m2;
    vector<tuple<int, int, int>> q1(m1), q2(m2);
    for(auto& [l, r, c] : q1) cin >> l >> r >> c;
    for(auto& [l, r, c] : q2) cin >> l >> r >> c;

    vector<bool> inq(n + 1);
    vector<int> cnt(n + 1);
    vector<ll> dis(n + 1, -inf);
    vector<vector<pii>> gra(n + 1);

    auto check = [&](int pn){
        inq.assign(n+1, 0), cnt.assign(n+1, 0), dis.assign(n+1, -inf);
        for(int i=0; i<=n; i++) gra[i].clear(); // [0, n+1]

        for(auto [l, r, c] : q1){
            gra[l-1].push_back({r, c});
        }
        for(auto [l, r, c] : q2){
            gra[r].push_back({l-1, c-pn});
        }
        for(int i=1; i<=n; i++){
            gra[i-1].push_back({i, 0}); // 0 <= pi - pi-1 <= 1 // 两者都要
            gra[i].push_back({i-1, -1});
        }

        queue<int> q;
        for(int i=0; i<=n; i++) q.push(i), inq[i] = true; // 保证所有的限制边都遍历到
        dis[0] = 0, dis[n] = pn; // 设定 p0 = 0 // 二分了 pn , 这里要保持一致
        while(!q.empty()){
            int u = q.front(); q.pop();
            inq[u] = false;
            
            // while 中直接剪枝
            if(u == 0 && dis[u] > 0) return false;
            if(u == n && dis[u] > pn) return false;

            for(auto [v, w] : gra[u]){
                if(dis[u] + w > dis[v]){
                    dis[v] = dis[u] + w;
                    cnt[v] = cnt[u] + 1;

                    if(!inq[v]) q.push(v), inq[v] = true;

                    if(cnt[v] >= n+1) return false; // 因为是 n+1 个点啊
                }
            }
        }
        return true;
    };
    
    int lo = -1, hi = n+1;
    while(lo + 1 != hi){
        int mid = lo + hi >> 1;
        if(check(mid)) hi = mid;
        else lo = mid;
    }
    cout << hi << '\n';
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t = 1; cin >> t;
    while(t--) solve();
    
    return 0;
}

此外,也可以转化成差分约束系统,两边取 log  即可

此篇完结.