生词
pushy a.纠缠不休的
coordinate [kəʊ’ɔ:dɪneɪt] n.坐标
constraints [kən’streɪnt] n.限制
subsequent a.随后的
line-up v.排成直线
arbitrarily ['ɑ:bɪtrəlɪ] adv.任意地;
题目阅读
一群牛排成一队,由于有些牛彼此喜欢,则会要求距离不超过一定的距离;反之,彼此厌恶的牛,则会要求距离至少不小于某个距离。用数轴表示牛的位置,某个坐标可以存在多个牛。已知牛的喜恶,求满足该限制情况下的1号牛与n号牛的距离
本题是差分约束问题。
设牛t1、t2的喜欢对方,且要求距离不超过m,本题输入要求为t1<t2,则可以写为
t1-t2 <= m
引理:
http://tsreaper.is-programmer.com/posts/180182
将图形理论与差分约束系统AX≤B加以联系。对于i∈[1,n],图中的每个顶点vi对应着n个未知量的一个xi。图中的每个有向边对应于两个未知量的m个不等式的其中一个。这样,通过求解新建图的单源最短路径问题就能得到差分约束系统的一组解。(联系最短路与差分约束)
为保证图的连通,在图中引入附加节点vs,使图中每个顶点vi都能从vs可达,并设弧<vs,vi>的权w(vs,vi)=0。对于每个差分约束xj-xi≤bk(注意是小于等于号),则弧<xi,xj>的权w(xi,xj)=bk。(1.引入超级源Vs,它与所有点联通,且边权为0。2.差分与边权的映射。)
——《图论及应用》
综上,则可以认为w(t1,t2)=m.
设牛t3、t4的厌恶对方,且要求距离不少过n,本题输入要求为t3<t4,则可以写为
t3-t4 >= n.
又因为差分对应最短路,需要差分式子为xj-xi≤bk(注意是小于等于号),则可以通过等式两边同乘负一,不等式相应变号将大于等于转换为小于等于。(大于等于需转换)
即t4-t3 <= -n
对于最短路问题而言,-n就是一个负权边,权值为w(t4,t3)=-n。
对于含负权边的问题可以考虑Floyd、BF、SPFA。又因为我们认为ti、tj是vs
到vi、vj的最短路dis[i]、dis[j],那么这是单源含负权边的最短路问题,一般采取高效的SPFA。如果使用SPFA的时候不想添加附加的节点vs的话,可以在初始化的时候把所有的点都加入队列中,其实就是相当于源点vs入队,开始算法后vs出队更新所得到的队列,因为没有边指向vs,所以后面的更新不会涉及vs,即在后面的计算中都不会用到。
由问题: the greatest possible distance between cows 1 and N. 求1号牛与n号牛的最大距离,即求t1-tn<=m的m,则本题的最大源vs是1号牛t1。
需要说明两个退化情况:
1.如果图中存在负权回路,则该差分约束系统不存在可行解。
2.vs到某点如果不存在最短路径,即最短路径为INF,则对于该点所表示的变量可以取任意值,都能满足差分约束系统的要求。
由说明可得:
负权回路:
如果不能排成一队,则输出-1.则1到n牛中存在一头牛到vs的所有路中没有最短路,不存在可行解,即出现负权回路,需要对负权回路判断 :一个点到源点的最短路上的边如果超过n-1,则说明存在负环。这里用数组来记录各节点的松弛次数,松弛等价于在该点到源点的最短路上加一条边。当存在一点的最短路的边数>=n,则存在负权回路。
无最短路:
如果1和n号牛的距离可以任意大,输出-2.则t1-tn<= m中,tn可以取任意值都比m小,则m为无穷大。那么,源点到n号牛不存在路,更不存在最短路,即最短路为无穷大。
综上,退化情况可以理解为:1.ti(i∈[1,n])没有解,即存在源点到一点存在路,但没有最短路,则存在负权回路;2.源到某个点没有路,即距离为无穷大。
有路无最短则有负回;
无路则距离无穷大。
有路有最短则正常
代码如下
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int ma=10010,mv=1010,INF=0x3f3f3f3f;
int first[mv],nex[ma],u[ma],v[ma],w[ma],vis[mv],dis[mv];
int out[mv];
int spfa(int n){
queue<int> que;
memset(vis,0,sizeof vis);
memset(dis,INF,sizeof dis);
int k,i;
que.push(1);
vis[1]=1;//入队后标记1已入
dis[1]=0;
while(!que.empty()){
k=que.front();
if(!vis[k]){
out[k]++;//注意入的是顶点,不是边i
if(out[k]>=n)return -1;/*入队超过n次则说明到源点的最短路有n条,
根据抽屉原理,该路上必然有两个相同的点,因此存在环。又根据松弛
能够避免正环的出现,故一定是负环*/
for(i=first[k];i!=-1;i=nex[i])
{
if(dis[u[i]]==INF)continue;
if(dis[v[i]]>dis[u[i]]+w[i]){
dis[v[i]]=dis[u[i]]+w[i];
vis[v[i]]=1;
que.push(v[i]);
}//如果被松弛过才入队
}
}
que.pop();
vis[k]=0;//两次vis的操作对象不同
}
return 1; //需要返回值
}
int main(){
int n,ml,md,i;
freopen("test.txt","r",stdin);
memset(first,-1,sizeof first);
cin>>n>>ml>>md;
for(i=1;i<=ml;i++)
{
cin>>u[i]>>v[i]>>w[i];
nex[i]=first[u[i]];
first[u[i]]=i;
}
for(i=1;i<=md;i++)
{
cin>>v[i+ml]>>u[i+ml]>>w[i+ml];//实际改为u到v的边为w
w[i+ml]=-w[i+ml];
nex[i+ml]=first[u[i+ml]];//由于等式变号,则边的源点为u,即改为样例v位置上的顶点
first[u[i+ml]]=i+ml;//需要修改入边的编号
}
int x=spfa(n);
if(x==-1)cout<<"-1"<<endl;
else if(dis[n]==INF)cout<<"-2"<<endl;
else cout<<dis[n]<<endl;
return 0;
}
代码中存在可优化的录入部分,因为转换不等号需要交换u、v和改变w,导致录入邻接表易混。最好建立录入函数,将录入的u、v、w修改后作为参数传入,自动录入邻接表和边集,而边集的秩则需要有全局变量来负责控制。
int e=1;
void add(int a,int b,int c){
u[e]=a;
v[e]=b;
w[e]=c;
nex[e]=first[a];
first[a]=e;
}
for(i=1;i<=ml;i++)
{
cin>>x>>y>>z;
add(x,y,z);
}
for(i=1;i<=md;i++)
{
cin>>x>>y>>z;
add(y,x,-z);
}
这样录入部分更加简洁、不易出错。