AcWing 1169 分糖果

\(AcWing\) \(1169\) 分糖果

一、题目描述

幼儿园里有 \(N\) 个小朋友,老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。

但是小朋友们也有 嫉妒心,总是会提出一些要求,比如:小明不希望小红分到的糖果比他的多,于是在分配糖果的时候, 老师需要满足小朋友们的 \(K\) 个要求。

幼儿园的糖果总是有限的,老师想知道他 至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。

输入格式
输入的第一行是两个整数 \(N,K\)

接下来 \(K\) 行,表示分配糖果时需要满足的关系,每行 \(3\) 个数字 \(X,A,B\)

如果 \(X=1\).表示第 \(A\) 个小朋友分到的糖果必须和第 \(B\) 个小朋友分到的糖果一样多。
如果 \(X=2\),表示第 \(A\) 个小朋友分到的糖果必须少于第 \(B\) 个小朋友分到的糖果。
如果 \(X=3\),表示第 \(A\) 个小朋友分到的糖果必须不少于第 \(B\) 个小朋友分到的糖果。
如果 \(X=4\),表示第 \(A\) 个小朋友分到的糖果必须多于第 \(B\) 个小朋友分到的糖果。
如果 \(X=5\),表示第 \(A\) 个小朋友分到的糖果必须不多于第 \(B\) 个小朋友分到的糖果。
小朋友编号从 \(1\)\(N\)

输出格式
输出一行,表示老师 至少 需要准备的糖果数,如果不能满足小朋友们的所有要求,就输出 \(−1\)

数据范围
\(1≤N<105\),
\(1≤K≤105\),
\(1≤X≤5\),
\(1≤A,B≤N\)
输入数据完全随机。

输入样例

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1

输出样例

11

二、题目解析

本题考查 差分约束

如果一个系统由\(n\)个变量和\(m\)个约束条件组成,其中每个约束条件形如\(x_j-x_i<=b\),则称为 差分约束系统。亦即,差分约束系统是 求解关于一组变量的特殊不等式组的方法。简而言之,差分约束就是用来 求解一种特殊的不等式组:

这种不等式组里面不等式的格式都是类似于\(X_a <= X_b + c\)这种形式。

差分约束问题可以用求单源最短路的算法来求解,一般使用\(spfa\)算法求解。

1.差分约束问题与最短路问题的联系

在求解 最短路 的算法中,核心的语句就是松弛操作,即\(dist[v] > dist[u] + w[i]\),就可以更新\(dist[v]\)\(dist[u] + w[i]\)了。在松弛某点\(v\)的距离的时候,会考察所有可以到达\(v\)的相邻点,每次从\(v\)的相邻点尝试去松弛\(v\)之后,都会有\(dist[v] <= dist[u] + w[i]\),所以最终最短路径上\(dist[v] = min(dist[u] + w[i])\)。如果将\(dist[v]\)看作\(X_a\),所有与之相邻的点看作\(X_b\),则求完最短路径后的\(\{X_a,X_b\}\)的值一定满足形如\(X_a <= X_b + c\)的不等式组的要求,也就是说,可以用\(spfa\)求最短路的过程来求解差分约束问题

2.不等式组解的最小值与最大值

在求 最短路 的过程中,如果起点\(s\)的距离设为\(0\),设从起点到某点\(t\)的路径上依次经过\(x_1,x_2,x_3\)三个点,对应的边权依次是\(w_1,w_2,w_3\)。则有\(x_1 <= s + w_1,x_2 <= x_1 + w_2,x_3 <= x_2 + w_3\),可以推出\(x_3 <= x_2 + w_3 <= x_1 + w_2 + w_3 <= s + w_1 + w_2 + w_3\)\(s\)在这里是定值,可以看出\(x_3\)的值不会超过\(s + w_1 + w_2 + w_3\),也就是可以找到\(x_3\)最大值,但是不一定能够知道\(x_3\)的最小值,其他变量\(x_1\)\(x_2\)也均有个上限,所以 求最短路得到的解实际上是不等式组的最大解。当然,在松弛过程中\(dist[v] = min(dist[u] + w[i])\),也就是为了满足所有的约束条件,\(dist[v]\)实际上是取 所有松弛结果的最小值 的。

