wsyhb
2020-08-01 23:21:21
前言
前置知识
定义
算法详解
易错点
用错算法
图不连通
代码
最近刷题时,偶然做到一道差分约束的简单题。明明以前学过差分约束,却怎么写都写不对。回过头来发现差分约束的模板题都没过,费了好大劲才解决。
为了不让其他学习差分约束的 OIer 们跟我犯同样的错误,我特地写下这篇题解,讲解了一下基本的算法思路,整理了常见的错误,希望每一个 OIer 都能在这篇题解中有所收获!
另外,如果您在这篇题解中发现任何错误或不当之处,欢迎在评论区指出!
在阅读这篇题解前,你需要掌握 Bellman-Ford
或 SPFA
求解单源最短路径。
(模板:P3371 【模板】单源最短路径(弱化版))
如果一个系统由
换句话说,差分约束系统是求解关于一组变量的特殊不等式组的方法。
(以上内容摘编自百度百科)
看到这个不等式,或许你会有点懵,我们先简单变个形:
或许您会觉得我在侮辱您的智商,不过请继续看下去:
现在将
单独考虑第
那么第
考虑
我们突然发现这是一个最短路的形式 -- 具体来说,
我们回到一开始的式子
上述不等式组显然有可能无解,那么怎么判定无解呢?
由于
什么,你不知道如何判定最短路无解?
当然是判负环呀!
具体来说,如果不存在负环,Bellman-Ford
在
再说说 SPFA
判负环,道理其实是一样的。你每次将一个点丢进队列里,相当于是对这一个点的所有出边进行松弛,那么只需要记录每个点的进队次数,若超过
另外一种思路
在一开始的推导中,我们将
与一开始的推导同理可得,从
那么只需要以此跑最长路,并判断正环即可。
注:读者可以尝试按照一开始的推导演算一遍,加深理解。
这里是重点,不少题解都没有详细讲解这两点,初学差分约束是很有可能踩雷的!
由于 dijkstra
算法,而只能使用 Bellman-Ford
或 SPFA
求最短(长)路的原因。
dijkstra
算法求最短(长)路为何不能用于负(正)权边
(如果您早就明白这是为什么,请自行跳过)
以求最短路为例,dijkstra
算法是利用贪心思想,每次用距离源点最近的、未讨论过的点去更新其他点的最短距离,有点类似于 bfs
(宽度优先搜索)。
但当出现负权边时,后面讨论的点相较于已讨论的点,可能会距离源点更近,导致已讨论的点会被更新,却不会再次拿来讨论,此时 dijkstra
算法就失效了。
不过,如果使用带有堆优化的时间复杂度为 dijkstra
算法,它会退化成 SPFA
算法,相当于将 SPFA
算法中的队列换成堆,时间复杂度为
最后放一个例子,以便读者理解:
以 dijkstra
算法求解单源最短路,所求得的
错误原因是
这里可能会存在点之间既不直接约束也不间接约束的情况,在建图上的体现就是图不连通。
举个简单的例子:(
依据第一种思路建图如下:
此时无论以 SPFA
)
理由是以其中一个点作为源点,只能计算出这个点所在连通块的答案,并没有计算其他连通块的答案。
解决方案
一、SPFA
一开始将所有点放入队列中而不是只放一个起点,这样每个点都被作为起点讨论过。
建一个虚拟源点
二、Bellman-Ford
无需操作,算出来的答案本来就是正确的。
因为每次松弛是对所有边进行操作,不会出现只计算一个连通块的情况。
两种思路、两种算法(SPFA
及 Bellman-Ford
)、两种解决方案,都在下方代码有所体现!
第一种思路(求最短路)+ SPFA
+ 解决方案1(全部点放入队列)
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=5e3+5;//点数
const int M=5e3+5;//边数
int End[M],Last[N],Next[M],Len[M],e;//链式前向星
void add_edge(int x,int y,int z)
{
End[++e]=y;
Next[e]=Last[x];
Last[x]=e;
Len[e]=z;
}
int dis[N],cnt[N];//cnt记录各个点入队次数
queue<int> q;
bool inq[N];//inq标记各个点是否在队列中
bool spfa(int s)
{
for(int i=1;i<=n;i++) dis[i]=1e9;//最短路,初始值为inf
dis[s]=0;
for(int i=1;i<=n;i++)//全部放入队列
{
q.push(i);
inq[i]=true;
cnt[i]++;
}
while(q.size())
{
int x=q.front();
q.pop();
inq[x]=false;
for(int i=Last[x];i;i=Next[i])
{
int y=End[i];
if(dis[y]>dis[x]+Len[i])
{
dis[y]=dis[x]+Len[i];
if(!inq[y])
{
q.push(y);
inq[y]=true;
cnt[y]++;
if(cnt[y]>n+1) return false;//返回 false,意味着无解
}
}
}
}
return true;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add_edge(y,x,z);
}
if(spfa(1))
{
for(int i=1;i<=n;i++) printf("%d%c",dis[i],i<n?' ':'\n');
}
else printf("NO\n");
return 0;
}
第二种思路(求最长路)+ SPFA
+ 解决方案2(虚拟源点)
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=5e3+5;//点数
const int M=1e4+5;//边数,注意虚拟源点连的边
int End[M],Last[N],Next[M],Len[M],e;//链式前向星
void add_edge(int x,int y,int z)
{
End[++e]=y;
Next[e]=Last[x];
Last[x]=e;
Len[e]=z;
}
int dis[N],cnt[N];//cnt记录各个点入队次数
queue<int> q;
bool inq[N];//inq标记各个点是否在队列中
bool spfa(int s)
{
for(int i=0;i<=n;i++) dis[i]=-1e9;//最长路,初始值为-inf
dis[s]=0;
q.push(s);
inq[s]=true;
cnt[s]++;
while(q.size())
{
int x=q.front();
q.pop();
inq[x]=false;
for(int i=Last[x];i;i=Next[i])
{
int y=End[i];
if(dis[y]<dis[x]+Len[i])
{
dis[y]=dis[x]+Len[i];
if(!inq[y])
{
q.push(y);
inq[y]=true;
cnt[y]++;
if(cnt[y]>n+1) return false;//返回 false,意味着无解
}
}
}
}
return true;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) add_edge(0,i,0);//虚拟源点连边
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add_edge(x,y,-z);
}
if(spfa(0))
{
for(int i=1;i<=n;i++) printf("%d%c",dis[i],i<n?' ':'\n');
}
else printf("NO\n");
return 0;
}
第一种思路 + Bellman-Ford
#include<bits/stdc++.h>
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
int n,m;
const int N=5e3+5;//点数
const int M=5e3+5;//边数
struct edge
{
int x,y,z;
}e[M];//存边
int dis[N];
bool Bellman_Ford(int s)
{
for(int i=1;i<=n;i++) dis[i]=1e9;
dis[s]=0;
for(int t=1;t<=n;t++)
for(int i=1;i<=m;i++)
dis[e[i].y]=min(dis[e[i].y],dis[e[i].x]+e[i].z);
for(int i=1;i<=m;i++)
if(dis[e[i].x]+e[i].z<dis[e[i].y]) return false;//返回 false,意味着无解
return true;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++) scanf("%d%d%d",&e[i].y,&e[i].x,&e[i].z);
if(Bellman_Ford(1))
{
for(int i=1;i<=n;i++) printf("%d%c",dis[i],i<n?' ':'\n');
}
else printf("NO\n");
return 0;
}