知识点:二分答案、一维差分 | 所属专栏:【洛谷题解】
前置知识:一维差分详解
目录
-
- 一、题目描述:
- 二、题目分析:
-
- [1. 暴力做法](#1. 暴力做法)
- [2. 为什么可以二分订单编号?](#2. 为什么可以二分订单编号?)
- [3. 如何检查前 x 份订单?](#3. 如何检查前 x 份订单?)
- [4. 用样例模拟一遍](#4. 用样例模拟一遍)
- 三、代码实现:
-
- [1. 算法思路:](#1. 算法思路:)
- [2. C++代码实现:](#2. C++代码实现:)
- 四、细节补充:
-
- [1. 为什么 ans 要初始化为 0?](#1. 为什么 ans 要初始化为 0?)
- [2. 为什么最后输出 ans + 1?](#2. 为什么最后输出 ans + 1?)
- [3. 为什么每次都要重新构造差分数组?](#3. 为什么每次都要重新构造差分数组?)
- [4. 为什么使用 long long?](#4. 为什么使用 long long?)
- [5. 剩余数量等于 0 仍然合法](#5. 剩余数量等于 0 仍然合法)
- [6. 复杂度分析](#6. 复杂度分析)
- 五、总结:
一、题目描述:
【题目描述】:
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来 n n n 天的借教室信息,其中第 i i i 天学校有 r i r_i ri 个教室可供租借。共有 m m m 份订单,每份订单用三个正整数描述,分别为 d j , s j , t j d_j,s_j,t_j dj,sj,tj,表示某租借者需要从第 s j s_j sj 天到第 t j t_j tj 天租借教室(包括第 s j s_j sj 天和第 t j t_j tj 天),每天需要租借 d j d_j dj 个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供 d j d_j dj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第 s j s_j sj 天到第 t j t_j tj 天中有至少一天剩余的教室数量不足 d j d_j dj 个。
现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。
【输入格式】:
第一行包含两个正整数 n , m n,m n,m,表示天数和订单的数量。
第二行包含 n n n 个正整数,其中第 i i i 个数为 r i r_i ri,表示第 i i i 天可用于租借的教室数量。
接下来有 m m m 行,每行包含三个正整数 d j , s j , t j d_j,s_j,t_j dj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。天数与订单均用从 1 1 1 开始的整数编号。
【输出格式】:
如果所有订单均可满足,则输出只有一行,包含一个整数 0 0 0。
否则(订单无法完全满足)输出两行,第一行输出一个负整数 − 1 -1 −1,第二行输出需要修改订单的申请人编号。
【输入样例】:
4 3
2 5 4 3
2 1 3
3 2 4
4 2 4
【输出样例】:
-1
2
【说明/提示】:
【输入输出样例说明】
第 1 1 1 份订单满足后, 4 4 4 天剩余的教室数分别为 0 , 3 , 2 , 3 0,3,2,3 0,3,2,3。第 2 2 2 份订单要求第 2 2 2 天到第 4 4 4 天每天提供 3 3 3 个教室,而第 3 3 3 天剩余的教室数为 2 2 2,因此无法满足。分配停止,通知第 2 2 2 个申请人修改订单。
【数据范围】
对于 10 % 10\% 10% 的数据,有 1 ≤ n , m ≤ 10 1\le n,m\le 10 1≤n,m≤10;
对于 30 % 30\% 30% 的数据,有 1 ≤ n , m ≤ 1000 1\le n,m\le 1000 1≤n,m≤1000;
对于 70 % 70\% 70% 的数据,有 1 ≤ n , m ≤ 10 5 1\le n,m\le 10^5 1≤n,m≤105;
对于 100 % 100\% 100% 的数据,有 1 ≤ n , m ≤ 10 6 1\le n,m\le 10^6 1≤n,m≤106, 0 ≤ r i , d j ≤ 10 9 0\le r_i,d_j\le 10^9 0≤ri,dj≤109, 1 ≤ s j ≤ t j ≤ n 1\le s_j\le t_j\le n 1≤sj≤tj≤n。
NOIP 2012 提高组 第二天 第二题
2022.2.20 新增一组 hack 数据
二、题目分析:
这道题需要按照顺序处理订单,找到第一份无法满足的订单。
先看样例:
- 第 1 1 1 份订单需要第 1 1 1 天到第 3 3 3 天每天租借 2 2 2 个教室,可以满足;
- 第 2 2 2 份订单需要第 2 2 2 天到第 4 4 4 天每天租借 3 3 3 个教室,但第 3 3 3 天只剩 2 2 2 个,无法满足。
因此,需要通知第 2 2 2 个申请人修改订单。
1. 暴力做法
最直接的做法是按照顺序处理每份订单:
- 遍历订单涉及的每一天,检查剩余教室是否足够;
- 如果有一天不够,输出当前订单编号;
- 如果每天都足够,再将这段时间的剩余教室数减去对应数量。
一份订单可能涉及 n n n 天, m m m 份订单最坏需要 O ( n m ) O(nm) O(nm)。
本题 n , m n,m n,m 都可能达到 10 6 10^6 106,这样的做法显然太慢。
每份订单都会让一段连续日期的剩余教室数量减少相同的值,这是一个区间减法,可以考虑使用差分。
但题目要求找到第一份无法满足的订单,如果每处理一份订单就还原一次数组,仍然需要大量时间。
因此,我们还需要想办法减少检查次数。
2. 为什么可以二分订单编号?
先考虑这样一个问题:
前 x x x 份订单能否全部满足?
随着 x x x 增大,加入的订单越来越多,每天剩余的教室数量只会减少或不变。
因此:
- 如果前 x x x 份订单可以全部满足,那么更少的订单也一定可以全部满足;
- 如果前 x x x 份订单不能全部满足,那么加入更多订单后,也不可能全部满足。
假设第 k k k 份是第一份无法满足的订单,检查结果如下:
| 检查的订单数量 | 前 1 1 1 份 | ... | 前 k − 1 k-1 k−1 份 | 前 k k k 份 | ... | 前 m m m 份 |
|---|---|---|---|---|---|---|
| 能否全部满足 | 能 | ... | 能 | 不能 | ... | 不能 |
如果第 1 1 1 份订单就无法满足,那么"能"的部分为空;如果全部订单都能满足,那么"不能"的部分为空。
可以看到,检查结果具有单调性。
本文使用二分查找:
最多有多少份订单可以按顺序全部满足?
把这个数量记为 ans:
- 如果
ans = m,说明全部订单都能满足,输出 0 0 0; - 否则,前
ans份订单都能满足,下一份订单ans + 1就是第一份无法满足的订单。
例如,样例中只有前 1 1 1 份订单可以满足,所以 ans = 1,第一份无法满足的订单编号为:
a n s + 1 = 2 ans+1=2 ans+1=2
3. 如何检查前 x 份订单?
现在,问题变成了:
给定 x x x,如何判断前 x x x 份订单能否全部满足?
我们可以从初始的可借教室数量出发,将前 x x x 份订单需要的教室都减去。
如果最后每天的剩余数量都不小于 0 0 0 ,说明前 x x x 份订单可以全部满足;如果某一天的剩余数量小于 0 0 0,说明这一天的教室不够,前 x x x 份订单不能全部满足。
为什么只看最后的剩余数量就可以?
因为每份订单的需求量都不小于 0 0 0:
- 如果减去前 x x x 份订单后,每天剩余数量仍不小于 0 0 0,那么按顺序处理这批订单时,之前各步的剩余数量只会更多,也就都能满足;
- 如果某一天的最终剩余数量小于 0 0 0,说明这批订单在这一天的总需求超过了可借数量,不可能全部满足。
接下来,用差分快速完成这些区间减法。
① 根据初始教室数量构造差分数组。
设 a[i] 表示第 i i i 天初始可借的教室数量,diff[i] 表示它的差分值:
d i f f i = a i − a i − 1 diff_i=a_i-a_{i-1} diffi=ai−ai−1
约定 a 0 = 0 a_0=0 a0=0,代码为:
cpp
for (int i = 1; i <= n; i++) {
diff[i] = a[i] - a[i - 1];
}
② 记录前 x x x 份订单。
第 j j j 份订单需要从第 s j s_j sj 天到第 t j t_j tj 天,每天租借 d j d_j dj 个教室。
也就是让区间 s j , t j s_j,t_j sj,tj 内的剩余数量都减去 d j d_j dj:
cpp
diff[s[j]] -= d[j];
diff[t[j] + 1] += d[j];
注意,这里是区间减法 :起点减去 d j d_j dj,结束位置的下一天加回 d j d_j dj。
③ 累加差分,检查剩余数量。
cpp
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += diff[i];
if (sum < 0) {
return false;
}
}
累加到第 i i i 天时,sum 就表示处理前 x x x 份订单后,第 i i i 天剩余的教室数量。
如果有一天小于 0 0 0,直接返回 false;如果所有天数检查完毕都没有出现负数,就返回 true。
4. 用样例模拟一遍
样例中,每天初始可借的教室数量为:
| 天数 i i i | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
初始数量 a[i] |
2 | 5 | 4 | 3 |
先根据初始数量构造差分数组:
| 天数 i i i | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 计算过程 | 2 − 0 2-0 2−0 | 5 − 2 5-2 5−2 | 4 − 5 4-5 4−5 | 3 − 4 3-4 3−4 |
差分值 diff[i] |
2 | 3 | -1 | -1 |
现在检查前 2 2 2 份订单:
- 第 1 1 1 份:第 1 1 1 天到第 3 3 3 天,每天租借 2 2 2 个;
- 第 2 2 2 份:第 2 2 2 天到第 4 4 4 天,每天租借 3 3 3 个。
在差分数组中记录这两次区间减法:
cpp
// 第 1 份订单
diff[1] -= 2;
diff[4] += 2;
// 第 2 份订单
diff[2] -= 3;
diff[5] += 3;
前 4 4 4 个位置的差分值变为:
| 天数 i i i | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
修改后的 diff[i] |
0 | 0 | -1 | 1 |
| 累加后的剩余数量 | 0 | 0 | -1 | 0 |
第 3 3 3 天的剩余数量为 − 1 -1 −1,说明教室不够,因此前 2 2 2 份订单不能全部满足。
如果只检查前 1 1 1 份订单,剩余数量为 0 , 3 , 2 , 3 0,3,2,3 0,3,2,3,都不小于 0 0 0,因此可以满足。
所以,最多可以满足前 1 1 1 份订单,第一份无法满足的是第 2 2 2 份。
三、代码实现:
1. 算法思路:
根据上面的分析,代码分为三个部分:
- 读入每天的教室数量和所有订单;
- 编写
check(x),用差分判断前 x x x 份订单能否全部满足; - 二分查找最多能满足的订单数量,再根据结果输出答案。
我们根据思路,一步一步来实现。
① 首先读入数据,并保存每份订单:
cpp
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> d[i] >> s[i] >> t[i];
}
二分过程中会多次检查不同数量的订单,所以需要先把所有订单保存下来。
② 编写 check(x)。
每次检查时,都从初始教室数量重新构造差分数组,然后记录前 x x x 份订单:
cpp
bool check(int x) {
for (int i = 1; i <= n; i++) {
diff[i] = a[i] - a[i - 1];
}
for (int i = 1; i <= x; i++) {
diff[s[i]] -= d[i];
diff[t[i] + 1] += d[i];
}
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += diff[i];
if (sum < 0) {
return false;
}
}
return true;
}
这里:
check(x)返回true,表示前 x x x 份订单可以全部满足;check(x)返回false,表示前 x x x 份订单不能全部满足。
③ 二分查找最多能满足的订单数量。
cpp
int l = 1, r = m;
int ans = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (check(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
- 如果前
mid份订单都能满足,先记录ans = mid,再尝试更多订单; - 如果前
mid份订单不能全部满足,就向前查找。
ans 初始化为 0 0 0,表示还没有找到可以全部满足的一批订单。如果第 1 1 1 份订单就无法满足,它会保持为 0 0 0。
最后根据 ans 输出:
cpp
if (ans == m) {
cout << 0 << '\n';
} else {
cout << -1 << '\n' << ans + 1 << '\n';
}
2. C++代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010; // n、m 最大为 10^6
int n, m;
long long a[N]; // 每天初始可借的教室数量
long long d[N], s[N], t[N]; // 订单的数量、开始日期和结束日期
long long diff[N]; // 剩余教室数量的差分数组
// 判断前 x 份订单能否全部满足
bool check(int x) {
// 根据初始教室数量重新构造差分数组
for (int i = 1; i <= n; i++) {
diff[i] = a[i] - a[i - 1];
}
// 记录前 x 份订单的区间减法
for (int i = 1; i <= x; i++) {
diff[s[i]] -= d[i];
diff[t[i] + 1] += d[i];
}
// 还原每天的剩余教室数量
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += diff[i];
if (sum < 0) {
return false;
}
}
return true;
}
int main() {
// ① 读入数据
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> d[i] >> s[i] >> t[i];
}
// ② 二分查找最多能满足的订单数量
int l = 1, r = m;
int ans = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (check(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
// ③ 根据结果输出
if (ans == m) {
cout << 0 << '\n';
} else {
cout << -1 << '\n' << ans + 1 << '\n';
}
return 0;
}
四、细节补充:
1. 为什么 ans 要初始化为 0?
如果第 1 1 1 份订单就无法满足,那么任何包含它的订单前缀都无法全部满足。
此时,二分过程中不会执行:
cpp
ans = mid;
因此需要提前设置:
cpp
int ans = 0;
这样最后输出 ans + 1,得到的就是第 1 1 1 份订单。
如果不初始化 ans,后续使用它会产生未定义行为。
2. 为什么最后输出 ans + 1?
本文查找的是最多能满足的订单数量。
如果前 ans 份订单都能满足,但无法满足更多订单,那么下一份,也就是 ans + 1,就是第一份无法满足的订单。
如果 ans == m,说明所有订单都能满足,此时不能输出 ans + 1,而应该按题目要求输出 0 0 0。
3. 为什么每次都要重新构造差分数组?
二分会多次调用 check,每次检查的订单数量可能不同。
上一次检查已经修改过差分数组,不能直接在它的基础上继续检查,否则不同批次的订单会混在一起。
所以,每次调用 check,都要先根据初始的 a 数组重新构造 diff[1] 到 diff[n]。
4. 为什么使用 long long?
同一天可能被多份订单同时租借。
当 10 6 10^6 106 份订单的需求叠加时,需求总数可能达到 10 15 10^{15} 1015,剩余教室数量也可能变成绝对值很大的负数,超出了 int 的范围。
因此,差分数组和累加变量都需要使用 long long:
cpp
long long diff[N];
long long sum = 0;
单份订单的数量、日期和每天初始可借教室数使用 int 也足够。不过为了统一类型,本文将数组都定义为 long long,构造差分时也不需要强制类型转换。
5. 剩余数量等于 0 仍然合法
判断条件是:
cpp
if (sum < 0)
当剩余数量等于 0 0 0 时,说明教室刚好全部借出,仍然可以满足订单。
只有剩余数量小于 0 0 0,才表示教室不足。
6. 复杂度分析
- 时间复杂度 :每次检查需要 O ( n + x ) O(n+x) O(n+x),最多为 O ( n + m ) O(n+m) O(n+m);二分检查约 O ( log m ) O(\log m) O(logm) 次,总时间复杂度为 O ( ( n + m ) log ( m + 1 ) ) O((n+m)\log(m+1)) O((n+m)log(m+1))。
- 空间复杂度 : O ( n + m ) O(n+m) O(n+m),用于保存初始教室数量、订单信息和差分数组。
五、总结:
这道题结合了二分答案 和差分:
- 确定二分目标:查找最多能满足多少份订单;
- 利用单调性:订单越多,剩余教室数量不会增加;
- 构造检查函数 :先根据初始教室数量构造差分,再减去前 x x x 份订单的需求;
- 判断是否满足 :还原后,只要有一天剩余数量小于 0 0 0,就说明这批订单不能全部满足;
- 确定输出 :全部满足时输出 0 0 0;否则输出最多能满足的订单数量加 1 1 1。
可以这样记住本题的分工:
二分负责找最多能满足多少份订单,差分负责检查这一批订单。
做完这道题后,可以回顾 P2367 语文成绩:两道题都用差分处理区间修改,但 P2367 在所有修改完成后统一求值,而本题还需要结合二分,确定应该检查多少份订单。
相关文章 :一维差分详解
如果这篇题解对你有帮助,欢迎点赞、收藏、关注,持续更新算法知识点和洛谷题解。