再来考察求最长路的过程,将求最短路的松弛条件 倒过来 就可以求最长路了,即\(dist[v] < dist[u] + w[i]\)时,就更新\(dist[v]\)\(dist[u] + w[i]\),最后\(dist[v] = max(dist[i] + w[i])\),也就是求完最长路后\(dist[v] >= dist[u] + w[i]\)

同样的如果起点\(s\)的距离设为\(0\),设从起点到某点\(t\)的路径上依次经过\(x_1,x_2,x_3\)三个点,对应的边权依次是\(w_1,w_2,w_3\)。则有\(x_1 >= s + w_1,x_2 >= x_1 + w_2,x_3 >= x_2 + w_3\),可以推出\(x_3 >= x_2 + w_3 >= x_1 + w_2 + w_3 >= s + w_1 + w_2 + w_3\)\(s\)在这里是定值,可以看出\(x_3\)的值不会小于\(s + w_1 + w_2 + w_3\),这样就求出了\(x_3\)最小值 了,也就是说 最长路可以求出不等式组的最小解

注意:
最短路 算法求解的是形如\(x_a <= x_b + c\)这种形式的不等式
最长路 算法求解的是形如\(x_a >= x_b + c\)这种形式的不等式

3.无约束的变量与无解情况

假设图中有一点是孤立的,与其他点没有关联边,则对应差分约束问题中的变量就是不受约束的,可以取任意值

再来考察无解的情况:

  • 如果求最短路时存在负环,假设负环上的点有\(x_1,x_2,x_3\),则有\(x_2 <= x_1 + w_1\)\(x_3 <= x_2 + w_2,x_1 <= x_3 + w_3\),可以推出\(x_1 <= x_3 + w_3 <= x_2 + w_2 + w_3 <= x_1 + w_1 + w_2 + w_3\),又\(w_1 + w_2 + w_3 < 0\)(存在负环),所以\(x_1 <= x_1 + w_1 + w_2 + w_3 < x_1\)得出了\(x_1 < x_1\)的矛盾结论了,

小结:求最短路时存在负环,说明差分约束问题无解

  • 求最长路时如果存在正环,设正环上的点有\(x_1,x_2,x_3\),则有\(x_2 >= x_1 + c_1,x_3 >= x_2 + c_2,x_1 >= x_3 + c_3\),可以推出\(x_1 >= x_3 + c_3 >= x_2 + c_2 + c_3 >= x_1 + c_1 + c_2 + c_3\),又\(c_1 + c_2 + c_3 > 0\)(存在正环),所以\(x_1 >= x_1 + c_1 + c_2 + c_3 > x_1\)得出了\(x_1 > x_1\)的矛盾结论了。

小结:求最长路时存在正环,说明差分约束问题无解

4.差分约束问题的建图

下面将不等式组转化为图:

建图的过程 深刻的反映 了求最短路、最长路与差分约束问题的关联。比如\(x_1 <= x_2 + 1\),是建一条\(x_2\)\(x_1\)长度为\(1\)的边,还是建一条\(x_1\)\(x_2\)长度为\(-1\)的边呢?在最短路问题中,我们需要\(x_1 <= x_2 + c\)这种形式的不等式,遇见\(x_1 >= x_2 + 1\)形式的不等式就转化为了\(x_2 <= x_1 - 1\),从而建立了\(x_1\)\(x_2\)长度为\(-1\)的边。而在最长路问题中,遇见\(x_1 >= x_2 + 1\)可以建一条\(x_2\)\(x_1\)长度为\(1\)的边,遇见\(x_1 <= x_2 + 1\)这种形式的不等式可以转化为\(x_2 >= x_1 - 1\),建立起了\(x_1\)\(x_2\)长度为\(-1\)的边。从而得出了一个 重要结论

同一个不等式在最长路和最短路问题中建图的方向是相反的,建立的边权互为相反数

