## [$AcWing$ $1169$ 分糖果](https://www.acwing.com/problem/content/1171/) ### 一、题目描述 幼儿园里有 $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$ 输入数据完全随机。 **输入样例**: ```cpp {.line-numbers} 5 7 1 1 2 2 3 2 4 4 1 3 4 5 5 4 5 2 3 5 4 5 1 ``` **输出样例**: ```cpp {.line-numbers} 11 ``` ### 二、题目解析 本题考查 **差分约束**。 如果一个系统由$n$个变量和$m$个约束条件组成,其中每个约束条件形如$x_j-x_i<=b(i,j∈[1,n],k∈[1,m])$,则称为 **差分约束系统**。亦即,差分约束系统是 **求解关于一组变量的特殊不等式组的方法**。简而言之,差分约束就是用来 **求解一种特殊的不等式组**: 这种不等式组里面不等式的格式都是类似于$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$,所有与之相邻的点($u_1,u_2,u_3,...$)看作$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 + 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$的矛盾结论了。 > **小结:求最长路时存在正环,说明差分约束问题无解** #### 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$ ```cpp {.line-numbers} #include 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=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$版本,挂了最后一个点 ```cpp {.line-numbers} #include 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 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=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$ ```cpp {.line-numbers} #include using namespace std; typedef long long LL; const int N = 100010, M = 300010; stack 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 > 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; } ```