This commit is contained in:
HuangHai
2025-08-30 18:35:01 +08:00
commit 1f397eca87
6258 changed files with 1487241 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
## [$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])$,则称为 **差分约束系统**。亦即,差分约束系统是 **求解关于一组变量的特殊不等式组的方法**。简而言之,差分约束就是用来 **求解一种特殊的不等式组**:<font color='red' size=4><b> 这种不等式组里面不等式的格式都是类似于$X_a <= X_b + c$这种形式。</b></font>
差分约束问题可以用求单源最短路的算法来求解,一般使用$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 <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$版本,挂了最后一个点
```cpp {.line-numbers}
#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$
```cpp {.line-numbers}
#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;
}
```

View File

@@ -0,0 +1,65 @@
#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 dist[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;
dist[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 (dist[v] < dist[u] + w[i]) {
dist[v] = dist[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 += dist[i];
printf("%lld\n", res);
}
return 0;
}

View File

@@ -0,0 +1,66 @@
#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;
}

View File

@@ -0,0 +1,86 @@
#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;
}

View File

@@ -0,0 +1,84 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 20010, INF = 0x3f3f3f3f;
int n, m1, m2;
int dist[N], cnt[N];
bool st[N];
int h[N], w[M], e[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(int sz) {
// spfa要调用2次,所以每次调用要清空一下st,cnt,dist
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, 0x3f, sizeof dist); // 最短路,初始化为正无穷
queue<int> q;
// 前sz个节点入队列
for (int i = 1; i <= sz; i++) {
dist[i] = 0;
q.push(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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;
if (cnt[v] >= n) return true; // 判负环
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main() {
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
while (m1--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
// 表示奶牛 A 和奶牛 B 至多相隔 L 的距离。
// b-a <=c ==> b < a + c
add(a, b, c);
}
while (m2--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
// 表示奶牛 A 和奶牛 B 至少相隔 D 的距离。
// b - a >=c => a <= b - c
add(b, a, -c);
}
// 奶牛排在队伍中的顺序和它们的编号是相同的
for (int i = 1; i < n; i++) add(i + 1, i, 0); // x_{i+1} - x_i >= 0 => x_i <= x_{i+1}
if (spfa(n))
puts("-1"); // 从n出发找一下负环,如果负环存在则无解
else {
spfa(1); // 如果从1号节点出发,可以成功走到n号节点,则 dist[n]就不会是INF,如果是INF就说明没有走到过 n点。表示在不等式组中不存在 x_n,x_1之间的传递关联关系,即x_n可以随意
if (dist[n] == INF)
puts("-2");
else
printf("%d\n", dist[n]);
}
return 0;
}

View File

@@ -0,0 +1,208 @@
## [$AcWing$ $1170$ 排队布局](https://www.acwing.com/problem/content/1172/)
### 一、题目描述
当排队等候喂食时,奶牛喜欢和它们的朋友站得靠近些。
农夫约翰有 $N$ 头奶牛,编号从 $1$ 到 $N$,沿一条直线站着等候喂食。
奶牛排在队伍中的顺序和它们的编号是相同的。
因为奶牛相当苗条,所以可能有两头或者更多奶牛站在同一位置上。
如果我们想象奶牛是站在一条数轴上的话,允许有两头或更多奶牛拥有相同的横坐标。
一些奶牛相互间存有好感,它们希望两者之间的距离 **不超过** 一个给定的数 $L$。
另一方面,一些奶牛相互间非常反感,它们希望两者间的距离 **不小于** 一个给定的数 $D$。
给出 $M_L$ 条关于两头奶牛间有好感的描述,再给出 $M_D$ 条关于两头奶牛间存有反感的描述。
你的工作是:
- ① 如果不存在满足要求的方案,输出$-1$
- ② 如果 $1$ 号奶牛和 $N$ 号奶牛间的距离可以任意大,输出$-2$
- ③ 计算出在满足所有要求的情况下,$1$ 号奶牛和 $N$ 号奶牛间可能的 **最大距离**
**输入格式**
第一行包含三个整数 $N$,$M_L$,$M_D$。
接下来 $M_L$ 行,每行包含三个正整数 $A,B,L$,表示奶牛 $A$ 和奶牛 $B$ **至多** 相隔 $L$ 的距离。
再接下来 $M_D$ 行,每行包含三个正整数 $A,B,D$,表示奶牛 $A$ 和奶牛 $B$ **至少** 相隔 $D$ 的距离。
**输出格式**
输出一个整数,如果不存在满足要求的方案,输出$-1$;如果 $1$ 号奶牛和 $N$ 号奶牛间的距离可以任意大,输出$-2$;否则,输出在满足所有要求的情况下,$1$ 号奶牛和 $N$ 号奶牛间可能的 **最大距离**。
**数据范围**
$2≤N≤1000,1≤M_L,M_D≤10^4,1≤L,D≤10^6$
**输入样例**:
```cpp {.line-numbers}
4 2 1
1 3 10
2 4 20
2 3 3
```
**输出样例**:
```cpp {.line-numbers}
27
```
### 二、题目解析
本题同样是 **差分约束** 的问题,要求$1$到$n$之间可能的 **最大距离**,这道题使我们更加深刻的理解了差分约束的思想。在[$AcWing$ $1169$ 糖果](https://www.acwing.com/problem/content/1171/) 里,仔细的讲解了差分约束的基本思想,以及 **不等式组**
- **求最大解需要求最短路**
- **求最小解需要求最长路**
这里不等式解的最大最小都是相对而言的。比如$a_2 <= a_1 + 1$,$a_3 <= a_2 + 1$,求最短路和最长路的建图如下图所示:
![](https://img-blog.csdnimg.cn/20201023192521343.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMwMjc3MjM5,size_16,color_FFFFFF,t_70)
- 对于 **最短路** 而言,设起点$a_1 = 0$,求得$a_2$的最大值是$1$,$a_3$的最大值是$2$
- 对于 **最长路** 而言,设起点$a_3 = 0$,求得$a_2$的最小值是$-1$,$a_1$的最小值是$-2$
所以所谓的最值都是相对的,本题求$1$到$n$之间的 **最大距离** 很能够体现这种相对的关系。
下面梳理下 **已知的结论**:
- ① 对于同一个不等式组,最短路和最长路建图是完全不同的,边的大小互为相反数,方向相反
- ② 如果不等式组无解,即没有合法的一组解使得所有的不等式都成立,那么 **建立的最短路图中一定存在负环,建立的最长路的图中一定存在正环**。
- ③ 如果不等式组有解,**最短路求得的是最大解,最长路求得的是最小解**。
要求$1$到$n$之间的距离,不妨设$1$号点的坐标就是$0$,从$0$出发到达终点$n$,要想距离最大,则$n$号点的解就要最大,所以需要求 **最短路**。
> 当然如果将$n$的坐标设置为$0$,也可以通过求最长路来使得$1$到$n$之间的距离最大。
> **疑**:这句话我没有理解上去,不明白为什么
本题的约束条件有三个,
#### $a_{i-1} <= a_i$
> **解释**:农夫约翰有 $N$ 头奶牛,编号从 $1$ 到 $N$,沿一条直线站着等候喂食。
#### $a_i - a_j <= L$
> **解释**:两者之间的距离 **不超过** 一个给定的数 $L$。
#### $a_i - a_j >= D$
> **解释**:两者间的距离 **不小于** 一个给定的数 $D$。
暂且不去分析具体的约束条件,先讨论一个问题,**什么情况下起点到终点的距离可以无限大**。关于这个问题,很多博客仅仅给出了必要条件,比如说从起点到达不了终点,图中的两点间没有可达的路径,没有约束距离就无限大,但这仅仅是必要条件而非充分条件。比如下面的不等式组:
$$
\large \left\{\begin{matrix}
a_1 <= a_2 - 1 & \\
a_2 <= a_3 - 1 &
\end{matrix}\right.
$$
最短路建图如下:
![](https://img-blog.csdnimg.cn/20201023194218359.png)
从$a_3$是可以到达$a_1$的,图中没有孤立的点,$a_3 = 0$时,$a_1$就可以无限小,他们的距离就无限大。这个简单的例子可以看出,**差分约束的不等式组对应的图如果没有环,起点和终点的距离是可以无限大的**。
要想两点间距离有限,**需要图中有一个适当的环**,来约束解的范围。上图的不等式组可以推出$a_1 <= a_3 - 2$,那么我再加上一个不等式约束$a_1 >= a_3 - 4$,,就成功的将$a_1$到$a_3$之间的距离限制在$4$个单位以内了。对应图中的表现不过是$a_1$到$a_3$连一条边权为$4$的边,我们分析此时环的长度$4 - 1 - 1 = 2$是大于$0$的。所以我们可以得出下面的结论:
- ① **最短路的图中如果存在负环,差分约束问题无合法解**
- ② **最短路的图中如果存在正环,正环上任意两点间的距离都是有限的**
- ③ **最短路的图中不存在环,图中任意两点间的距离都可以是无限大的**
> 这个结论可以类比到最长路的图中。
**换一种表达形式的话就是如果要图中的两点间距离有限,需要存在正环,且这两个点都在环上。**
那么本题我们是否需要判断了负环还要判断正环呢?实际上是不需要的。
第一个约束条件$a_i >= a_{i-1}$,构建的最短路图中$a_n$可以到达$a_{n-1}$,$a_{n-1}$可以到达$a_{n-2}$,...。所以$a_n$可以到达每一点,包括$a_1$。我们先以$a_n$为起点,求一遍最短路,如果存在负环,则不等式组无解,如果不存在负环,那么不等式组肯定有解,下面要判断的就是$a_n - a_1$的结果是否可以无限大。在确定了不等式组有解的情况下,我们以$a_1$为起点再求一遍最短路,当然$a_1$不一定能够到达每一点,如果$a_1$到达不了$a_n$,说明不存在一个环,环上同时包含$a_1$到$a_n$。如果$a_1$出发可以到达$a_n$,则一定存在由$a_1、a_n$构成的正环。从$a_1$出发可以到达$a_n$,$a_n$出发可以到达$a_1$,这个条件保证了环的存在。第一次以$a_n$为起点求最短路时不存在负环,说明这个环一定不是负环,那么就是正环了(即使环的长度为$0$也是有解的)。根据我们上一段推出的结论可知,此时$a_1$和$a_n$之间的距离是有限的,并且第二次求最短路过程中$a_1$是起点,求得的$a_n$是最大值,所以此时的$a_n$就是我们要求的最大距离了。
**最后再总结下本题给我们的经验**:
- ① 最短路存在负环,差分约束问题无解
- ② 最短路存在正环,环上任意两点之间距离有限
- ③ 最短路不存在环,任意两点间距离可以是无穷大
---
- ④ 最长路存在正环,差分约束问题无解
- ⑤ 最长路存在负环,环上任意两点之间距离有限
- ⑥ 最长路不存在环,任意两点间距离可以是无穷大
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 20010, INF = 0x3f3f3f3f;
int n, m1, m2;
int dist[N], cnt[N];
bool st[N];
int h[N], w[M], e[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(int sz) {
// spfa要调用2次,所以每次调用要清空一下st,cnt,dist
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, 0x3f, sizeof dist); // 最短路,初始化为正无穷
queue<int> q;
// 前sz个节点入队列
for (int i = 1; i <= sz; i++) {
dist[i] = 0;
q.push(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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;
if (cnt[v] >= n) return true; // 判负环
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main() {
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
while (m1--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
// 表示奶牛 A 和奶牛 B 至多相隔 L 的距离。
// b-a <=c ==> b < a + c
add(a, b, c);
}
while (m2--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
// 表示奶牛 A 和奶牛 B 至少相隔 D 的距离。
// b - a >=c => a <= b - c
add(b, a, -c);
}
// 奶牛排在队伍中的顺序和它们的编号是相同的
for (int i = 1; i < n; i++) add(i + 1, i, 0); // x_{i+1} - x_i >= 0 => x_i <= x_{i+1}
if (spfa(n))
puts("-1"); // 从n出发找一下负环,如果负环存在则无解
else {
spfa(1); // 如果从1号节点出发,可以成功走到n号节点,则 dist[n]就不会是INF,如果是INF就说明没有走到过 n点。表示在不等式组中不存在 x_n,x_1之间的传递关联关系,即x_n可以随意
if (dist[n] == INF)
puts("-2");
else
printf("%d\n", dist[n]);
}
return 0;
}
```

