浅谈最短路和差分约束

浅谈最短路和差分约束
amiracle差分约束系统
n 个未知量 x1, x2 ⋯ xn,m 个不等式条件
这样的一组不等式就是一个差分约束系统,差分约束系统的求解可以巧妙的转化为最短路问题
和最短路的关系
先移项,得到
转化的充要性证明
限制条件转化为有向边,跑最短路得到的 dis 满足
思考一下变量关系,若是关系无环比如说是一条链,容易想到,此时的不等式和最短路都是有解的。若是变量关系成环,设存在一个有向环
由每条边对应的约束,有
将上述不等式逐项代入相消可得
若不等式组有解,则必有
否则会出现矛盾,而这也意味着图中不存在负环,所以此时最短路也是有解的,充分性成立
因此,不等式组有解等价于图中的最短路有解,这个转化是充分且必要的
最小解和最大解
若是
因此,
负权图最短路
有负权边后,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 非常类似换掉 pq 的 dijkstra,它是 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 的边,主要目的是让图连通,方便处理,也相当于加了限制条件
注意 : 加源点后图中就是 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 条限制的情况下,至少要取多少个整数
解析
每条限制可转化成
整理一下 :
这题需要求最小解,我们要跑最长路。此外,这题值域有 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 个立方体一排,涂色,有两种限制,分别有 M1,M2 个。第一种:[Li, Ri] 中的立方体涂色个数 ≥ k,第二种:[Li, Ri] 外的立方体涂色个数 ≥ k。求满足限制的最小染色立方体数
(n ≤ 3000, 0 ≤ M1, M2 ≤ 3000)
解析
和上面的题比较相似。不同点在于限制二是
限制条件即为 :
要注意的是,为了解的正确性,我们至少将所有的限制边都遍历过一遍,而此题仅从超级源点出发不是很能保证满足这个条件。因为
另,此人竟然提交了惊人的 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;
}此外:
此篇完结.












