差分约束系统

一、何为差分约束系统?

差分约束系统 systemofdifferenceconstraints(system of difference constraints) ,是求解关于一组变数的特殊

不等式组之方法。 如果一个系统由 nn 个变量和 mm 个约束条件组成,其中每个约束条件形如 xixjbk(i,j[1,n],k[1,m])x_i - x_j \le b_k (i, j∈[1, n], k∈[1, m]),则称其为差分约束系统。亦即,差分约束系统是求解关于一组变量的特殊不等式组的方法。

通俗一点地说,差分约束系统就是一些不等式的组,而我们的目标是通过给定的约束不等式组求出最大值或者最小值或者差分约束系统是否有解。

这个不等式组有两种情况:

  • 无解。
  • 有无数组解。

二. 差分约束系统的解

差分约束系统的解只有以下两种情况:

  • 无解(很显然)
  • 有无数组解:

先来看一组数据:

{x1x21x2x32x3x11\begin{cases}x_1 - x_2 \le 1\\x_2 - x_3 \le 2\\x_3 - x_1 \le -1\end{cases}

经过暴力枚举依靠我们高超的数感不难得到有其中两组解:

{x1=3x2=2x3=0\begin{cases}x_1 = 3\\x_2 = 2\\x_3 = 0\end{cases}

{x1=10x2=9x3=7\begin{cases}x_1 = 10\\x_2 = 9\\x_3 = 7\end{cases}

这两个解的联系?第二组解中的每个元素比第一组解中的相应元素大 77

这并不是巧合:若 x=(x1,x2,,xn)x = ( x1, x2, …, xn ) 是一个差分约束系统 AxbAx ≤ b 的一个解, dd 为任意常数。则 x+d=(x1+d,x2+d,,xn+d)x + d = ( x1 + d, x2 + d, …, xn + d ) 也是该系统 AxbAx ≤ b 的解。

对于每个 xix_ixjx_j,有 (xj+d)(xi+d)=xjxi(x_j + d) - (x_i + d) = x_j - x_i 。因此,若 xx 满足 AxbAx≤b,则 x+dx + d 也同样满足,因此,差分约束系统若有解,则必有无数组解。

三、算法思想

  • 每一个约束条件的不等式 xixjbk(i,j[1,n],k[1,m])xi-xj≤bk (i,j∈[1,n],k∈[1,m])
  • 转换一下 xibk+xjxi ≤ bk+xj
  • 我们令bk=w(j,i)bk= w(j, i),再将不等式中的i和j变量替换掉,i = v, j = u,将x数组的名字改成d,
  • 则不等式变为:d[v]w(u,v)+d[u]d[v] ≤ w(u, v)+ d[u]与求单源最短路算法中的松弛操作极为相似
if (dis[v] > dis[u] + w(u, v))
{
    dis[v] = dis[u] + w(u, v);
}
  • 则松驰完成后d[v] 的状态必定是w(u,v)+d[u]≤ w(u, v)+ d[u]
  • 因为图中大概率会出现负权边,所以我们通常需要使用SPFASPFA算法。(关于SPFA,它已经死了

四、差分约束系统的难点——建图!!!

**差分约束的难点在于建图,而建图的难点在于找出题目中隐藏的不等式。能使用

差分约束解决的题目,一定在题面中体现了若干个形式相同或不同的不等式。**

建图函数

建图函数————》》(链式前向星

inline void add(int a, int b, int w)
{
    e[++tot].next = head[a];
    e[tot].to = b;
    e[tot].w = w;
    head[a] = tot;
}

建图函数————》》(vector

d[a].push_back({b,k});

建图方法

首先根据题目的要求进行不等式组的统一化。

%%%注意建图函数 add()add() 中参数的位置

将不等式全部化成 xi – xjkxi \ – \ xj \le k 的形式, 这样建立 jij \to i ,权值为kk的边。

{a=b建双向边add(a,b,0) & add(b,a,0)a>bab>0ba1add(a,b,1)a<bab1add(b,a,1)ab+kbakadd(a,b,k)ab+kabkadd(b,a,k)此处省略多种毒瘤......  \begin{cases}a=b 建双向边 \Longrightarrow add(a,b,0) \ \& \ add(b,a,0)\\ a>b \Longrightarrow a-b>0 \Longrightarrow b-a \le 1 \Longrightarrow add(a,b,1)\\ a<b \Longrightarrow a-b \le -1 \Longrightarrow add(b,a,-1)\\ a \ge b+k \Longrightarrow b-a \le k \Longrightarrow add(a,b,k)\\ a \le b+k \Longrightarrow a-b \le k \Longrightarrow add(b,a,k)\\此处省略多种毒瘤 ...... ~~\end{cases}

。。。。。。

如果是左式 aba-b ,那么 add(b,a,k)add(b,a,k) ;如果是 bab-a ,那么 add(a,b,k)add(a,b,k) ; // vectorvector 同理

//dis[i]设为0x3f
 	if(dis[e[i].to]>dis[tmp]+e[i].w)
	{
		dis[e[i].to]=dis[tmp]+e[i].w;
	}
	//----------
	//a-b<=k
	//add(b,a,k);
	//d[b].push_back({a,k});

五、超级源点

因为建出的图可能不连通,所以我们需要虚拟一个不存在的值 x0x0,并对于每一个

xnxn,我们都要多建一条有向边 xnx0<=0xn - x0 <= 0add(0,i,0)add(0,i,0),这样不会破

坏任何不等式,且使原图连通。我们通常称之为 超级源点超级源点

六、常见spfa判断负环

一般输出答案分为两种:

  1. 输出一组满足的解
  2. 无满足解

如何判断是否有满足解

即是判断图中是否存在负环

不严谨证明:对于某个点 xx,与之相连的边至少有 nn(总点数)1-1种, 所以我们可以定义一个 fhfh 数组记录每次点 xx 入队的次数, 如果次数 >n1>n-1 ,即图中存在负环。

inline void Spfa(int x)
{
    init();
    queue<int> q;
    dis[x] = 0;
    vis[x] = 1;
    fh[x]++;
    q.push(x);
    while (!q.empty())
    {
        int tmp = q.front();
        q.pop();
        vis[tmp] = 0;
        for (register int i = head[tmp]; i; i = e[i].next)
        {
            if (dis[e[i].to] > dis[tmp] + e[i].w)
            {
                dis[e[i].to] = dis[tmp] + e[i].w;
                if (!vis[e[i].to])
                {
                    q.push(e[i].to);
                    vis[e[i].to] = 1;
                    fh[e[i].to]++; //---
                    if (fh[e[i].to] > n)
                    {             //---
                        flag = 1; //---
                        return;   //---
                    }             //---
                }
            }
        }
    }
}

七、判断负环优化

上述的判断负环方法最坏情况为O(N2)O(N^2),在比赛中极大几率会TLE 所以接下来给大家推荐几种优化方法

SLF(Small Label First)优化

优化思路:将原队列改成双端队列,对要加入队列的点 p,如果 dist[p] 小于队头元素 u 的 dist[u],将其插入到队头,否则插入到队尾。

inline void Spfa(int x)
{
    deque<int> q; //---
    dis[x] = 0;
    vis[x] = 1;
    q.push_back(x);
    while (!q.empty())
    {
        int tmp = q.front();
        q.pop_front();
        vis[tmp] = 0;
        for (register int i = head[tmp]; i; i = e[i].next)
        {
            if (dis[e[i].to] > dis[tmp] + e[i].w)
            {
                dis[e[i].to] = dis[tmp] + e[i].w;
                if (!vis[e[i].to])
                {
                    if (!q.empty() && dis[e[i].to] > dis[tmp]) //---
                        q.push_back(e[i].to);                  //---
                    else                                       //---
                        q.push_front(e[i].to);                 //---
                    vis[e[i].to] = 1;
                    fh[e[i].to]++;
                    if (fh[e[i].to] > n)
                    {
                        cout << "No";
                        exit(0);
                    }
                }
            }
        }
    }
}

DFS(深搜)优化

便于寻找图中的负环,不建议用,会莫名其妙TLE!~~ 不过这真的是神器!!!

对于20组数据 SLF优化 —— 4s DFS优化 —— 172ms

在这里插入图片描述

神器啊!!!b——

bool spfa(int x)
{
    vis[x] = true;
    for (int i = 0; i < g[x].size(); i++)
    {
        int to = g[x][i].to;
        int w = g[x][i].w;
        if (dis[x] + w < dis[to])
        {
            dis[to] = dis[x] + w;
            if (vis[to])
                return false;
            if (!spfa(to))
                return false;
        }
    }
    vis[x] = false;
    return true;
} //vector

八、题解

1、P5960 【模板】差分约束算法

题目描述

给出一组包含 mm 个不等式,有 nn 个未知数的形如:

{xc1xc1y1xc2xc2y2xcmxcmym\begin{cases}x_{c_1} - x_{c'_1} \le y_1\\x_{c_2} - x_{c'_2} \le y_2\\\cdots\\x_{c_m} - x_{c'_m} \le y_m\end{cases}

的不等式组,求任意一组满足这个不等式组的解。

输入格式

第一行为两个正整数 n,mn,m,代表未知数的数量和不等式的数量。

接下来 mm 行,每行包含三个整数 cccc'yy,代表一个不等式 xcxcyx_c - x_c' \le y

输出格式

一行, nn 个数,表示 x1x_1 , x2xnx_2 \cdots x_n 的一组可行解,如果有多组解,请输出任意

一组,无解请输出 NONO

输入输出样例

输入 #1

3 3
1 2 3
2 3 -2
1 3 1

输出 #1

5 3 5

数据范围

对于 100%100\% 的数据, 1n,m5×103,104y104,1c,cn,cc1 \le n, m \le 5 \times 10^3, -10^4 \le y \le10^4, 1 \le c, c' \le n, c \ne c'

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 99;
int n, m, head[maxn], dis[maxn], vis[maxn], tot, fh[maxn], flag;
inline int Read()
{
    int x = 0, f = 1;
    char c = getchar();
    while (c > '9' || c < '0')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
    {
        x = x * 10 + c - '0';
        c = getchar();
    }
    return x * f;
}
struct Edge
{
    int to, next, w;
} e[maxn];
inline void init()
{
    memset(dis, 0x3f, sizeof(dis));
    memset(vis, 0, sizeof(vis));
    memset(fh, 0, sizeof(fh));
}
inline void add(int a, int b, int w)
{
    e[++tot].next = head[a];
    e[tot].to = b;
    e[tot].w = w;
    head[a] = tot;
}
void Spfa()
{
    init();
    queue<int> q;
    dis[0] = 0;
    vis[0] = 1;
    q.push(0);
    while (!q.empty())
    {
        int tmp = q.front();
        q.pop();
        vis[tmp] = 0;
        for (int i = head[tmp]; i; i = e[i].next)
        {
            if (dis[e[i].to] > dis[tmp] + e[i].w)
            {
                dis[e[i].to] = dis[tmp] + e[i].w;
                if (!vis[e[i].to])
                {
                    q.push(e[i].to);
                    vis[e[i].to] = 1;
                    fh[e[i].to]++;
                    if (fh[e[i].to] > n)
                    {
                        flag = 1;
                        return;
                    }
                }
            }
        }
    }
}
int main()
{
    cin >> n >> m;
    //init();
    for (int i = 1, u, v, w; i <= m; i++)
    {
        cin >> u >> v >> w;
        add(v, u, w);
    }
    for (int i = 1; i <= n; i++)
        add(0, i, 0);
    Spfa();
    if (!flag)
    {
        for (int i = 1; i <= n; i++)
            cout << dis[i] << " ";
    }
    else
        cout << "NO";
    return 0;
}

2、P1993 小 K 的农场

题目描述

小 K 在 MC 里面建立很多很多的农场,总共 nn 个,以至于他自己都忘记了每个农场中种植作物的具体数量了,他只记得一些含糊的信息(共 mm 个),以下列三种形式描述:

农场 aa 比农场 bb 至少多种植了 cc 个单位的作物; 农场 aa 比农场 bb 至多多种植了 cc 个单位的作物; 农场 aa 与农场 bb 种植的作物数一样多。 但是,由于小 K 的记忆有些偏差,所以他想要知道存不存在一种情况,使得农场的种植作物数量与他记忆中的所有信息吻合。

输入格式

第一行包括两个整数 nnmm,分别表示农场数目和小 K 记忆中的信息数目。

接下来 mm 行:

如果每行的第一个数是 11,接下来有三个整数 aa, bb, cc,表示农场 aa 比农场 bb 至少多种植了 cc 个单位的作物;

如果每行的第一个数是 22,接下来有三个整数 aa, bb, cc,表示农场 aa 比农场 bb 至多多种植了 cc 个单位的作物;

如果每行的第一个数是 33,接下来有两个整数 aa, bb,表示农场 aa 种植的的数量和 bb 一样多。

输出格式

如果存在某种情况与小 K 的记忆吻合,输出 YesYes,否则输出 NoNo

输入输出样例

输入 #1

3 3
3 1 2
1 1 3 1
2 2 3 2

输出 #1

Yes

说明/提示

对于 100%100\% 的数据,保证 1n,m,a,b,c5×1031 \le n,m,a,b,c \le 5 \times 10^3

代码

#include <bits/stdc++.h>
#define maxn 10005
using namespace std;
struct edge
{
    int to, w;
};
inline int read()
{
    int x = 0, f = 1;
    char c = getchar();
    while (c > '9' || c < '0')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
    {
        x = x * 10 + c - '0';
        c = getchar();
    }
    return x * f;
}
vector<edge> g[maxn];
bool vis[maxn];
int n, m, dis[maxn];
bool spfa(int x)
{
    vis[x] = true;
    for (int i = 0; i < g[x].size(); i++)
    {
        int to = g[x][i].to;
        int w = g[x][i].w;
        if (dis[x] + w < dis[to])
        {
            dis[to] = dis[x] + w;
            if (vis[to])
                return false;
            if (!spfa(to))
                return false;
        }
    }
    vis[x] = false;
    return true;
}
int main()
{
    n = read();
    m = read();
    for (int i = 1; i <= n; i++)
        g[0].push_back({i, 0});
    for (int i = 1, ins, a, b, c; i <= m; i++)
    {
        ins = read();
        a = read();
        b = read();
        if (ins == 2)
        {
            c = read();
            g[b].push_back({a, c});
        }
        else if (ins == 1)
        {
            c = read();
            g[a].push_back({b, -c});
        }
        else
        {
            g[a].push_back({b, 0});
            g[b].push_back({a, 0});
        }
    }
    memset(dis, 0x3f, sizeof(dis));
    dis[0] = 0;
    if (spfa(0))
        cout << "Yes";
    else
        cout << "No";
    return 0;
}

七、总结一下

差分约束系统的步骤:

  1. 构造不等式
  2. 通过不等式连边
  3. 建图(难)
  4. 跑最短路

THE END\mathcal{THE \ END}

posted @ 2021-04-23 18:59  蒟蒻orz  阅读(5)  评论(0编辑  收藏  举报  来源