比如\(x_1 <= x_2 + 1\),最短路问题是建一条\(x_2\)\(x_1\)长度为\(1\)的边,而在最长路问题中则是建一条\(x_1\)\(x_2\)长度为\(-1\)的边(\(x_2 >= x_1 - 1\))。

其他类型的不等式:

  • 对于\(x_1 < x_2 + c\)形式的不等式,可以转化为\(x_1 <= x_2 + c - 1\)形式
  • 对于\(x_1 = x_2\)形式的不等式,可以转化为\(x_1 <= x_2\)\(x_2 <= x_1\)两个不等式

由于建的图不一定连通,所以为了保证从起点出发一定能到达所有点,一般会建一个超级源点,从这个超级源点向各个点引一条长度为\(0\)的边,即在不等式组中加上了\(x_0 <= x_1,x_0 <= x_2,...,x_0 <=x_n\)这么多不等式。

5.回归本题

由于每个小朋友都需要分到糖,所以某个变量都不能小于\(1\),所以建图时可以由\(x_0\)向各点引一条长度为\(1\)的边,因为要求\(x_1 >= 1\)等价于\(x_1 >= x_0 + 1,x_0 = 0\)本题是求差分约束的最小解,所以需要求最长路

\(spfa\)算法求 最长路时需要将距离数组初始化为负无穷,当存在正环时存在某个点会一直被更新,所以更新超过一定次数后就说明无解。

坑点:本题的\(spfa\)使用队列会超时,可以用数组模拟队列,或者用栈替换队列。

6.答疑解惑

\(SPFA\)算法中,通常使用队列(queue)作为广度优先搜索的数据结构来遍历图的顶点。然而,有时候将队列替换为栈(stack)可以提升算法的性能。

原理如下:当使用队列时,\(SPFA\)算法采用的是一种宽度优先搜索(\(BFS\))的策略,每次从队列头部取出一个顶点进行处理。这会导致算法以层级的方式逐步扩展搜索范围,从而保证了找到的路径是最短路径。

相比之下,如果将队列替换为栈,\(SPFA\)算法就会采用深度优先搜索(\(DFS\))的策略。深度优先搜索并不关心当前处理的顶点与起始点的距离,而是一直深入到无法继续深入为止,然后回溯到上一个顶点进行处理。

使用栈而不是队列的主要好处在于,深度优先搜索会更快地遍历图的分支,而不是逐层扩展搜索范围。在某些情况下,这可能会导致更早地发现最短路径,从而提高算法的性能。

然而,需要注意的是,由于深度优先搜索不保证找到的路径是最短路径,所以在使用栈替代队列时,\(SPFA\)算法可能会得到非最短路径的结果。因此,如果对于问题的解必须是最短路径,使用队列作为搜索数据结构是更可靠的选择。

数组模拟队列版本,可以\(AC\)

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int q[N], cnt[N];
LL d[N];
bool st[N];
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool spfa() {
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i++) {
        q[++tt] = i;
        st[i] = true;
        d[i] = 1;
    }

    while (hh <= tt) {
        int u = q[hh++];
        st[u] = false;

        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (d[v] < d[u] + w[i]) {
                d[v] = d[u] + w[i];
                cnt[v] = cnt[u] + 1;
                if (cnt[v] >= n) return true;
                if (!st[v]) {
                    q[++tt] = v, st[v] = true;
                }
            }
        }
    }
    return false;
}
int main() {
    // 最小值最长路,i->j,dj>=di+c

    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); // A=B,A>=B,B>=A
        if (x == 2) add(a, b, 1);               // A<B,B-1>=A
        if (x == 3) add(b, a, 0);               // A>=B,B->A
        if (x == 4) add(b, a, 1);               // A>B,A>=B+1,B->A
        if (x == 5) add(a, b, 0);               // A<=B,B>=A,A->B
    }

    if (spfa())
        puts("-1");
    else {
        LL res = 0;
        for (int i = 1; i <= n; i++) res += d[i];
        printf("%lld\n", res);
    }

    return 0;
}

