差分约束的大概样子 (大概)
差分约束一般是由特殊的n元一次不等式组组成的,它包含N个变量X1-Xn和M个约束条件,而且每个约束条件都是由两个变量做差组成的,形如,其中的ck是常数,它需要我们找到一组解使得每个不等式都成立。
关键算法:图的基本算法,不等式,SPFA求负环和最短(长)路。
具体请看下面的例题:
目录
题目 AcWing-2128 狡猾的商人 (等式转化为不等式)
题目 AcWing-362 区间 (模板)
给定 个区间 和 个整数 。
你需要构造一个整数集合 ,使得 , 中满足 的整数 不少于 个。
求这样的整数集合 最少包含多少个数。
思路
首先,这应该是差分约束的模板题,AcWing的题解区已经提供了非常多的高质量优秀解法(不限于差分约束解法),所以我就说明一下解题步骤好了:(s数组表示前缀和)
1.首先我们先想个办法把原始题目搞成差分的形式:
让区间内的整数个数不少于个 ——>
这样我们看到了差分约束的模型。
2.差分约束比较考验我们寻找不等式的能力,我们还需要从题目中挖掘出以下两个不等式:
,区间中被选择的数字只能增加不能减少;
,对于每一个数,我们只能选择它一次。
3.现在我们需要把不等式整理为固定的形式:目标主要是第三个不等式,因为它不等号方向与其他两个不一致:(目标不等式范例 )
4.建立一个有向图,并在图中连接以下与上面提到的不等式相对应的边:
权值为 ;(根据题目提供的数据建立边)
权值为 0;(从 s[0] 到 s[50000] )
权值为 -1;(从 s[0] 到 s[50000] )
5.使用SPFA求解图中单源最长路,求到s[50000]的值,这个值就是答案。
代码
#include <bits/stdc++.h>
using namespace std;
#define Rint register int //just for fun
#define NINF 0xCF
#define MAXN 100005
int n;
int head[MAXN], ver[2*MAXN], edge[2*MAXN], Next[2*MAXN], tot;
void g_add (int x, int y, int z) {
ver[++tot]=y; edge[tot]=z; Next[tot]=head[x]; head[x]=tot;
}
int dist[MAXN];
bool vis[MAXN];
//这里没有使用cnt数组来计算是否有正环,因为不需要,具体原因请去AcWing的题解中寻找智慧
void SPFA () { //一个平淡无奇的SPFA算法
queue<int>que;
memset(dist, NINF, sizeof(dist));
//目标是最长路,如果图中有正环则无解
que.push(0); //从实际的-1开始求单源最长路
vis[0] = 1;
dist[0] = 0;
while (que.size()) {
int t = que.front(); que.pop();
vis[t] = 0;
for (Rint i=head[t], j; i; i=Next[i]) {
j = ver[i];
if (dist[j] < dist[t] + edge[i]) {
//上一行中的 < 代表我们求解的不等式形如s[a]-s[b]>=c
//如果不等式形如s[a]-s[b]<=c就将 < 改为 >
dist[j] = dist[t] + edge[i];
if (!vis[j]) {
que.push(j);
vis[j] = 1;
}
}
}
}
}
int main () {
while (scanf("%d", &n) != EOF) {
for (Rint i=1, a, b, c; i<=n; i++) {
scanf("%d %d %d", &a, &b, &c);
a++; b++;
g_add(a-1, b, c); //不等式1
//a可以取值为0,会导致a-1 = -1
//所有数字被+1,来避免从 点-1 向其他点连边
}
for (Rint k=1; k<=50001; k++) {
g_add(k-1, k, 0); //不等式2
g_add(k, k-1, -1); //不等式3
}
SPFA();
printf("%d\n", dist[50001]);
//答案为s[50000],也就是查询0-50000取了几个数,而s[50000]=dist[50001]
}
}
//注意:数组长度应该至少为n的3倍大,以便添加所有需要的边
已经测试,可以通过
题目 AcWing-2128 狡猾的商人 (等式转化为不等式)
刁姹接到一个任务,为税务部门调查一位商人的账本,看看账本是不是伪造的。
账本上记录了 个月以来的收入情况,其中第 个月的收入额为 。
当 大于 时表示这个月盈利 元,当 小于 时表示这个月亏损 元。
所谓一段时间内的总收入,就是这段时间内每个月的收入额的总和。
刁姹的任务是秘密进行的,为了调查商人的账本,她只好跑到商人那里打工。
她趁商人不在时去偷看账本,可是她无法将账本偷出来,每次偷看账本时她都只能看某段时间内账本上记录的收入情况,并且她只能记住这段时间内的总收入。
现在,刁姹总共偷看了 次账本,当然也就记住了 段时间内的总收入,你的任务是根据记住的这些信息来判断账本是不是假的。
思路
根据分析,我们可以发现如果账本是伪造的,那么由差分约束建立的图会出现负环,具体的原因可以去AcWing的题解区寻找智慧。(啊哈,我偷懒了)
1.找找题目中的不等关系
对于每一次偷看得到的结果,有:;
(S[]表示前缀和,t 表示结束时间,s 代表开始时间,v 表示从 s 到 t 的总收入)
2.把上面的等式转化成带有 的形式,形成以下两条不等式:
;
;
3.建立有向图,把不等式塞进去:
权值为 v;
权值为 -v;
4.依旧是跑一次最长路SPFA,看是否有负环。
本题告诉我们,差分约束除了可以处理不等式外,还可以处理等式。
代码
#include <bits/stdc++.h>
using namespace std;
#define Rint register int
#define NINF 0xCF
#define MAXN 2005
int n, m, w;
int s[MAXN];
int head[MAXN], ver[2*MAXN], edge[2*MAXN], Next[2*MAXN], tot;
void g_add (int x, int y, int z) {
ver[++tot]=y; edge[tot]=z; Next[tot]=head[x]; head[x]=tot;
}
int dist[MAXN], cnt[MAXN];
//这次要计算是否出现负环了,所以加上cnt数组
bool vis[MAXN];
bool SPFA () {
queue<int> que;
memset(dist, NINF, sizeof(dist));
memset(cnt, 0, sizeof(cnt));
memset(vis, 0, sizeof(vis));
que.push(0);
dist[0] = 0;
while (que.size()) {
int x = que.front(); que.pop();
vis[x] = 0;
for (Rint i=head[x], y; i; i=Next[i]) {
y = ver[i];
if (dist[y] < dist[x] + edge[i]) {
dist[y] = dist[x] + edge[i];
cnt[y] = cnt[x] + 1;
if (cnt[y] >= n) return 0;
//没有负环的话,cnt[y]最高大概是n-1
if (!vis[y]) {
que.push(y);
vis[y] = 0;
}
}
}
}
return 1;
}
int main () {
scanf("%d", &w);
while (w--) {
scanf("%d %d", &n, &m);
for (Rint i=1, x, y, z; i<=m; i++) {
scanf("%d %d %d", &x, &y, &z);
g_add(x-1, y, z); //不等式1
g_add(y, x-1, -z); //不等式2
//这两个不等式是由一个等式拆分而来的
}
if (SPFA()) printf("true\n");
else printf("false\n");
memset(head, 0, sizeof(head));
tot = 0; //记得初始化
}
}
已经测试,可以通过
题目 AcWing-3265 再卖菜
在一条街上有 个卖菜的商店,按 至 的顺序排成一排,这些商店都卖一种蔬菜。
第一天,每个商店都自己定了一个正整数的价格。
店主们希望自己的菜价和其他商店的一致,第二天,每一家商店都会根据他自己和相邻商店的价格调整自己的价格。
具体的,每家商店都会将第二天的菜价设置为自己和相邻商店第一天菜价的平均值(用去尾法取整)。
注意,编号为 的商店只有一个相邻的商店 ,编号为 的商店只有一个相邻的商店 ,其他编号为 的商店有两个相邻的商店 和 。
给定第二天各个商店的菜价,可能存在不同的符合要求的第一天的菜价,请找到符合要求的第一天菜价中字典序最小的一种。
字典序大小的定义:对于两个不同的价格序列 和 (b1,b2,b3,…,bn),若存在 ,使得 ,且对于所有 ,则认为第一个序列的字典序小于第二个序列。
思路
这个题目的不等式难以寻找,找到不等式后还需某些处理。。。
1.我们现设 a[] 表示第一天,b[] 表示第二天,那么我们可以得到以下3个等式
1. ;
2.;
3.
外加一个特殊条件 ;因为卖价是正整数
2.转化我们写出的不等式,这里我们必须注意向下取整符号对等式的影响
1. ;
2.;
3.;
以上的3个不等式考虑了向下取整的影响,但是仍然不是差分约束的形式
3.把我们找到的不等式变换成差分约束的形式
1. ;
2.;
3.;
4.建立有向图,进行SPFA求最长路求解
代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define Rint register int
#define NINF 0xCF
#define MAXN 505
int n;
int a[MAXN], b[MAXN], s[MAXN];
int head[MAXN], ver[2*MAXN], edge[2*MAXN], Next[2*MAXN], tot;
void g_add (int x, int y, int z) {
ver[++tot]=y; edge[tot]=z; Next[tot]=head[x]; head[x]=tot;
}
int dist[MAXN];
bool vis[MAXN];
void SPFA () {
queue<int>que;
memset(dist, NINF, sizeof(dist)); //目标是最长路
que.push(0);
dist[0] = 0;
while (que.size()) {
int t = que.front(); que.pop();
vis[t] = 0;
for (Rint i=head[t], j; i; i=Next[i]) {
j = ver[i];
if (dist[j] < dist[t] + edge[i]) { //更新为更大的权
dist[j] = dist[t] + edge[i];
if (!vis[j]) { //尽可能少的去尝试
que.push(j);
vis[j] = 1;
}
}
}
}
}
int main () {
while (scanf("%d", &n) != EOF) {
for (Rint i=1; i<=n; i++) {
scanf("%d", &b[i]);
}
for (Rint i=2; i<=n-1; i++) {
g_add(i-2, i+1, 3*b[i]); // 1<i<n 时的情况
g_add(i+1, i-2, -(3*b[i]+2));
}
g_add(0, 2, 2*b[1]); //当i等于1时
g_add(2, 0, -(2*b[1]+1));
g_add(n-2, n, 2*b[n]); //当 i 等于 n 时
g_add(n, n-2, -(2*b[n]+1));
for (Rint i=1; i<=n; i++) g_add(i-1, i, 1);
//商人只能卖出正整数价格,故s[i-1]-s[i]>=1
SPFA(); //肯定没有负环,因为这个题保证一定有解
for (Rint i=1; i<=n; i++) {
if (i > 1) printf(" ");
printf("%d", dist[i]-dist[i-1]);
}
printf("\n");
}
}
已经测试,可以通过
题目 AcWing-393 雇佣收银员
一家超市要每天 小时营业,为了满足营业需求,需要雇佣一大批收银员。
已知不同时间段需要的收银员数量不同,为了能够雇佣尽可能少的人员,从而减少成本,这家超市的经理请你来帮忙出谋划策。
经理为你提供了一个各个时间段收银员最小需求数量的清单 。
表示午夜 到凌晨 的最小需求数量, 表示凌晨 到凌晨 的最小需求数量,以此类推。
一共有 个合格的申请人申请岗位,第 个申请人可以从 时刻开始连续工作 小时。
收银员之间不存在替换,一定会完整地工作 小时,收银台的数量一定足够。
现在给定你收银员的需求清单,请你计算最少需要雇佣多少名收银员。
思路
这题的转化难度是有的,让我们从寻找这个题的答案格式开始,答案应该是 0点到24点所雇佣的总人数之和(,其中a表示某时刻上岗人数)。对于某个时刻,在这个时刻前8小时内上岗的人数的总和应该大于等于这个时刻对收银员的最小需求。(如:);同时因为时间是循环的,所以这个等式也需要可循环。我们可以使用差分数组来代替8个连续的a以表示区间和。转化成如下形式:,到这一步,我们想到可以用差分约束试试。
再看看还有没有其他的条件:
1. 在每一个时刻上岗的人都不能是负数 ;
2. 在每一时刻上岗的人都不能超过来申请的人 (其中mem[]为某一时刻申请的总人数);
3. 同时要注意时间是循环的,所以需要特别处理:(例:)
好的,现在我们有了不等式,那么我们可以看看我们最终需要求解什么问题,同时我们也意识到在上面的 3. 中,出现了3个s[],不能直接连边。结合两个问题我们尝试将s[23]变为我们寻找的方向。因为s[23]代表了最终的答案,即雇佣的人数,也是我们可以直接“尝试”出的量之一,需要注意的是,我所说的尝试表示我们枚举答案,并通过计算其正确性的方式,完成最终答案的求解。具体请看代码。
代码
#include <bits/stdc++.h>
using namespace std;
#define Rint register int
#define NINF 0xCF
#define MAXN 55
#define MAXM 2004
int n, m, ans, TT;
int mem[MAXN], R[MAXM];
int head[MAXM], ver[2*MAXM], edge[2*MAXM], Next[2*MAXM], tot;
void g_add (int x, int y, int z) {
ver[++tot]=y; edge[tot]=z; Next[tot]=head[x]; head[x]=tot;
}
int dist[MAXM], cnt[MAXM];
bool vis[MAXM];
bool SPFA () {
queue<int> que;
memset(dist, NINF, sizeof(dist));
memset(vis, 0, sizeof(vis));
memset(cnt, 0, sizeof(cnt));
que.push(0);
dist[0] = 0;
while (que.size()) {
int x = que.front(); que.pop();
vis[x] = 0;
for (Rint i=head[x], y; i; i=Next[i]) {
y = ver[i];
if (dist[y] < dist[x] + edge[i]) { //更新为更大的权
dist[y] = dist[x] + edge[i];
cnt[y] = cnt[x] + 1;
if (cnt[y] >= 25) return 0; //0~24,总共25个点
if (!vis[y]) {
que.push(y);
vis[y] = 0;
}
}
}
}
return 1; //没有负环,说明这一种情况是正确的
}
//为什么有负环就不是正确答案:
//如果在图上 点8->点7->点6->...->点0 的权值(正数,表示需求) + 点0->点8 的权值(负数,表示供给) < 0
//那么形成了负环
//同时也就表示了在 0时到7时 招募的人数总数不够 8时 的总需求
//所以不是正确答案
int main () {
scanf("%d", &TT);
while (TT--) {
for (Rint i=1; i<=24; i++) scanf("%d", &mem[i]);
scanf("%d", &m);
memset(R, 0, sizeof(R));
for (Rint i=1, x; i<=m; i++) {
scanf("%d", &x);
R[x+1]++; //记录每一个时刻有多少人可以上岗
//为了防止出现读取R[-1]的情况,我们后移整个数组
}
bool hass = 0; //记录是否找到了答案
for (ans=0; ans<=m; ans++) { //当然可以二分,请自行发掘
//枚举答案,尝试雇佣0个到所有员工
//我们从小到大尝试,第一种正确的情况就是最优的
tot = 0;
memset(head, 0, sizeof(head));
g_add(0, 24, ans);
g_add(24, 0, -ans);
//我们要雇佣ans个员工,那么我们s[24]就应该等于ans
for (Rint i=0; i<24; i++)
g_add(i, i+1, 0);
//s[i+1]-s[i]>=0 毕竟我们不能雇佣数量为负数的员工(阴兵)
for (Rint i=1; i<=24; i++)
g_add(i, i-1, -R[i]);
//s[i]-s[i-1]<=R[i] 毕竟我们每个时间可以雇佣的员工的个数最多为R[i]
for (Rint i=0; i<=24; i++) {
if (i <= 16)
g_add(i, i+8, mem[i+8]);
else
g_add(i, i-16, mem[i-16]-ans);
//因为时间是连续的,24点也就是0点,所以应该是环状的
}
//我们要雇佣的员工个数一定得足够不是吗
if (SPFA()) { //寻找是否有负环
printf("%d\n", ans);
hass = 1;
break;
}
}
//如果我们雇佣所有人都不能完成这件事,那么就无解
if (!hass) printf("No Solution\n");
}
}
已经测试,可以通过
核心算法地格式大致的统一了