差分约束

差分约束概念

如果一个系统由n个变量和m个约束条件组成,形成m个形如xixjk的不等式(i,j∈[1,n],k为常数),则称其为差分约束系统。亦即,差分约束系统是求解关于一组变量的特殊不等式组的方法。

求解差分约束系统,可以转化成图论的单源最短路径(或最长路径)问题。

观察xixj<=ck,会发现它类似最短路中的三角不等式dis[v]<=dis[u]+w[u,v],即dis[v]dis[u]<=w[u,v]。因此,以每个变量xi为结点,对于约束条件xixj<=ck,连接一条边(j,i),边权为ck。我们再增加一个源点s,s与所有定点相连,边权均为0。对这个图,以s为源点运行Bellman-ford算法(或SPFA算法),最终{dis[i]}即为一组可行解。

解释:不等式的形式等同于图论问题中的最短路的求解过程,故将差分约束的不等式问题转换为图论问题

求变量的最大值或最小值(求解性问题)

源点要满足的条件:从源点出发,一定可以走到所有的边

结论:如果求的是最小值,则应该求最长路; 如果求的是最大值,则应该求最短路;

问题:如何转化xi<=C,其中c是一个常数,这类的不等式

方法:建立一个虚拟源点0,然后建立0>i,长度是c的边即可。

以求xi的最大值为例:求所有从xi出发,构成的不等式链xi<=xj+c1<=xk+c2+c1<=....<=x0+c1+c2+...,其中x0是虚拟源点,初始值是已知的,即可求出xi的一个范围的上界
最终xi的最大值等于所有上界的最小值,原因见下图(类似短板效应)

综上,求变量的最大值(都是<=的不等式),即求所有上界中的最小值,即求图上最短路;同理,求变量最大值,即求图上最长路
求最短路时如果图上有负环,那么该变量误解;求最长路时如果图上有正环,则变量无解

求不等式组的可行解(判定性问题)

源点要满足的条件:从源点出发,一定可以走到所有的边

步骤:

  1. 先将每个不等式xi<=xj+ck,转化成一条从xj走到xi,长度为ck的一条边
  2. 找一个超级源点,使得该源点一定可以遍历到所有边
  3. 从源点求一遍单源最短路

结果1:如果存在负环,则原不等式组一定无解
结果2:如果没有负环,则dis[i]就是原不等式组的一个可行解

注: 为何条件要求源点一定要可以走到所有边,为何不是所有点?
每条边都是一个限制条件,差分约束为的是满足所有限制条件,所以必须保证所有边都能走到才能保证满足所有限定条件
如果某些点是孤立点,走到走不到都无所谓,走不到说明对该点没有限制,它的取值是任意而已

SPFA解法

  1. 求变量的最大值或最小值应用实例
    题目描述

分析方法
由于本题求解的为最小值,故由题意可以得到以下方程组

但是,差分约束问题易错点就在于不等关系找的不全面,本题中还有一个容易忽略的要求每个小朋友都要分到糖果。即,如果设s[i]为小朋友i分到的糖果数量,还应有关系s[i]>=1。为了满足差分约束形式的要求,可以设定一个值为0的点s[0],则上述不等关系式可写为s[i]>=s[0]+1

之后考虑差分约束转换为图论问题的条件从源点出发,一定可以走到所有的边。显然,s[0]即可满足要求。故从s[0]开始进行spfa(因要判断是否有解,即图中是否存在正环),将所有小朋友的糖果数量相加即为最终答案

代码实现

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <stack>

using namespace std;
using LL = long long;

const int N = 1e5 + 10, M = 3e5 + 10;

int n, m;
int h[N], e[M], ne[M], w[M], idx;
stack<int> q;
bool st[N];
int cnt[N], dis[N];

void add(int a, int b, int c)
{
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx ++;
}
bool spfa()
{
    q.push(0);
    st[0] = true;
    
    while (q.size())
    {
        int t = q.top();
        q.pop();
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int p = e[i];
            if (dis[p] < dis[t] + w[i])
            {
                dis[p] = dis[t] + w[i];
                cnt[p] = cnt[t] + 1;
                
                if (cnt[p] >= n + 1) return true;
                if (!st[p])
                {
                    q.push(p);
                    st[p] = true;
                }
            }
        }
    }

    return false;
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m --)
    {
        int x, a, b;
        cin >> x >> a >> b;
        if (x == 1) add(a, b, 0), add(b, a, 0);
        else if (x == 2) add(a, b, 1);
        else if (x == 3) add(b, a, 0);
        else if (x == 4) add(b, a, 1);
        else add(a, b, 0);
    }
    for (int i = 1; i <= n; ++ i) add(0, i, 1);
    
    if (spfa()) cout << -1 << endl;
    else 
    {
        LL sum = 0;
        for (int i = 1; i <= n; ++ i) sum += dis[i];
        cout << sum << endl;
    }

    return 0;
}
  1. 求不等式组的可行解
    题目描述

