虽然小明并没有多少钱,但是小明办了很多张银行卡,共有 n 张,以至于他自己都忘记了每张银行卡里有多少
钱了。 他只记得一些含糊的信息,这些信息主要以下列三种形式描述:
1. 银行卡a 比银行卡b 至少多c 元。 2. 银行卡a 比银行卡b 至多多c 元。 3. 银行卡a 和银行卡c 里的存款一样多。 但是由于小明的记忆有些差,他想知道是否存在一种情况,使得银行卡的存款情况和他记忆中的所有信息吻合。 输入格式: 第一行输入两个整数n 和m,分别表示银行卡数目和小明记忆中的信息的数目。(1≤n,m≤10000) 接下来 m 行:
如果每行第一个数是1,接下来有三个整数a,b,c,表示银行卡a 比银行卡b 至少多c 元。 如果每行第一个数是2,接下来有三个整数a,b,c,表示银行卡a 比银行卡b 至多多c 元。 如果每行第一个数是3,接下来有两个整数 a,b,表示银行卡a 和b 里的存款一样多。(1≤n,m,a,b,c≤10000) 输出格式: 如果存在某种情况与小明的记忆吻合,输出Yes,否则输出No。
样例输入
3 3
3 1 2
1 1 3 1
2 2 3 2
样例输出
Yes
首先讲一下差分约束系统
差分约束系统是最短路的一类经典应用。如果一个不等式组由n个变量和m个约束条件
组成,且每个约束条件都是形如 xi−xj≤k,1≤i,j≤n 的不等式,则称其为 差分约束系统
(system of difference constraints)。差分约束系统是求解一组变量的不等式组的
算法。 问题转化
我们在求解差分约束系统时,可以将其转化为图论中单源最短路(或最长路)问题。 对于不等式中的其中一组 xj−xi≤k,我们会发现它类似最短路网络(全部由最短路上
的边组成的子图)中的三角不等式 dv−du≤w<u,v>,即 du+w<u,v>≥dv,所以我们
可以理解成从顶点u 到顶点 v 连一条权值为 w<u,v> 的边,用最短路算法得到最短路
的答案 di,也就求出了原不等式组的一个解。 因此我们可以将每个变量xi作为一个顶点,对于约束条件 xj−xi≤k,连接一条边权为
k 的有向边 <i,j>。我们再增加一个超级源s,s 连向其余每个顶点,边权均为0。对这
个图执行单源最短路算法,如果程序正常结束,那么得到的最短路答案数组di 就是满
足条件的一组解;若图中存在负环,则该不等式组无解。
两种连边方法
连边有两种方法,第一种是连边后求最长路的方法,第二种是连边后求最短路
的方法。 例: xj−xi≤k
若求最短路,则变形为 xi+k≥xj,从i 到j 连一条权值为k 的边。 若求最长路,则变形为 xj−k≤xi,从j 到i 连一条权值为k 的边。 考虑到差分约束系统的边权可能为负,我们套用前面介绍的 SPFA 算法可解决这个问题。
直接上代码
#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
const int MAX_M=10000;
const int MAX_N=1000;
const int inf=0x3f3f3f3f;
int head[MAX_N];
int number_entrance[MAX_N];
int dis[MAX_N];
int inq[MAX_N];
int ans=0;
int n,m;
struct edge{
int w;
int to;
int next;
}eid[MAX_M];
void insert(int u,int v,int w){
eid[ans].w=w;
eid[ans].to=v;
eid[ans].next=head[u];
head[u]=ans++;
}
int spfa(int start){
int sum=0;
int totle=1;
memset(inq,0, sizeof(inq));
memset(head,-1, sizeof(head));
memset(number_entrance,0, sizeof(number_entrance));
memset(dis,inf, sizeof(dis));
inq[start]=1;
number_entrance[start]++;
dis[start]=0;
deque<int> q;
q.push_back(start);
while(!q.empty()){
int x=q.front();q.pop_front();
if(dis[x]*totle>sum){
q.push_back(x);
continue;
}
//这里采用了LLL优化算法(large lable last)
sum-=dis[x];
inq[x]=0;
totle--;
for(int i=head[x];i!=-1;i=eid[i].next){
int y=eid[i].to;
if(dis[x]+eid[i].w<dis[y]){
dis[y]=dis[x]+eid[i].w;
if(inq[y]==0){
number_entrance[y]++;
inq[y]=1;
totle++;
sum+=dis[y];
if(dis[y]<dis[q.front()]){
q.push_front(y);
}//这里采用了SLF优化算法(small lable first)
else
q.push_back(y);
if(number_entrance[y]>=n)
return 0;
}
}
}
}
return 1;
}
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++){
insert(0,i,0);
insert(i,0,0);
}
for(int i=0;i<m;i++){
int temp;
cin>>temp;
if(temp==3){
int a,b;
cin>>a>>b;
insert(a,b,0);
insert(b,a,0);
}
else if(temp==2){
int a,b,c;
cin>>a>>b>>c;
insert(a,b,-c);
} else{
int a,b,c;
cin>>a>>b>>c;
insert(b,a,c);
}
}
if(spfa(0)==1)
cout<<"YES";
else
cout<<"NO";
return 0;
}
这里采用的是最短路的算法,所以产生负圈的时候就直接返回0,不能产生满足方程组的变量。
• SLF:Small Label First 策略,设要加入的顶点是 j,队首元素为i,若
d[j]<d[i],则将j 插入队首,否则插入队尾;
• LLL:Large Label Last 策略,设队首元素为i,队列中所有最短距离值的
平均值为x,若 d[i]>x 则将i 插入到队尾,查找下一元素,直到找到某一顶点i 使 得 d[i]≤x,则将i 出队进行松弛操作。 • SLF 可使速度提高 15~ 20%;SLF + LLL 可提高约 50%。 在解决算法题目时,不带优化的 SPFA 就足以解决问题;而一些题目会故意制
造出让 SPFA 效率低下的数据,即使你使用这两个优化也无法避免“被卡”。
因此,SLF 和 LLL 两个优化仅作了解就可以了,在竞赛中不必使用。 对于稀疏图而言,SPFA 相比堆优化的 Dijkstra 有很大的效率提升,但是对于
稠密图而言,SPFA 最坏为 O(VE),远差于堆优化 Dijkstra 的 O((V+E)logV)。
当然,在图中包含负权边时,SPFA 几乎是唯一的选择。因此,大家在做题时,
还是要根据数据的具体情况来判断使用哪种最短路算法。