前言
差分约束系统是一种特殊的N元一次不等式组,不等式组的每一个不等式称为一个约束条件。通过不等式的变形,可以通过最短路算法对差分约束系统进行求解。
相关定义
-
约束条件:一个满足的不等式称为一个约束条件
-
差分约束系统:一个由若干个约束条件构成的N元一次不等式组称为一个差分约束系统
性质
-
$i,j\in[1,n]$,$k\in [1,m]$
观察约束条件的通式$a_i-a_j\leq n_k$,移项得$a_i\leq a_j+n_k$,是不是和最短路转移中的三角形不等式$d_y\leq d_x+edeg_i$很像?(明明是一模一样)。因此,我们可以将每个约束条件$a_i-a_j\leq n_k$看作一条从$j$连向$i$的权值为$n_k$的有向边,利用最短路算法进行求解。由于$n_k$可能为负数,因此要用SPFA进行求解(当然,dijkstra算法经过一番乱搞也是可以实现的)。
如果题目给出的约束条件是形如$a_i-a_j\geq n_k$,要怎么办呢?有两种办法,一是原样插入,跑最长路;二是将其变形为$a_j-a_i\leq -n_k$,将它看作一条从$i$连向$j$的权值为$-n_k$的有向边,还是跑最短路。
接下来会对差分约束系统的实现进行系统地讲解。在判断差分约束系统的可行性时需要判断负环,因此接下来会先讲解判定负环的方法。
负环
定义
-
环:若一张有向图(若为无向图,则将一条无向边看作两条反向的有向边)上存在一条可以从一个节点经过它回到该节点,则这条路径称作环
-
负环:边的权值和为负数的环称为负环
观察最短路转移的三角形不等式$d_y\leq d_x+edge_i$,若图中存在负环,则$d_y$会随着更新越来越小且会不停更新,即最短路算法Bellman-Ford和SPFA永远不能正常结束。因此,Bellman-Ford算法和SPFA算法可以用来判定负环的存在。
Bellman-Ford算法
Bellman-Ford算法判定负环理论复杂度较慢(虽然SPFA也可能被卡到和Bellman-Ford一样慢),因此不对此进行具体讲解。大致地说,Bellman-Ford算法最多会进行$n-1$轮迭代,若迭代轮数超过$n-1$轮,说明算法没有正常运行,即图中存在负环。时间复杂度$O(nm)$。
SPFA算法
显然一条最短路最多经过$n$个节点,若经过超过$n$个节点,则说明算法没有正常运行,图中存在负环。可以在更新最短路的同时维护一个$cnt$数组,初始化$cnt_s=0$,其中$s$表示最短路的起点,在更新$d_y=d_x+edge_i$的同时,更新$cnt_y=cnt_x+1$。若算法过程中出现$cnt_i\geq n$,说明图中存在负环。理论上时间复杂度会比Bellman-Ford算法稍快一点,但仍可能被卡到$O(nm)$。
~~众所周知,SPFA早在NOI2018就被宣布死亡了。~~因此,很可能出现算法超时的情况。SPFA求负环有一些玄学优化,如把队列换成栈、把bfs改成dfs等,这些玄学优化的确可以提高SPFA判负环的效率,但据说有可能严重降低不存在负环时求最短路的时间。当然,宁可用一些玄学的方法,也不能任它TLE直接宣布死亡。因此还有一个玄学的方法,就是当你知道程序要超时的时候,救程序于水火之中,直接判断它存在负环输出结束。换句话说,你可以限制程序运行的时间,或者限制队列的长度,让它不超时,直接判定存在负环。虽然这样可能会导致答案错误,但总比直接TLE宣布死亡来得好。
众所周知,ctime库里有一个clock()函数可以返回运行的时间,利用它,若程序即将超过时限,直接判定为负环即可。用法如下:
t=clock()/CLOCKS_PRE_SEC;//CLOCKS_PRE_SEC是一个常数
这样t就是当前程序的已运行时间。注意,需要在程序开头先存储一下从启动程序到运行开始使用的时间,然后用t减去这个时间,才是运行的正确时间。
//洛谷P3385 【模板】负环
#include
#include
#include
#include
using namespace std;
const int N=2e3+100,M=3e3+100;
int t,n,m,tot;
int head[N],ver[2*M],edge[2*M],Next[2*M];
int d[N],cnt[N];
bool v[N];
void add(int x,int y,int z)
{
ver[++tot]=y,edge[tot]=z,Next[tot]=head[x],head[x]=tot;
}//邻接表存边
bool spfa()
{
memset(v,0,sizeof(v));
memset(d,0x3f,sizeof(d));
memset(cnt,0,sizeof(cnt));//初始化
queue q;
q.push(1);
d[1]=0,v[1]=1;
while(q.size())
{
int x=q.front();q.pop();v[x]=0;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(d[x]+edge[i]=n)
return 1;//判定负环
if(!v[y])
{
q.push(y);
v[y]=1;
}
}
}
}
return 0;//不存在负环
}//spfa
int main()
{
scanf("%d",&t);
while(t--)
{
memset(head,0,sizeof(head));
memset(ver,0,sizeof(ver));
memset(edge,0,sizeof(edge));
memset(Next,0,sizeof(Next));
tot=0;//初始化
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
if(z>=0)
add(y,x,z);
}
if(spfa())
puts("YE5");
else
puts("N0");
}
return 0;
}
差分约束系统
开头已经讲过最短路求解差分约束系统的大致方法。显然,对于差分约束系统的一组解{$a_i|i\in[1,n]$},{$a_i+\Delta|i\in[1,n]$}也是一组解(因为$\Delta$会在作差是被消掉)。因此,为了判断差分约束系统是否有解,我们可以先求一组负数解,即增加一个编号为0的节点(这个节点的编号应该是一个对题目没有影响的编号,若有的题目用编号0会对答案产生影响,则需要另取编号),令$a_0=0$,并从0号节点向每个节点连一条边,这样就可以保证$\forall i,a_i\leq0$。
以0号节点为起点跑最短路,显然,若图中存在负环,则说明永远满足不了所有的约束条件,差分约束系统无解;否则{$a_i|i\in[1,n]$}就是一组解。特别地,若跑的是最长路,则求的是一组正解,无解的判定条件为图中存在正环。
//洛谷P1993 小K的农场
//卡时
#include
#include
#include
#include
#include
using namespace std;
const int INF=1.99,N=1e4+100,M=2e4+100;
int n,m,tot,start;
int head[N],ver[2*M],Next[2*M],edge[2*M];
int d[N],cnt[N];
bool v[N];
void add(int x,int y,int z)
{
ver[++tot]=y,edge[tot]=z,Next[tot]=head[x],head[x]=tot;
}//邻接表存边
bool spfa()
{
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));
queue q;
q.push(0);
d[0]=0,v[0]=1;
while(q.size())
{
if((double)(clock()-start)/CLOCKS_PER_SEC>=INF)
return 1;//要超时直接判定存在负环
int x=q.front();q.pop();v[x]=0;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(d[x]+edge[i]n)
return 1;
if(!v[y])
{
v[y]=1;
q.push(y);
}
}
}
}
return 0;
}//spfa
int main()
{
start=clock();
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
add(0,i,0);
for(int i=1;i<=m;i++)
{
int k,x,y,z;
scanf("%d",&k);
if(k==1)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,-z);
}
if(k==2)
{
scanf("%d%d%d",&x,&y,&z);
add(y,x,z);
}
if(k==3)
{
scanf("%d%d",&x,&y);
add(x,y,0),add(y,x,0);
}
}
if(spfa())
puts("No");
else
puts("Yes");
return 0;
}
习题
负环
-
模板题:【模板】负环
差分约束系统
-
简单应用题:小K的农场,种树