差分约束系统详解

前言

差分约束系统是一种特殊的N元一次不等式组,不等式组的每一个不等式称为一个约束条件。通过不等式的变形,可以通过最短路算法对差分约束系统进行求解。


相关定义

  • 约束条件:一个满足的不等式称为一个约束条件

  • 差分约束系统:一个由若干个约束条件构成的N元一次不等式组称为一个差分约束系统


性质

  • $i,j\in[1,n]$,$k\in [1,m]$

  • $n_k\in R$


观察约束条件的通式$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的农场种树


2019.9.9 于厦门外国语学校石狮分校

你可能感兴趣的:(差分约束系统详解)