题解:AcWing 1169 糖果

【题目来源】

AcWing:1169 糖果 - AcWing题库

【题目描述】

幼儿园里有 \(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\)

【输入样例】

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

【输出样例】

11

【核心思想】

  1. 问题分析:给定 \(N\) 个小朋友和 \(K\) 个约束条件,每个约束定义了两个小朋友糖果数量的关系(等于、小于、大于、不少于、不多于)。需要求满足所有约束的最小糖果总数,若无法满足则输出 \(-1\)。这是一个差分约束系统问题,可以转化为图论中的最长路径问题求解。

  2. 算法选择

    • 差分约束系统:将约束条件转化为不等式 \(x_j - x_i \geq c\),构建有向图,求最长路径
    • SPFA 算法求最长路径:使用栈优化版本,检测正环(即差分约束系统无解)
    • 超级源点:添加虚拟源点 \(0\),向所有节点连边权为 \(1\) 的边,确保每个小朋友至少分到 \(1\) 个糖果
    • 计数数组 \(cnt[]\):记录节点被松弛的次数,超过 \(n+1\) 次说明存在正环
  3. 关键步骤

    • 约束转化(将5种约束转化为不等式):
      • \(X=1\)\(a = b\)):\(a \geq b\)\(b \geq a\),添加边 \(b \to a\)(权0)和 \(a \to b\)(权0)
      • \(X=2\)\(a < b\)):\(b \geq a + 1\),添加边 \(a \to b\)(权1)
      • \(X=3\)\(a \geq b\)):\(a \geq b\),添加边 \(b \to a\)(权0)
      • \(X=4\)\(a > b\)):\(a \geq b + 1\),添加边 \(b \to a\)(权1)
      • \(X=5\)\(b \geq a\)):\(b \geq a\),添加边 \(a \to b\)(权0)
    • 添加超级源点:对于 \(i\)\(1\)\(n\),添加边 \(0 \to i\)(权1),确保 \(dist[i] \geq 1\)
    • SPFA 求最长路径
      • 初始化 \(dist[i] = -INF\)\(dist[0] = 0\)
      • 使用栈存储待松弛节点,超级源点 \(0\) 入栈
      • 当栈非空时,取出栈顶节点 \(t\),松弛其所有邻接边
      • \(dist[j] < dist[t] + w\),更新 \(dist[j]\)\(cnt[j] = cnt[t] + 1\)
      • \(cnt[j] \geq n+1\),说明存在正环,返回无解
    • 计算答案:若存在正环输出 \(-1\),否则输出 \(\sum_{i=1}^{n} dist[i]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot m)\),SPFA 最坏情况,其中 \(m\) 为约束转化后的边数(最多 \(2K + n\) 条边)
    • 空间复杂度:\(O(n + m)\),邻接表存储图
    • 适用于差分约束系统、不等式组求解问题
  5. 差分约束系统的核心思想

    • 不等式转化:将约束条件统一转化为 \(x_j - x_i \geq c\) 的形式,对应图中的边 \(i \to j\)(权 \(c\)
    • 最长路径建模:求满足所有不等式的最小解,等价于求从超级源点出发的最长路径
    • 正环检测:若图中存在正环,则对应的不等式组无解(可以无限增大)
    • 超级源点技巧:通过添加虚拟源点,将多个起点问题转化为单源问题
    • 最小解性质:最长路径的解是满足所有约束的最小解
    • 适用于不等式组求解、资源分配、调度问题等场景

【解题思路】

【算法标签】

差分约束

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型
const int N = 100005, M = N * 3;  // 最大顶点数和边数
int n, m;  // n: 顶点数, m: 约束数量
int h[N], e[M], ne[M], w[M], idx;  // 链式前向星存储图
int q[N], dist[N];  // 队列和距离数组
int cnt[N], st[N];  // 松弛计数和顶点是否在栈中

/**
 * 添加有向边
 * @param a 起点
 * @param b 终点
 * @param c 权重
 */
void add(int a, int b, int c)
{
    e[idx] = b;        // 边指向的顶点
    w[idx] = c;        // 边的权重
    ne[idx] = h[a];    // 指向原链表头
    h[a] = idx++;      // 更新头指针
}

/**
 * SPFA算法求最长路径,并检测正环
 * 使用栈实现,适合检测正环
 * @return 存在正环返回false,否则返回true
 */
bool spfa()
{
    stack<int> q;  // 使用栈而不是队列,更易检测正环

    // 初始化距离为负无穷
    memset(dist, -0x3f, sizeof(dist));
    dist[0] = 0;  // 超级源点距离为0

    q.push(0);    // 超级源点入栈
    st[0] = true; // 标记在栈中

    while (!q.empty())
    {
        int t = q.top();  // 取出栈顶
        q.pop();
        st[t] = false;    // 标记不在栈中

        // 遍历t的所有邻接边
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];  // 邻接顶点

            // 松弛操作:求最长路径
            if (dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];  // 更新最长距离
                cnt[j] = cnt[t] + 1;       // 松弛次数+1

                // 如果顶点j被松弛了n+1次,说明存在正环
                if (cnt[j] >= n + 1)  // 注意是n+1,因为有超级源点
                {
                    return false;  // 存在正环
                }

                // 如果j不在栈中,入栈
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return true;  // 不存在正环
}

signed main()  // 因为使用了#define int long long
{
    // 输入顶点数和约束数量
    cin >> n >> m;

    // 初始化邻接表
    memset(h, -1, sizeof(h));

    // 处理m个约束
    while (m--)
    {
        int x, a, b;
        cin >> x >> a >> b;

        // 根据约束类型添加边
        if (x == 1)  // a = b
        {
            add(b, a, 0);  // a ≥ b
            add(a, b, 0);  // b ≥ a
        }
        else if (x == 2)  // a < b
        {
            add(a, b, 1);  // b ≥ a + 1
        }
        else if (x == 3)  // a ≥ b
        {
            add(b, a, 0);  // a ≥ b
        }
        else if (x == 4)  // a > b
        {
            add(b, a, 1);  // a ≥ b + 1
        }
        else  // x == 5: b ≥ a
        {
            add(a, b, 0);  // b ≥ a
        }
    }

    // 添加超级源点到所有顶点的边
    for (int i = 1; i <= n; i++)
    {
        add(0, i, 1);  // 每个顶点至少分配1
    }

    // 执行SPFA,检测是否存在正环
    if (!spfa())
    {
        puts("-1");  // 存在正环,无解
    }
    else
    {
        // 计算所有顶点的距离和
        int res = 0;
        for (int i = 1; i <= n; i++)
        {
            res += dist[i];
        }
        cout << res << endl;
    }

    return 0;
}

【运行结果】

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1
11
posted @ 2026-06-16 10:13  团爸讲算法  阅读(6)  评论(0)    收藏  举报