zoukankan      html  css  js  c++  java
  • 差分约束

    差分约束概念

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

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

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

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

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

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

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

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

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

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

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

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

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

    步骤:

    1. 先将每个不等式(x_i <= x_j + c_k),转化成一条从(x_j)走到(x_i),长度为(c_k)的一条边
    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. (0 leq s_i - s_{i - 1} leq num[i] , 1 leq i leq 24)

    2. (i geq 8 , s_i - s_{i - 8} geq r_i)

      (0 < i < 7 , s_i + s_{24} - s_{i + 16} geq r_i)

    推导后可得

    1. (s_i geq s_{i - 1} + 0)

    2. (s_{i - 1} geq s_i - num[i])

    3. (i geq 8 , s_i geq s_{i - 8} + r_i)

    4. (0 < i < 7 , s_i geq s_{i + 16} - s_{24} + r_i)

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

    代码实现

    #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边权说明无解),因此对于(scc_a)中的点(p_1), (p_2)(scc_b)中的点(q_1),用(p_1)更新(q_1)和用(p_2)更新(q_1)得到的结果是相同的,因此可以用(scc_a)等效其中的所有点
    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;
    }
    
  • 相关阅读:
    【noip模拟赛10】奇怪的贸易 高精度
    【noip模拟赛8】魔术棋子
    【noip模拟赛7】足球比赛 树
    P2502 [HAOI2006]旅行 并查集
    python发邮件:
    读取excel表格.py
    allure的其他参数
    生成allure测试报告:
    Java
    调用阿里云接口实现短信消息的发送源码——CSDN博客
  • 原文地址:https://www.cnblogs.com/G-H-Y/p/15203996.html
Copyright © 2011-2022 走看看