\(Queue\)版本,挂了最后一个点

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;
int n, m;

int cnt[N];
LL dist[N];
bool st[N];

int h[N], e[M], w[M], ne[M], idx;
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool spfa() {
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        dist[i] = 1;
        st[i] = true;
        q.push(i);
    }

    while (q.size()) {
        int u = q.front();
        q.pop();

        st[u] = false;

        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[u] + w[i]) {
                dist[j] = dist[u] + w[i];
                cnt[j] = cnt[u] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]) {
                    q.push(j);
                    st[j] = 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); // A=B,A>=B,B>=A
        if (x == 2) add(a, b, 1);               // A<B,B-1>=A
        if (x == 3) add(b, a, 0);               // A>=B,B->A
        if (x == 4) add(b, a, 1);               // A>B,A>=B+1,B->A
        if (x == 5) add(a, b, 0);               // A<=B,B>=A,A->B
    }

    if (spfa())
        puts("-1");
    else {
        LL res = 0;
        for (int i = 1; i <= n; i++) res += dist[i];
        printf("%lld\n", res);
    }
    return 0;
}

用栈模拟队列 ,可以\(AC\)

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;

stack<int> q; // 有时候换成栈判断环很快就能找到答案
LL dist[N];
bool st[N];
int cnt[N];
int n, m; // 表示点数和边数
// 邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

bool spfa() {                         // 最长路,判正环
    memset(dist, -0x3f, sizeof dist); // 最长路,需要初始化为-0x3f

    // 超级源点出发
    dist[0] = 0;
    q.push(0);
    st[0] = 1;

    while (q.size()) {
        int u = q.top();
        q.pop();
        st[u] = 0;

        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (dist[v] < dist[u] + w[i]) { // 求最长路
                dist[v] = dist[u] + w[i];
                cnt[v] = cnt[u] + 1;
                // 注意多加了超级源点,共n+1个节点,边数最多是n
                if (cnt[v] > n) return 1;
                if (!st[v]) {
                    q.push(v);
                    st[v] = 1;
                }
            }
        }
    }
    return 0;
}

int main() {
    scanf("%d %d", &n, &m);
    memset(h, -1, sizeof h);
    while (m--) {
        int x, a, b; // X为大小关系描述
        scanf("%d %d %d", &x, &a, &b);

        // 求最小值,需要最长路,即形如: a>=b+1这样形式的式子

        // 表示第 a 个小朋友分到的糖果必须和第 b 个小朋友分到的糖果一样多
        //  a == b  =>  (a >= b , b >= a)  相等的关系,需要传入两条对称的边,即 a>=b,b>=a,边长为0
        if (x == 1) add(a, b, 0), add(b, a, 0);
        // 表示第 A 个小朋友分到的糖果必须少于第 B 个小朋友分到的糖果
        //  a<b => b > a => b >= a+1
        if (x == 2) add(a, b, 1);
        // 表示第 a 个小朋友分到的糖果必须不少于第 b 个小朋友分到的糖果
        // 不少于即大于等于 a >= b
        if (x == 3) add(b, a, 0);
        // 表示第 a 个小朋友分到的糖果必须多于第 b 个小朋友分到的糖果
        //  a>b => a >= b+1
        if (x == 4) add(b, a, 1);
        // 表示第 a 个小朋友分到的糖果必须不多于第 b 个小朋友分到的糖果。
        //  a<=b => b>=a
        if (x == 5) add(a, b, 0);
    }

    // 重点:需要自己从题意中挖掘出一个性质,即 每个小朋友都要至少一个糖果
    //  xi >= 1  => xi >= x0 + 1
    // 将所有节点与超级源点x0相连
    for (int i = 1; i <= n; i++) add(0, i, 1);

    if (spfa())
        puts("-1");
    else {
        LL res = 0;
        for (int i = 1; i <= n; i++) res += dist[i]; // 累加所有最小值的和
        printf("%lld\n", res);
    }
    return 0;
}
posted @ 2022-03-26 15:11  糖豆爸爸  阅读(95)  评论(0编辑  收藏  举报
Live2D