分析方法
num[i]表示给定收银员中,开始工作时间为i的人数为num[i]
s[i]表示R[0]R[i]对应时间段分配收银员的数量为s[i]
r[i]表示时间i要求的收银员数量为r[i]
以上说法较为抽象,以样例为例(为了使用前缀和,数据整体向后迁移一位)

s[]是待求值

0, 23, 22, 1, 10 因前缀和转化为 1, 24, 23, 2, 11

num[1] num[2] num[3] num[4] num[5] num[6] num[7] num[8] num[9] num[10] num[11] num[12] num[13] num[14] num[15] num[16] num[17] num[18] num[19] num[20] num[21] num[22] num[23] num[24]
1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1

1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

r[1] r[2] r[3] r[4] r[5] r[6] r[7] r[8] r[9] r[10] r[11] r[12] r[13] r[14] r[15] r[16] r[17] r[18] r[19] r[20] r[21] r[22] r[23] r[24]
1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

由题意可得以下不等关系

  1. 0sisi1num[i] , 1i24

  2. i8 , sisi8ri

    0<i<7 , si+s24si+16ri

推导后可得

  1. sisi1+0

  2. si1sinum[i]

  3. i8 , sisi8+ri

  4. 0<i<7 , sisi+16s24+ri

前3项均符合差分约束仅包含两个变量的形式要求,但第4项中的存在3个变量,其中,s24是要求解的值
正确的方法为从小到大遍历s24的所有可能取值,第一次满足所有不等式要求的值即为答案
此求解过程体现出的即为求不等式组的可行解

代码实现

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>

using namespace std;

const int N = 30, M = 100;

int n, m;
int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int cnt[N], dis[N];
int x[N], s[N], r[N], num[N];
queue<int> q;

