【洛谷题解】P1083 [NOIP 2012 提高组] 借教室(二分答案 + 差分)

知识点:二分答案、一维差分 | 所属专栏:【洛谷题解】

前置知识:一维差分详解

目录

    • 一、题目描述:
    • 二、题目分析:
      • [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. 复杂度分析)
    • 五、总结:

一、题目描述:

题目链接:P1083 NOIP 2012 提高组 借教室

【题目描述】:

在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。

面对海量租借教室的信息,我们自然希望编程解决这个问题。

我们需要处理接下来 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. 暴力做法

最直接的做法是按照顺序处理每份订单:

  1. 遍历订单涉及的每一天,检查剩余教室是否足够;
  2. 如果有一天不够,输出当前订单编号;
  3. 如果每天都足够,再将这段时间的剩余教室数减去对应数量。

一份订单可能涉及 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. 算法思路:

根据上面的分析,代码分为三个部分:

  1. 读入每天的教室数量和所有订单;
  2. 编写 check(x),用差分判断前 x x x 份订单能否全部满足;
  3. 二分查找最多能满足的订单数量,再根据结果输出答案。

我们根据思路,一步一步来实现。

① 首先读入数据,并保存每份订单:

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),用于保存初始教室数量、订单信息和差分数组。

五、总结:

这道题结合了二分答案 和差分:

  1. 确定二分目标:查找最多能满足多少份订单;
  2. 利用单调性:订单越多,剩余教室数量不会增加;
  3. 构造检查函数 :先根据初始教室数量构造差分,再减去前 x x x 份订单的需求;
  4. 判断是否满足 :还原后,只要有一天剩余数量小于 0 0 0,就说明这批订单不能全部满足;
  5. 确定输出 :全部满足时输出 0 0 0;否则输出最多能满足的订单数量加 1 1 1。

可以这样记住本题的分工:

二分负责找最多能满足多少份订单,差分负责检查这一批订单。

做完这道题后,可以回顾 P2367 语文成绩:两道题都用差分处理区间修改,但 P2367 在所有修改完成后统一求值,而本题还需要结合二分,确定应该检查多少份订单。


相关文章 :一维差分详解

如果这篇题解对你有帮助,欢迎点赞、收藏、关注,持续更新算法知识点和洛谷题解。

相关推荐
小此方1 小时前
[算法实战论:动态规划]回文串问题的dp解法,回文串问题的母题,以及以此为基础的派生题
算法·动态规划
Pniubi2 小时前
力扣560.和为k的子数组
算法·leetcode·职场和发展
D e re k ZH2 小时前
C++ 从零入门(一):基础语法与程序结构
数据结构·c++·算法
Pniubi3 小时前
力扣438.找到字符串中所有字母异位词
算法·leetcode·职场和发展
zjxtxdy4 小时前
C++面向对象程序开发-类与对象
开发语言·c++
東隅已逝,桑榆非晚5 小时前
算法6. 双指针法-查找总价格为目标的两个商品
c++·学习·算法
Eyfcom5 小时前
〖物联网终端实战 01〗UWB 室内高精度定位:仓储 AGV 与人员的厘米级轨迹追踪
物联网·算法·物流系统·快递系统·跨境物流快递系统·物联网终端实战
qq_406981036 小时前
c++拷贝构造函数
开发语言·c++
bro_Java6666 小时前
《排序的底层逻辑:比较、交换与分治》
java·算法·排序算法