View File

@@ -0,0 +1,61 @@
#include <bits/stdc++.h>
using namespace std;
// 这里也可以把[a,b]区间往右移动一个单位,
// 这样就可以空出来S[0]这个点,作为超级原点
const int N = 50010, M = N * 3 + 10;
int dist[N];
int m; // m个区间,可以理解为m条边
bool st[N];
// 邻接表
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++;
}
void spfa() {
queue<int> q;
// 求最大路径长度
memset(dist, -0x3f, sizeof dist);
dist[0] = 0;
st[0] = true;
q.push(0);
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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];
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
}
int main() {
cin >> m;
memset(h, -1, sizeof h);
/*
① s(i) >= s(i-1) + 0
② s(i-1)>= s(i) - 1
*/
for (int i = 1; i < N; i++) add(i - 1, i, 0), add(i, i - 1, -1);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
a++, b++;
add(a - 1, b, c); // s(b) >= s(a-1) + c
}
spfa();
printf("%d\n", dist[50001]);
return 0;
}

View File

@@ -0,0 +1,117 @@
## [$AcWing$ $362$. 区间](https://www.acwing.com/problem/content/364/)
### 一、题目描述
给定 $n$ 个区间 [$a_i,b_i$] 和 $n$ 个整数 $c_i$。
你需要构造一个整数集合 $Z$,使得 $∀i∈[1,n]$,$Z$ 中满足 $a_i≤x≤b_i$ 的整数 $x$ 不少于 $c_i$ 个。
求这样的整数集合 $Z$ **最少** 包含多少个数。
**输入格式**
第一行包含整数 $n$。
接下来 $n$ 行,每行包含三个整数 $a_i,b_i,c_i$。
**输出格式**
输出一个整数表示结果。
**数据范围**
$1≤n≤50000$,
$0≤a_i,b_i≤50000$,
$0≤c_i≤b_i−a_i+1$
**输入样例**:
```cpp {.line-numbers}
5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1
```
**输出样例**:
```cpp {.line-numbers}
6
```
### 二、题目解析
本题同样考察差分约束,难点在于想到用 **前缀和** 的方法去求解。题目中有两个地方可以想到需要使用前缀和:
- 第一个地方是集合$Z$中在$[a_i,b_i]$中的的$x$不少于$c_i$个,**要求一个集合在若干个区间中数的个数,最快的方法就是使用前缀和**。
- 另一个是只要求集合在给定区间中数的个数不小于$c$,但是这些数的选取并不是唯一的,所以$Z$中具体数的值并不重要,只需要知道在每个区间内$Z$中元素的个数是多少,这里也 **暗示了使用前缀和**。
设$s[i]$表示集合$Z$中有$s[i]$个元素是在$1$到$i$之间的,则在区间$[a,b]$中就存在$s[b] - s[a - 1]$个集合中的元素。
下面将题目条件 **转化为差分约束的条件**,
- ① 前缀和中默认的条件:$s[i - 1] <= s[i]$
- ② 第$i$个元素要么在集合中、要么不在,这意味着$s[i]$至多比$s[i - 1]$多$1$,所以有$s[i-1] >= s[i] - 1$
- ③ 需要加上题目中的约束条件,在$a$到$b$区间中出现的个数不少于$c$个,即$s[b] - s[a - 1] >= c$
需要注意的是$a$和$b$的范围是从$0$开始的,为了在区间左端点等于$0$时使用前缀和数组不发生越界,需要将所有的区间均右移一个单位。最多有$50000$左右个点,上面的三类约束条件都可能出现$50000$来次,所以最多有$150000$左右条边。要想求集合中至少包含多少个数,只需要求$s[50001]$的最小值即可(这里向右偏移了$1$个单位,所以是$50001$),**求不等式组解的最小值自然是求最长路了**。建图时虚拟源点$0$到$1$,$1$到$2$,...都是有连边的,所以所有的点都是可达的。集合$Z$中最坏情况是包含了$50000$以内所有的点,所以一定是有解的。最后补充一句,这里的前缀和$s[i]$在$spfa$算法中也就是距离数组$d$。
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 这里也可以把[a,b]区间往右移动一个单位,
// 这样就可以空出来S[0]这个点,作为超级原点
const int N = 50010, M = N * 3 + 10;
int dist[N];
int m; // m个区间,可以理解为m条边
bool st[N];
// 邻接表
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++;
}
void spfa() {
queue<int> q;
// 求最大路径长度
memset(dist, -0x3f, sizeof dist);
dist[0] = 0;
st[0] = true;
q.push(0);
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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];
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
}
int main() {
cin >> m;
memset(h, -1, sizeof h);
/*
① s(i) >= s(i-1) + 0
② s(i-1)>= s(i) - 1
*/
for (int i = 1; i < N; i++) add(i - 1, i, 0), add(i, i - 1, -1);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
a++, b++;
add(a - 1, b, c); // s(b) >= s(a-1) + c
}
spfa();
printf("%d\n", dist[50001]);
return 0;
}
```

Binary file not shown.

View File

@@ -0,0 +1,360 @@
## [$AcWing$ $393$. 雇佣收银员](https://www.acwing.com/problem/content/395/)
### 一、题目描述
一家超市要每天 $24$ 小时营业,为了满足营业需求,需要雇佣一大批收银员。
已知不同时间段需要的收银员数量不同,为了能够雇佣 **尽可能少** 的人员,从而减少成本,这家超市的经理请你来帮忙出谋划策。
经理为你提供了一个各个时间段收银员 **最小需求数量** 的清单 $R(0),R(1),R(2),…,R(23)$。
$R(0)$ 表示午夜 $00:00$ 到凌晨 $01:00$ 的最小需求数量,$R(1)$ 表示凌晨 $01:00$ 到凌晨 $02:00$ 的最小需求数量,以此类推。
一共有 $N$ 个合格的申请人申请岗位,第 $i$ 个申请人可以从 $t_i$ 时刻开始连续工作 $8$ 小时。
收银员之间不存在替换,一定会完整地工作 $8$ 小时,收银台的数量一定足够。
现在给定你收银员的需求清单,请你计算 **最少需要雇佣** 多少名收银员。
**输入格式**
第一行包含一个不超过 $20$ 的整数,表示测试数据的组数。
对于每组测试数据,第一行包含 $24$ 个整数,分别表示 $R(0),R(1),R(2),…,R(23)$。
第二行包含整数 $N$。
接下来 $N$ 行,每行包含一个整数 $t_i$。
**输出格式**
每组数据输出一个结果,每个结果占一行。
如果没有满足需求的安排,输出 `No Solution`。
**数据范围**
$0≤R(i)≤1000$,
$0≤N≤1000$,
$0≤t_i≤23$
**输入样例**:
```cpp {.line-numbers}
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
5
0
23
22
1
10
```
**输出样例**:
```cpp {.line-numbers}
1
```
### 二、题目分析
因为求解的是 **最小值** 所以需要使用 **最长路** 来求解,对于差分约束的题目 **难点** 在于 **找全** 题目中涉及到的不等式关系。
我们先把 **基础概念** 定义清楚:
* ① $num[i]$ : **$i$点可以来开始投入工作的人数**
* ② $r[i]$: **$i$点 到$i+1$点的最小需求数量**
* ③ $x_i$ : **从$i$点投入工作的人中选择的人数**
> **解释**:
> 1、 $r(0)$ 表示午夜 $00:00$ 到凌晨 $01:00$ 的最小需求数量,$r(1)$ 表示凌晨 $01:00$ 到凌晨 $02:00$ 的最小需求数量,以此类推。
> 2、$i \in [0,23]$
> 3、上面有用中括号括上$i$,有用下标标$i$的,原因是 ① ②是已知的,准备放入数组,而$x_i$是未知的,不想放入数组。
根据题目的描述,可以得到 **不等式关系**:
① $0 <= x_i <= num[i]$
> **解释**:每个时间点$i$,选中的人数,必然小于等于可选的人数。
② $x_{i-7} + x_{i-6} + x_{i-5} + ... +x_i >= R_i$
每一个时刻$i$都需要满足对应的收银员的最小数量。
因为每个员工工作时间最长是$8$小时,那么如果在$i$这个时刻他还在工作岗位上,那么他一定是在最近$8$个小时内上岗的,即$x_{i-7},x_{i-6},...,x_i$上岗的。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230717141618.png)
对于②不是差分约束的 **标准形式**,但是可以发现其实加的是一整段的和所以我们考虑 <font color='red'><b>前缀和</b></font> 来处理,前缀和就需要考虑将$0$这个位置空出来。原本$r[i]$是表示$i$到$i+1$时段至少需要的员工数,$r[0]$也是有实际含义的
,所以将所有的位置都往后移动一位,所以改为用 **区间的右端点表示这段区间**。
- 用$r[i]$表示$i - 1$到$i$时段至少需要的员工数
```cpp {.line-numbers}
for (int i = 1; i <= 24; i++) scanf("%d", &r[i]);
```
- 用$S_i$表示$x_1 + x_2 + ... x_i$,$S_0 = 0$,
然后我们可以使用关于$S_i$的表达式来表示①②:
> **解释**:$x_i$的含义是从$i$点中来的人中选择人数,人员是可以从$0$点来的,所以$x_0$是有效值,可我们需要用到前缀和,前缀和要求下标从$1$开始,所有将所有位置都往后移动一位。
对于①可以得到:
$0 <= S_i - S_{i-1} <= num[i]$
> **解释**:由于照顾前缀和同学,所以$x_i$向后进行了错一位操作,$1 <= i <= 24$
对于②因为是连续工作八小时所以需要分段来看,我们以$8$作为分界线分为两段:
* $S_i - S_{i-8} >= R_i,i >= 8$
* $S_i + S_{24} - S_{i+16} >= R_i,0 < i < 8$
可以找一下规律,凑够八段就行
![](https://img2022.cnblogs.com/blog/8562/202203/8562-20220328155546310-1851603672.png)
因为求解的是 **最小值** 所以使用 **最长路** 求解,也即需要将不等式整理成$a >= b + c$的形式,整理一下上面的不等式得到:
* $S_i >= S_{i-1} + 0$
* $S_{i-1} >= S_i - num[i]$
* $S_i >= S_{i-8} + R_i,i \in [8,24]$
* $S_i >= S_{i+16} - S_{24} + R_i,i \in [1,7]$
我们需要从超级源点$s_0$出发,求出到$s_{24}$的最长路,$s_{24}$就是问题的解。
注意到上面的第三个约束条件不符合差分约束的标准形式,一般的差分约束的不等式是$a >= b + c$,其中$c$是常数,这里却出现了$s_{24}$。
因为我们 **最坏情况** 下就是把$n$个来应聘的都招进来,所以$s_{24}$的最大值只能是$n$。这里$n$的范围不大,图中的点数和边数也少,直接枚举,当从小到大,找到第一个可以使得不等式组有解的$S_{24}$时,就是找到了答案。
> **解释**:不等式组有解,意味着最长路无正环
注意这里对$s_{24}$的枚举就意味着:
$s_{24}$在枚举的时候,它是一个常数,常数的引入,需要再加两个不等式 $s(24)=c$
这两个约束条件。
① $\large s_{24} \geqslant c \Rightarrow s_{24} \geqslant c +s_0 \Rightarrow s_{24} \geqslant s_0 + c$
```cpp {.line-numbers}
add(0, 24, c);
```
② $\large s_{24} \leqslant c \Rightarrow s_{24} \leqslant c +s_0 \Rightarrow s_0 \geqslant s_{24} -c$
```cpp {.line-numbers}
add(24, 0, -c);
```
### 三、枚举
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 30, M = 100;
int n; // n个合格的申请人申请岗位
int r[N]; // 各个时间段需要的人员数量
int num[N]; // num[i]:i点可以来工作的人数
int dist[N]; // 本题是求“最少需要雇佣”,所以是最长路
int cnt[N]; // 用于判正环(最长路)
bool st[N]; // spfa专用是否在队列中的标识
// 邻接表
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++;
}
// 建图
void build(int c) {
// 清空邻接表
memset(h, -1, sizeof h);
idx = 0;
// s(i):从1点到i点,需要雇佣的人员数量
for (int i = 1; i <= 24; i++) {
add(i - 1, i, 0); // s(i) >= s(i-1) + 0
add(i, i - 1, -num[i]); // s(i-1) >= s(i)-num[i]
}
// i>=8 时,s(i) >= s(i-8) + r(i)
for (int i = 8; i <= 24; i++) add(i - 8, i, r[i]);
// 7=>i>=1 s(i)>=s(i+16)−s(24)+r(i)
for (int i = 1; i <= 7; i++) add(i + 16, i, -c + r[i]);
// s24在枚举的时候,它是一个常数,常数的引入,需要再加两个不等式 s(24)=c
add(0, 24, c);
// s(24)>=c -> s(24) >= c +s(0)
// -> s(24) >= s(0) + c
add(24, 0, -c);
// s(24)<=c -> s(24) <= c +s(0)
// -> s(0) >= s(24) -c
}
// spfa找正环
bool spfa(int c) {
build(c); // 建图
// 每次初始化
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, -0x3f, sizeof dist);
queue<int> q;
// 超级源点
for (int i = 0; i <= 24; i++) {
q.push(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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;
// 一共24个点,发现正环了则返回false
if (cnt[v] >= 25) return true;
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
// 各个时间段收银员最小需求数量的清单
// 这里为了使用前缀和,向后进行了错一位操作
for (int i = 1; i <= 24; i++) scanf("%d", &r[i]);
scanf("%d", &n); // n个合格的申请人申请岗位
memset(num, 0, sizeof num); // 多组测试数据,所以需要每次清零
for (int i = 0; i < n; i++) {
int t;
scanf("%d", &t);
// 申请人可以从num[t+1]时刻开始连续工作8小时,++代表这个时段可以干活的人数+1
num[t + 1]++; // 这里使用了偏移量+1存储,为了照顾前缀和同学
}
// 枚举1~1000所有点,找到最小的
bool success = false;
for (int i = 1; i <= 1000; i++) // 看看当前枚举到的i是否使得SPFA有环,有环就是无解,无环就有解
if (!spfa(i)) {
printf("%d\n", i);
success = true;
break;
}
if (!success) puts("No Solution");
}
return 0;
}
```
### 四、二分
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 30, M = 100;
int n; // n个合格的申请人申请岗位
int r[N]; // 各个时间段需要的人员数量
int num[N]; // 第i个申请人可以从num[i]时刻开始连续工作8小时
int dist[N]; // 最长距离,本题是求“最少需要雇佣”,所以是最长路
int cnt[N]; // 用于判正环(最长路)
bool st[N]; // spfa专用是否在队列中的标识
// 邻接表
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++;
}
// 建图
void build(int c) {
// 每次清空邻接表
memset(h, -1, sizeof h);
idx = 0;
// s(i):从1点到i点,需要雇佣的人员数量
for (int i = 1; i <= 24; i++) {
add(i - 1, i, 0); // s(i) >= s(i-1) + 0
add(i, i - 1, -num[i]); // s(i-1) >= s(i)-num[i]
}
// s(i) >= s(i-8) + r(i)
for (int i = 8; i <= 24; i++) add(i - 8, i, r[i]);
// s(i)>=s(i+16)−s(24)+r(i)
for (int i = 1; i <= 7; i++) add(i + 16, i, -c + r[i]);
// s24的引入,需要再加两个不等式 s(24)=c
add(0, 24, c);
// s(24)>=c -> s(24) >= c +s(0)
// -> s(24) >= s(0) + c
add(24, 0, -c);
// s(24)<=c -> s(24) <= c +s(0)
// -> s(0) >= s(24) -c
}
// spfa找正环
bool spfa(int c) {
build(c); // 建图
// 每次初始化
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, -0x3f, sizeof dist);
queue<int> q;
// 超级源点大法好~
for (int i = 0; i <= 24; i++) {
q.push(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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;
// 一共24个点,发现正环了则返回true
if (cnt[v] >= 25) return true;
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
// 各个时间段收银员最小需求数量的清单
// 这里为了使用前缀和,向后进行了错一位操作
for (int i = 1; i <= 24; i++) scanf("%d", &r[i]);
scanf("%d", &n); // n个合格的申请人申请岗位
memset(num, 0, sizeof num); // 多组测试数据,所以需要每次清零
for (int i = 0; i < n; i++) {
int t;
scanf("%d", &t);
// 申请人可以从num[t+1]时刻开始连续工作8小时
num[t + 1]++; //++代表这个时段可以干活的人数+1
}
// 二分总人数
int l = 0, r = n;
// 雇佣的人员,最少是0,最多是1000
// 人员雇佣的越多,肯定越能满足用工要求,但成本会高
// 所以,存在单调性,可以二分
while (l < r) {
int mid = (l + r) >> 1;
if (!spfa(mid)) // 如果不等式组有解,向左逼近
r = mid;
else
l = mid + 1; // 无解向右逼近
}
if (spfa(l)) // 如果最终计算出来的结果还是无解,那就是无解
puts("No Solution");
else
printf("%d\n", l); // 输出最小值
}
return 0;
}
```

View File

@@ -0,0 +1,111 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 30, M = 100;
int n; // n个合格的申请人申请岗位
int r[N]; // 各个时间段需要的人员数量
int num[N]; // 第i个申请人可以从num[i]时刻开始连续工作8小时
int dist[N]; // 最长距离,本题是求“最少需要雇佣”,所以是最长路
int cnt[N]; // 用于判正环(最长路)
bool st[N]; // spfa专用是否在队列中的标识
// 邻接表
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++;
}
// 建图
void build(int c) {
// 每次清空邻接表
memset(h, -1, sizeof h);
idx = 0;
// s(i):从1点到i点,需要雇佣的人员数量
for (int i = 1; i <= 24; i++) {
add(i - 1, i, 0); // s(i) >= s(i-1) + 0
add(i, i - 1, -num[i]); // s(i-1) >= s(i)-num[i]
}
// s(i) >= s(i-8) + r(i)
for (int i = 8; i <= 24; i++) add(i - 8, i, r[i]);
// s(i)>=s(i+16)−s(24)+r(i)
for (int i = 1; i <= 7; i++) add(i + 16, i, -c + r[i]);
// s24的引入,需要再加两个不等式 s(24)=c
add(0, 24, c);
// s(24)>=c -> s(24) >= c +s(0)
// -> s(24) >= s(0) + c
add(24, 0, -c);
// s(24)<=c -> s(24) <= c +s(0)
// -> s(0) >= s(24) -c
}
// spfa找正环
bool spfa(int c) {
build(c); // 建图
// 每次初始化
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, -0x3f, sizeof dist);
queue<int> q;
// 超级源点大法好~
for (int i = 0; i <= 24; i++) {
q.push(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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;
// 一共24个点,发现正环了则返回true
if (cnt[v] >= 25) return true;
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
// 各个时间段收银员最小需求数量的清单
// 这里为了使用前缀和,向后进行了错一位操作
for (int i = 1; i <= 24; i++) scanf("%d", &r[i]);
scanf("%d", &n); // n个合格的申请人申请岗位
memset(num, 0, sizeof num); // 多组测试数据,所以需要每次清零
for (int i = 0; i < n; i++) {
int t;
scanf("%d", &t);
// 申请人可以从num[t+1]时刻开始连续工作8小时
num[t + 1]++; //++代表这个时段可以干活的人数+1
}
// 二分总人数
int l = 0, r = n;
// 雇佣的人员,最少是0,最多是1000
// 人员雇佣的越多,肯定越能满足用工要求,但成本会高
// 所以,存在单调性,可以二分
while (l < r) {
int mid = (l + r) >> 1;
if (!spfa(mid)) // 如果不等式组有解,向左逼近
r = mid;
else
l = mid + 1; // 无解向右逼近
}
if (spfa(l)) // 如果最终计算出来的结果还是无解,那就是无解
puts("No Solution");
else
printf("%d\n", l); // 输出最小值
}
return 0;
}

View File

@@ -0,0 +1,105 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 30, M = 100;
int n; // n个合格的申请人申请岗位
int r[N]; // 各个时间段需要的人员数量
int num[N]; // num[i]:i点可以来工作的人数
int dist[N]; // 本题是求“最少需要雇佣”,所以是最长路
int cnt[N]; // 用于判正环(最长路)
bool st[N]; // spfa专用是否在队列中的标识
// 邻接表
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++;
}
// 建图
void build(int c) {
// 清空邻接表
memset(h, -1, sizeof h);
idx = 0;
// s(i):从1点到i点,需要雇佣的人员数量
for (int i = 1; i <= 24; i++) {
add(i - 1, i, 0); // s(i) >= s(i-1) + 0
add(i, i - 1, -num[i]); // s(i-1) >= s(i)-num[i]
}
// i>=8 时,s(i) >= s(i-8) + r(i)
for (int i = 8; i <= 24; i++) add(i - 8, i, r[i]);
// 7=>i>=1 s(i)>=s(i+16)−s(24)+r(i)
for (int i = 1; i <= 7; i++) add(i + 16, i, -c + r[i]);
// s24在枚举的时候,它是一个常数,常数的引入,需要再加两个不等式 s(24)=c
add(0, 24, c);
// s(24)>=c -> s(24) >= c +s(0)
// -> s(24) >= s(0) + c
add(24, 0, -c);
// s(24)<=c -> s(24) <= c +s(0)
// -> s(0) >= s(24) -c
}
// spfa找正环
bool spfa(int c) {
build(c); // 建图
// 每次初始化
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, -0x3f, sizeof dist);
queue<int> q;
// 超级源点
for (int i = 0; i <= 24; i++) {
q.push(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
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;
// 一共24个点,发现正环了则返回false
if (cnt[v] >= 25) return true;
if (!st[v]) {
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
// 各个时间段收银员最小需求数量的清单
// 这里为了使用前缀和,向后进行了错一位操作
for (int i = 1; i <= 24; i++) scanf("%d", &r[i]);
scanf("%d", &n); // n个合格的申请人申请岗位
memset(num, 0, sizeof num); // 多组测试数据,所以需要每次清零
for (int i = 0; i < n; i++) {
int t;
scanf("%d", &t);
// 申请人可以从num[t+1]时刻开始连续工作8小时,++代表这个时段可以干活的人数+1
num[t + 1]++; // 这里使用了偏移量+1存储,为了照顾前缀和同学
}
// 枚举0~1000所有点,找到最小的
bool success = false;
for (int i = 0; i <= 1000; i++) // 看看当前枚举到的i是否使得SPFA有环,有环就是无解,无环就有解
if (!spfa(i)) {
printf("%d\n", i);
success = true;
break;
}
if (!success) puts("No Solution");
}
return 0;
}