从差分约束到同余最短路
以前总听说过这个算法,终于可以学一学了。 :::info[AI 使用说明] 使用 AI 进行了部分文本修缮。 :::
算法解决的问题(差分约束部分)
先讲差分约束,差分约束主要是解决形如
算法解决步骤
用这些不等式可以做什么?我们考虑给他们变形一下,得到
我们看一下这一张图片,从
答案是肯定的,但我们还要注意一下这条边的方向,一定是
不难发现,图是不一定连通的,所以应当设置一个超级源点向每个点都连一条边,边权随意。
接下来就需要选择合适的最短路算法,对于这一块,我们需要根据题面合理分析,如果出现负权边,就应当使用 spfa,否则可以使用 dijkstra,毕竟他跑得更快嘛。
还有一个问题,出现负环怎么办,我们看一下如下的图:
从中可以列出不等式组:
我们把这些不等式转化一下,得到
当然题目一般来说不会简单的给你形如
- 对于
x_i - x_j \ge k 的不等式,我们在两边同乘-1 ,得到x_j - x_i \le -k 。 - 对于
x_i - x_j = k 的不等式,应当给他拆成两部分,即x_i - x_j \le k 和x_i - x_j \ge k 。 - 对于给出一段区间和,我们可以用前缀和数组,定义
q_i 表示前i 个数的和,那么q_r - q_{l-1} = k ,然后再用第二种方法拆分。
这三种技巧在后续的题目也会经常用到,所以还是要记牢。
算法代码
一般来说都是要使用 spfa 的,我们使用 SLF+LLL 优化,但是码量会更多,因为 LLL 优化比较难写,之后的题目我就只用 SLF 优化了。下面是时间对比(怎么这么糊,将就看吧):
可以发现其实差别不大。
:::success[朴素 spfa 代码]
#include <bits/stdc++.h>
#define ll long long
#define puf push_front
#define pof pop_front
#define pub push_back
#define pob pop_back
using namespace std;
const int N = 5e3 + 5, inf = 1e9;
int n, m, vis[N], dis[N], sz[N];
struct node{int to, w;};
vector <node> ed[N];
bool spfa(){//这部分几乎是板子
queue <int> q;
for(int i = 1;i <= n + 1;i++) dis[i] = inf;
vis[n + 1] = 1, dis[n + 1] = 0, q.push(n + 1);
while(!q.empty()){
int now = q.front();
q.pop(); vis[now] = 0;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i]){
dis[i] = dis[now] + bw; sz[i] = sz[now] + 1;
//注意一共 n+1 个点所以判断条件是 sz[i] > n
if(sz[i] > n) return 0; if(vis[i]) continue;
vis[i] = 1, q.push(i);
}
}
}
return 1;
}
int main(){
cin >> n >> m;
//建立超级源点
for(int i = 1;i <= n;i++) ed[n + 1].pub({i, 0});
for(int i = 1;i <= m;i++){
//建边
int u, v, w; cin >> u >> v >> w;
ed[v].pub({u, w});
}
if(!spfa()) return cout << "NO", 0;
for(int i = 1;i <= n;i++) cout << dis[i] << " ";
return 0;
}
::: :::success[SLF+LLL 优化代码]
#include <bits/stdc++.h>
#define ll long long
#define puf push_front
#define pof pop_front
#define pub push_back
#define pob pop_back
using namespace std;
const int N = 5e3 + 5, inf = 1e9;
int n, m, vis[N], dis[N], sz[N];
struct node{int to, w;};
vector <node> ed[N];
bool spfa(){
deque <int> q;
for(int i = 1;i <= n + 1;i++) dis[i] = inf;
vis[n + 1] = 1, dis[n + 1] = 0, q.puf(n + 1);
int cnt = 1; ll sum = dis[n + 1];
while(!q.empty()){
int now = q.front();
//LLL优化
while(1ll * dis[now] * cnt > sum) q.pof(), q.pub(now), now = q.front();
q.pof(); vis[now] = 0, cnt--, sum -= dis[now];
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i]){
dis[i] = dis[now] + bw; sz[i] = sz[now] + 1;
if(sz[i] > n) return 0; if(vis[i]) continue;
vis[i] = 1, cnt++, sum += dis[i];
//SLF优化
if(q.empty() || dis[i] < dis[q.front()]) q.puf(i);
else q.pub(i);
}
}
}
return 1;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= n;i++) ed[n + 1].pub({i, 0});
for(int i = 1;i <= m;i++){
int u, v, w; cin >> u >> v >> w;
ed[v].pub({u, w});
}
if(!spfa()) return cout << "NO", 0;
for(int i = 1;i <= n;i++) cout << dis[i] << " ";
return 0;
}
:::
例题
P1993 小 K 的农场
这道题几乎也是板子,只需要根据那
#include <bits/stdc++.h>
#define ll long long
#define puf push_front
#define pof pop_front
#define pub push_back
#define pob pop_back
using namespace std;
const int N = 5e3 + 5, inf = 1e9;
int n, m, vis[N], dis[N], sz[N];
struct node{int to, w;};
vector <node> ed[N];
bool spfa(){
deque <int> q;
for(int i = 1;i <= n + 1;i++) dis[i] = inf;
vis[n + 1] = 1, dis[n + 1] = 0, q.puf(n + 1);
while(!q.empty()){
int now = q.front();
q.pof(); vis[now] = 0;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i]){
dis[i] = dis[now] + bw; sz[i] = sz[now] + 1;
if(sz[i] > n) return 0; if(vis[i]) continue;
vis[i] = 1;
if(q.empty() || dis[i] < dis[q.front()]) q.puf(i);
else q.pub(i);
}
}
}
return 1;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= n;i++) ed[n + 1].pub({i, 0});
for(int i = 1;i <= m;i++){
int op, u, v, w; cin >> op;
if(op == 1) cin >> u >> v >> w, ed[u].pub({v, -w});
if(op == 2) cin >> u >> v >> w, ed[v].pub({u, w});
if(op == 3) cin >> u >> v, ed[u].pub({v, 0}), ed[v].pub({u, 0});
}
cout << (spfa() ? "Yes" : "No");
return 0;
}
:::
P2294 [HNOI2005] 狡猾的商人
这一道题目也几乎是板子,把区间和转化成前缀和相减就可以了,还是很简单的,这里不过多赘述。
这时候可能有人会问了,定义
#include <bits/stdc++.h>
#define ll long long
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
using namespace std;
const int N = 105, inf = 1e9;
int n, m, T, dis[N], vis[N], sz[N];
struct node{int to, w;};
vector <node> ed[N];
bool spfa(){
deque <int> q;
for(int i = 0;i <= n + 1;i++) dis[i] = inf;
vis[n + 1] = 1, dis[n + 1] = 0, q.puf(n + 1);
while(!q.empty()){
int now = q.front();
q.pof(), vis[now] = 0;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i]){
dis[i] = dis[now] + bw, sz[i] = sz[now] + 1;
if(sz[i] > n + 1) return 0;//此时有 n+2 个点最多执行 n+1 轮
if(vis[i]) continue;
vis[i] = 1;
//注意,一定要判队列是否为空,否则 RE 找上门
if(!q.empty() && dis[i] < dis[q.front()]) q.puf(i);
else q.pub(i);
}
}
}
return 1;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> T;
while(T--){
cin >> n >> m;
for(int i = 0;i <= n;i++) ed[n + 1].pub({i, 0});
for(int i = 1;i <= m;i++){
int s, t, w; cin >> s >> t >> w;
ed[s - 1].pub({t, w}), ed[t].pub({s - 1, -w});
}
cout << (spfa() ? "true" : "false") << "\n";
for(int i = 0;i <= n + 1;i++) ed[i].clear(), vis[i] = sz[i] = 0;
}
return 0;
}
:::
P4878 [USACO05DEC] Layout G
这一道题目看似是板子,但是它有坑!设
接下来考虑
这样看是不是显而易见?我们应该选择长度最短的那一条才能满足所有不等式。于是解法就出来了!从
#include <bits/stdc++.h>
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
using namespace std;
const int N = 1005, inf = 1e9;
int n, T1, T2, dis[N], vis[N], sz[N];
struct node{ int to, w; };
vector <node> ed[N];
bool spfa(int s, int l, int r){//spfa板子
deque <int> q;
for(int i = l;i <= r;i++) dis[i] = inf, vis[i] = sz[i] = 0;
dis[s] = 0, q.puf(s), vis[s] = 1;
while(!q.empty()){
int now = q.front();
q.pof(), vis[now] = 0;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i]){
dis[i] = dis[now] + bw;
sz[i] = sz[now] + 1;
if(sz[i] >= (r - l + 1)) return 0;
if(vis[i]) continue;
vis[i] = 1;
if(q.empty() || dis[i] < dis[q.front()]) q.puf(i);
else q.pub(i);
}
}
}
return 1;
}
int main(){
cin >> n >> T1 >> T2;
for(int i = 1;i <= n;i++) ed[n + 1].pub({i, 0});
for(int i = 2;i <= n;i++) ed[i].pub({i - 1, 0});//注意这部分建边
while(T1--){//第一组关系
int l, r, w; cin >> l >> r >> w;
ed[l].pub({r, w});
}
while(T2--){//第二组关系
int l, r, w; cin >> l >> r >> w;
ed[r].pub({l, -w});
}
//简单的判断
if(!spfa(n + 1, 1, n + 1)) return cout << "-1", 0;
spfa(1, 1, n);
cout << (dis[n] == inf ? -2 : dis[n]);
return 0;
}
:::
P3275 [SCOI2011] 糖果
注意,这一道题目并不能用 spfa 拿到满分,不过也是一道不错的练手题。
上一题说到了最短路其实是求答案的上界,那么这一题要求下界就应该使用最长路,具体来说就是把题目给的信息转化成不等式,实现起来不难,唯一要注意的地方是每个小朋友都要分到糖果这句话,对于这种条件,我们可以建立超级源点,向每个点连一条边权为
:::warning[注意这份代码非正解]
#include <bits/stdc++.h>
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
#define int long long
using namespace std;
const int N = 1e5 + 5, inf = 1e9;
int n, k, dis[N], sz[N]; bool vis[N];
struct node{ int to, w; };
vector <node> ed[N];
bool spfa(){//简单的spfa
deque <int> q;
for(int i = 1;i <= n + 1;i++) dis[i] = -inf;
dis[n + 1] = 0, q.puf(n + 1), vis[n + 1] = 1;
while(!q.empty()){
int now = q.front();
q.pof(); vis[now] = 0;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw > dis[i]){
dis[i] = dis[now] + bw; sz[i] = sz[now] + 1;
if(sz[i] >= n + 1) return 0; if(vis[i]) continue;
vis[i] = 1;
if(q.empty() || dis[i] < dis[q.front()]) q.puf(i);
else q.pub(i);
}
}
}
return 1;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> n >> k;
//简单的建边
for(int i = 1;i <= n;i++) ed[n + 1].pub({i, 1});
for(int i = 1;i <= k;i++){
int bh, u, v; cin >> bh >> u >> v;
if(bh == 1) ed[v].pub({u, 0}), ed[u].pub({v, 0});
if(bh == 2) ed[u].pub({v, 1});
if(bh == 3) ed[v].pub({u, 0});
if(bh == 4) ed[v].pub({u, 1});
if(bh == 5) ed[u].pub({v, 0});
}
if(!spfa()) return cout << -1, 0;
int ans = 0;
for(int i = 1;i <= n;i++) ans += dis[i];
cout << ans;
return 0;
}
:::
接下来讲讲这题正解怎么做,我们注意到本题的边权只有
所以受此启发,这题的正解就是缩点,然后拓扑排序一下求最长路就行了。 :::success[code]
#include <bits/stdc++.h>
#define pub push_back
#define int long long
using namespace std;
const int N = 1e5 + 5, inf = 1e9;
int n, k, tot, top, dp[N], ind[N]; bool pd[N];
int low[N], dfn[N], stk[N], zx[N], siz[N];
struct node{ int to, w; };
vector <node> ed[N], ed2[N]; queue <int> q;
void dfs(int x){//tarjan求强连通分量
low[x] = dfn[x] = ++tot;
stk[++top] = x, pd[x] = 1;
for(auto bl : ed[x]){
int i = bl.to;
if(!dfn[i]){
dfs(i);
low[x] = min(low[x], low[i]);
} else if(pd[i])
low[x] = min(low[x], dfn[i]);
}
if(low[x] == dfn[x]){
while(1){
int y = stk[top--]; pd[y] = 0;
zx[y] = x; siz[x]++;
if(y == x) break;
}
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> n >> k;
for(int i = 1;i <= n;i++) ed[n + 1].pub({i, 1});
for(int i = 1;i <= k;i++){
int bh, u, v; cin >> bh >> u >> v;
if(bh == 1) ed[v].pub({u, 0}), ed[u].pub({v, 0});
if(bh == 2) ed[u].pub({v, 1});
if(bh == 3) ed[v].pub({u, 0});
if(bh == 4) ed[v].pub({u, 1});
if(bh == 5) ed[u].pub({v, 0});
}
n++; for(int i = 1;i <= n;i++) if(!dfn[i]) dfs(i);
for(int i = 1;i <= n;i++){//缩点
for(auto bl : ed[i]){
int j = bl.to, bw = bl.w;
if(zx[i] == zx[j] && bw == 1)
return cout << -1, 0;
int z1 = zx[i], z2 = zx[j];
if(z1 != z2) ed2[z1].pub({z2, bw}), ind[z2]++;
}
}
for(int i = 1;i <= n;i++) if(zx[i] == i && ind[i] == 0) q.push(i);
//拓扑排序
while(!q.empty()){
int t = q.front(); q.pop();
for(auto bl : ed2[t]){
int i = bl.to, bw = bl.w;
dp[i] = max(dp[i], dp[t] + bw);
if(--ind[i] == 0) q.push(i);
}
}
int ans = 0;
for(int i = 1;i <= n;i++)
ans += dp[zx[i]];
cout << ans;
return 0;
}
:::
算法解决的问题(同余最短路部分)
可以看这道题的题面,这是同余最短路的标准形式,这里我把它放出来。
求解这个方程:
算法解决步骤
我们先从简化版的题目跳楼机讲起,即求有多少个整数
我们可以这样想,如果一个数满足了
这样做有什么好处?这也是我初次看题解不太理解的地方,我们这样想,如果按照原式
所以可以从中受到启发,定义
我们考虑到结果
其实这两个不等式也很好理解,显然
得到了不等式,接下来就可以用类似差分约束的办法解决。显然
算法代码
不难发现,这一块没有负权边,所以可以使用 dijkstra 算法求解,这里给出 P3403 的代码。 :::success[code]
#include <bits/stdc++.h>
#define int long long
#define pub push_back
using namespace std;
const int N = 1e5 + 5, inf = 1e18;
int h, x, y, z, dis[N];
bool vis[N];
struct node{int to, w;};
vector <node> ed[N];
bool operator < (node t1, node t2){ return t1.w > t2.w; }
void dij(){ //朴实无华的 dijkstra
priority_queue <node> q;
memset(vis, 0, sizeof vis);
for(int i = 0;i < x;i++) dis[i] = inf;
dis[0] = 0, q.push({0, 0});
while(!q.empty()){
int now = q.top().to;
q.pop(); if(vis[now]) continue;
vis[now] = 1;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i])
dis[i] = dis[now] + bw,
q.push({i, dis[i]});
}
}
}
int calc(int h){ //计算答案
dij(); int ans = 0;
for(int i = 0;i < x;i++)
if(dis[i] <= h) ans += (h - dis[i]) / x + 1;
return ans;
}
signed main(){
cin >> h >> x >> y >> z;
for(int i = 0;i < x;i++){ //简单的建边
ed[i].pub({(i + y) % x, y});
ed[i].pub({(i + z) % x, z});
}
cout << calc(h - 1);
return 0;
}
:::
但是我好像发现了一个问题,就是用 spfa 好像更快耶,由于特殊的建边方式,spfa 算法并不会被卡,所以接下来的题目我们就用 spfa 吧。
为了使证据充分,我们以 P2371 [国家集训队] 墨墨的等式 这道题为例,看一下时间对比,这一道题也是标准的模板,需要注意的是有两个地方:
- 注意
a_i = 0 的情况。 - 模数可以取一个最小且非零的
a_i ,这样子可以减小复杂度。
下面是时间对比图片:
:::success[spfa 代码]
#include <bits/stdc++.h>
#define int long long
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
using namespace std;
const int N = 15, MAX = 5e5 + 5, inf = 1e12;
int n, m, l, r, a[N], dis[MAX], zx = MAX;
bool vis[MAX];
struct node{int to, w;};
vector <node> ed[MAX];
void spfa(){
deque <int> q;
for(int i = 0;i < zx;i++) dis[i] = inf;
dis[0] = 0, q.puf(0);
while(!q.empty()){
int now = q.front();
q.pof(), vis[now] = 0;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i]){
dis[i] = dis[now] + bw;
if(vis[i]) continue; vis[i] = 1;
if(dis[i] < dis[q.front()]) q.puf(i);
else q.pub(i);
}
}
}
}
int calc(int h){
int ret = 0;
for(int i = 0;i < zx;i++)
if(dis[i] <= h) ret += (h - dis[i]) / zx + 1;
return ret;
}
signed main(){
cin >> m >> l >> r;
for(int i = 1;i <= m;i++){
int x; cin >> x;
if(x) a[++n] = x, zx = min(zx, x);
}
if(n == 0) return cout << 0, 0;
for(int i = 0;i < zx;i++)
for(int j = 1;j <= n;j++)
if(a[j] != zx) ed[i].pub({(i + a[j]) % zx, a[j]});
spfa(); cout << calc(r) - calc(l - 1);
return 0;
}
::: :::success[dijkstra 代码]
#include <bits/stdc++.h>
#define int long long
#define pub push_back
using namespace std;
const int N = 15, MAX = 5e5 + 5, inf = 1e12;
int n, m, l, r, a[N], dis[MAX], zx = MAX;
bool vis[MAX];
struct node{int to, w;};
bool operator < (node t1, node t2){ return t1.w > t2.w; }
vector <node> ed[MAX];
void dij(){
priority_queue <node> q;
memset(vis, 0, sizeof vis);
for(int i = 0;i < zx;i++) dis[i] = inf;
dis[0] = 0, q.push({0, 0});
while(!q.empty()){
int now = q.top().to; q.pop();
if(vis[now]) continue;
vis[now] = 1;
for(auto bl : ed[now]){
int i = bl.to, bw = bl.w;
if(dis[now] + bw < dis[i])
dis[i] = dis[now] + bw,
q.push({i, dis[i]});
}
}
}
int calc(int h){
int ret = 0;
for(int i = 0;i < zx;i++)
if(dis[i] <= h) ret += (h - dis[i]) / zx + 1;
return ret;
}
signed main(){
cin >> m >> l >> r;
for(int i = 1;i <= m;i++){
int x; cin >> x;
if(x) a[++n] = x, zx = min(zx, x);
}
if(n == 0) return cout << 0, 0;
for(int i = 0;i < zx;i++)
for(int j = 1;j <= n;j++)
if(a[j] != zx) ed[i].pub({(i + a[j]) % zx, a[j]});
dij(); cout << calc(r) - calc(l - 1);
return 0;
}
::: 同余最短路的例题不多,以后应该也会更新的。