void add(int a, int b, int c)
{
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx ++;
}
void build(int x)
{
    idx = 0;
    memset(h, -1, sizeof h);
    add(0, 24, x), add(24, 0, -x); // 规定s[24] = x,相当于也是添加的一个限定条件,s[24] = x <=> s[24] >= x && s[24] <= x
    // 这里其实没必要判断r[i]是否为0,因为即使为0也不过是一个>=0的限定条件
    // for (int i = 1; i <= 24; ++ i)
    //     if (r[i])
    //     {
    //         if (i >= 8) add(i - 8, i, r[i]);
    //         else add(i + 16, i, r[i] - x);
    //     }
    for (int i = 1; i < 8; ++ i) add(i + 16, i, r[i] - x);
    for (int i = 8; i <= 24; ++ i) add(i - 8, i, r[i]);
    for (int i = 0; i <= 23; ++ i)
    {
        add(i, i + 1, 0);
        add(i + 1, i, -num[i + 1]);
    }
}
bool spfa(int x)
{
    build(x); // 将x作为s[24]构造一个图

    memset(st, 0, sizeof st);
    memset(dis, -0x3f, sizeof dis);
    memset(cnt, 0, sizeof cnt);
    
    dis[0] = 0;
    q.push(0);
    st[0] = true;

    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int p = e[i];
            if (dis[p] < dis[t] + w[i])
            {
                dis[p] = dis[t] + w[i];
                cnt[p] = cnt[t] + 1;

                if (cnt[p] >= 24) return true;
                if (!st[p])
                {
                    q.push(p);
                    st[p] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    int T;
    cin >> T;
    while (T --)
    {
        for (int i = 1; i <= 24; ++ i) cin >> r[i];
        cin >> n;
        memset(num, 0, sizeof num);
        while (n --)
        {
            int t;
            cin >> t;
            ++ t;
            ++ num[t];
        }

        bool flag = false;
        // 判断给定s[24]的合法性,从小到大第一次合法的即为答案要求的最小值
        for (int i = 0; i <= 1000; ++ i) // 枚举s[24]
            if (!spfa(i))
            {
                cout << i << endl;
                flag = true;
                break;
            }
        
        if (!flag) cout << "No Solution" << endl;
    }

    return 0;
}

Tarjan强连通分量缩点解法

SPFA在面对不同数据时的实际表现不稳定,为了保险起见可以采用Tarjan强连通分量缩点,复杂度比较稳定

题目描述
为了更好对比SPFA解法和Tarjan强连通分量解法,采用同一道题目进行讲解

算法思路
从宏观来看:
在有向有环图中,由于依赖关系并非线性排列的,存在环路,所以需要采用SPFA判断环路以及求解最值

但对于拓扑图,依赖关系都是单向的,如果按照拓扑序遍历所有点,即可以线性复杂度维护所有点的要求,最终求和即可

从细节上来看:

  1. 第一步首先需要将有向有环图转化为DAG,使用Tarjan缩点的过程参照之前的写法即可
  2. 建立一张缩点后的图,此过程应同时完成有无可行性解的验证
    通用解决方案为统计每一个scc边权和,若边权和为正则代表存在正环,在本题中即为无解;但在本题中,能够保证边权非负,即只需存在一条正权边即代表存在正环即代表题目无解
  3. 按照拓扑序遍历(scc编号从大到小)整张图,维护每个点满足要求的值(Tarjan算法能够保证求得的scc编号值越大则对应优先级越高)
    此时的遍历是在缩点后的图上进行的,会把一个scc等效为一个点,这样做带来的一个疑惑是一个scc中那么多点对其它scc内的点的更新结果都是一样的吗?为何可以用一个点等效一个scc的所有点
    原因在于此时可以保证任意一个scc内边权均为0(若存在非0边权说明无解),因此对于scca中的点p1, p2sccb中的点q1,用p1更新q1和用p2更新q1得到的结果是相同的,因此可以用scca等效其中的所有点
  4. 遍历缩点后图中所有scc,累计求和即可(注意求和是需要对所有点求和,需要用scc的值*scc中点的个数)

代码实现

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <stack>

using namespace std;
using LL = long long;

const int N = 1e5 + 10, M = 6 * N; // 题目给定边数最多=2*N,从0出发的边数=N,需要建2次图,共6*N

int n, m;
int h[N], hs[N], e[M], ne[M], w[M], idx;
stack<int> stk;
bool in_stk[N];
int id[N], Size[N], scc_cnt;
int dfn[N], low[N], timestamp;
int dis[N];

void add(int *h, int a, int b, int c)
{
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx ++;
}
void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    stk.push(u); in_stk[u] = true;

    for (int i = h[u]; ~i; i = ne[i])
    {
        int p = e[i];
        if (!dfn[p])
        {
            tarjan(p);
            low[u] = min(low[u], low[p]);
        }
        else if (in_stk[p]) low[u] = min(low[u], dfn[p]);
    }

    if (dfn[u] == low[u])
    {
        int y;
        ++ scc_cnt;
        do {
            y = stk.top(); stk.pop();
            id[y] = scc_cnt;
            in_stk[y] = false;
            ++ Size[scc_cnt];
        } while (y != u);
    }

}
int main()
{
    memset(h, -1, sizeof h);
    memset(hs, -1, sizeof hs);
    
    cin >> n >> m;
    // 第一次建图
    for (int i = 0; i < m; ++ i)
    {
        int t, a, b;
        cin >> t >> a >> b;

        if (t == 1) add(h, a, b, 0), add(h, b, a, 0);
        else if (t == 2) add(h, a, b, 1);
        else if (t == 3) add(h, b, a, 0);
        else if (t == 4) add(h, b, a, 1);
        else add(h, a, b, 0);
    }
    for (int i = 1; i <= n; ++ i) add(h, 0, i, 1);

    // for (int i = 0; i <= n; ++ i)
    //     if (!dfn(i)) tarjan(i);
    // 本题中,0号点为超级源点,从该点出发即可走到所有点,因此从0号点开始tarjan即可
    tarjan(0);
    
    bool flag = true;
    for (int i = 0; i <= n; ++ i)
    {
        for (int j = h[i]; ~j; j = ne[j])
        {
            int p = e[j];
            int a = id[i], b = id[p];
            // 进行有无可行性解的验证,验证同一scc中是否存在正权边
            if (a == b)
            {
                if (w[j] > 0)
                {
                    flag = false;
                    break;
                }
            }
            else
                add(hs, a, b, w[j]); // 所求并非方案数,因此可以建立重边,无需判重
        }
        if (!flag) break;
    }

    if (!flag) cout << -1 << endl;
    else 
    {
        // 递推求解每个点符合要求的最小值
        for (int i = scc_cnt; i >= 1; -- i)
            for (int j = hs[i]; ~j; j = ne[j])
            {
                int p = e[j];
                dis[p] = max(dis[p], dis[i] + w[j]);
            }
        
        LL sum = 0;
        for (int i = 1; i <= scc_cnt; ++ i) sum += (LL)dis[i] * Size[i]; // 这里的点是一个个scc,统计要计算的所有点,因此需要*Size[i]

        cout << sum << endl;
    }
    return 0;
}
posted @   0x7F  阅读(280)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示