WikiOI 1217 借教室 (NOIP2012)二分写法

本文介绍了一个基于先到先得原则的教室预订系统设计方案。该方案通过编程方式处理连续多天的教室租借请求,确保每个请求按顺序得到满足,若无法满足则通知用户调整请求。

题目描述 Description
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要

向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。

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

我们需要处理接下来n天的借教室信息,其中第i天学校有ri个教室可供租借。共有m份

订单,每份订单用三个正整数描述,分别为dj, sj, tj,表示某租借者需要从第sj天到第tj天租

借教室(包括第sj天和第tj天),每天需要租借dj个教室。

我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提

供dj个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。

借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教

室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申

请人修改订单。这里的无法满足指从第sj天到第tj天中有至少一天剩余的教室数量不足dj个。

现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改

订单。

输入描述 Input Description
第一行包含两个正整数n, m,表示天数和订单的数量。

提高组 day2

第二行包含n个正整数,其中第i个数为ri,表示第i天可用于租借的教室数量。

接下来有m行,每行包含三个正整数dj, sj, tj,表示租借的数量,租借开始、结束分别在

第几天。

每行相邻的两个数之间均用一个空格隔开。天数与订单均用从1开始的整数编号。

输出描述 Output Description
如果所有订单均可满足,则输出只有一行,包含一个整数 0。否则(订单无法完全满足)

输出两行,第一行输出一个负整数-1,第二行输出需要修改订单的申请人编号。

样例输入 Sample Input
4 3

2 5 4 3

2 1 3

3 2 4

4 2 4

样例输出 Sample Output
-1

2

数据范围及提示 Data Size & Hint
【输入输出样例说明】

classroom.out

-1

2

第 1 份订单满足后,4 天剩余的教室数分别为 0,3,2,3。第 2 份订单要求第 2 天到

第 4 天每天提供 3 个教室,而第 3 天剩余的教室数为 2,因此无法满足。分配停止,通知第

2 个申请人修改订单。

【数据范围】

对于 10%的数据,有1 ≤ n, m ≤ 10;

对于 30%的数据,有1 ≤ n, m ≤ 1000;

对于 70%的数据,有1 ≤ n, m ≤ 105;

对于 100%的数据,有1 ≤ n, m ≤ 10^6, 0 ≤ ri, dj≤ 10^9, 1 ≤ sj≤ tj≤ n。

program mys;

var i,j,mid,k,m,n,l,r:longint;
a,e,b,c,d:array[0..1010000]of longint;
f:array[0..1000100]of int64;

function erf(x:longint):boolean;
begin 
fillchar(f,sizeof(f),0);
fillchar(e,sizeof(e),0);
for i:=1 to x do 
begin 
f[a[i]]:=f[a[i]]+c[i];
f[b[i]+1]:=f[b[i]+1]-c[i];
end;
for i:=1 to n do 
begin 
e[i]:=e[i-1]+f[i];
if e[i]>d[i] then exit(false);
end;
exit(true);
end;

begin 
readln(n,m);
for i:=1 to n do 
read(d[i]);
for i:=1 to m do
readln(c[i],a[i],b[i]);

l:=0;r:=m;
while l<=r do 
begin 
mid:=(l+r)div 2;
if erf(mid)=false then 
r:=mid-1
else l:=mid+1;
if l=m+1 then 
begin 
writeln(0);
halt;
end;
if l>r then 
begin 
writeln(-1);
writeln(l);
halt;
end;
end;
end.
<think>题目:P1083 [NOIP2012 提高组] 借教室 题目描述:我们需要处理n天的借教室订单,有m个订单。每个订单包含三个参数:d_j, s_j, t_j,表示从第s_j天到第t_j天,每天需要借d_j个教室。 我们按照订单顺序处理,如果某个订单无法满足(即某一天剩余的教室数量不足),则拒绝该订单,并且输出该订单编号。 若所有订单都能满足,则输出0。 输入格式: 第一行:两个正整数n, m,表示天数和订单数量。 第二行:n个正整数,第i个数表示第i天可用于租借的教室数量。 接下来m行,每行三个正整数d_j, s_j, t_j,表示第j个订单。 输出格式: 若所有订单都能满足,输出0;否则输出-1,并输出第一个无法满足的订单编号(即第一次出现不能满足的订单)。 注意:订单是按照顺序处理的,所以我们需要依次处理每个订单,并且一旦发现某个订单无法满足,就停止处理。 常规思路:直接模拟,对于每个订单,将区间[s_j, t_j]内的每一天都减去d_j,如果某一天减成了负数,则说明该订单不能满足。但是这样时间复杂度是O(n*m),对于n和m最大10^6,会超时。 优化思路:使用差分数组进行区间更新,然后结合前缀和还原,同时需要二分答案(因为订单是顺序处理的,第一个不满足的订单具有二分性质:如果第x个订单不满足,则x之后的订单都不满足,而x之前的都满足)。 具体步骤: 1. 读入n, m,以及每天的教室数量数组r(1-indexed,方便处理) 2. 读入订单,每个订单包括d, s, t 3. 二分订单编号,判断从第1个订单到第mid个订单是否都能满足。判断时,使用差分数组来快速进行区间减法操作,然后检查每一天是否出现负数(即供不应求)。 二分框架: left = 1, right = m while (left <= right) { mid = (left+right)/2 if (check(mid)) { // 前mid个订单都能满足 left = mid+1 } else { right = mid-1 } } 如果left==m+1,说明所有订单都满足,输出0。 否则,输出-1和left(因为left是第一个不满足的订单编号) 注意:在check(mid)函数中,我们用差分数组来模拟前mid个订单的教室借用情况。 差分数组diff[1...n]初始为0,然后对于每个订单i (1<=i<=mid),执行: diff[s_i] += d_i diff[t_i+1] -= d_i (注意t_i+1要在数组范围内,否则不用处理) 然后计算前缀和,即第k天的教室使用量 = diff[1] + diff[2] + ... + diff[k] 然后检查每一天k:如果使用量 > r[k](第k天原有的教室数量),则返回false(表示不能满足)。 否则返回true。 但是注意:差分数组的更新是累加的,所以每次check(mid)都需要重新初始化diff数组,然后重新处理前mid个订单。 然而,如果每次check都重新处理前mid个订单,那么每次check的时间复杂度是O(mid+n),总的时间复杂度是O((m+n)*log(m)),对于n,m最大10^6,log(m)约为20,总操作次数为20*(10^6+10^6)=40e6,在C++中是可以接受的。 实现细节: - 数组大小:天数和订单数量最大均为10^6,所以数组开1000000+10。 - 注意差分数组的边界:当t_i+1<=n时才需要减,否则不需要(超出数组范围)。 代码步骤: 1. 读入数据 2. 二分订单编号 3. 编写check函数:判断前x个订单是否都能满足。 注意:二分时,我们寻找的是第一个不能满足的订单。如果前x个订单都满足,那么第一个不能满足的订单在x+1之后;如果前x个订单不能满足,那么第一个不能满足的订单在x或之前。 输出:如果所有订单都满足(即left=m+1),输出0;否则输出-1和left(因为left是第一个不满足的订单编号)。 但是注意:我们二分的结果是left,因为当check(mid)满足时,说明前mid个订单都可以,那么第一个不满足的订单在mid+1,所以left=mid+1;当check(mid)不满足时,第一个不满足的订单在mid或之前,所以right=mid-1。最后left指向第一个不满足的订单。 因此,如果最后left落在[1, m]之间,则输出left;如果left==m+1,则说明所有订单都满足。 代码实现: 注意:差分数组在每次check时都需要重新初始化(可以每次check都创建一个新的全0数组,但这样频繁创建数组可能效率不高,所以我们可以使用一个全局的diff数组,然后在每次check时用循环将其清零?但是清零操作需要O(n),总时间复杂度O(n*logm)≈20*10^6=2e7,可以接受。或者我们可以用两个数组,一个用于存储原教室数量,另一个用于差分,然后在check函数中每次将差分数组清零的操作改为用memset?但是memset也是O(n)的。另外,我们可以使用一个临时数组,然后在check函数内部定义,利用作用域自动释放,这样也可以。 由于n最大1e6,logm最多20,所以最多执行20次O(n)的操作,总操作次数20e6,可以过关。 另一种优化:不实际清零,而是记录当前check的订单数,然后每次只重新添加前x个订单?这样不行,因为每个check的x不同。 因此,我们选择在check函数内部定义局部数组,然后每次重新计算。但是这样会多次申请和释放,但总空间在20*10^6*4(int)=80MB,可能超过栈空间(局部数组在栈上可能太大),所以我们将其定义为全局数组,然后在每次check时用循环填充0(或者用memset)。但是注意:全局数组的话,每次check都要清零,我们可以用循环清零,或者用memset,但memset对于1e6的数组大约1ms,20次就是20ms,可以接受。 或者,我们可以在check函数内使用vector,但是动态分配也会有点时间开销。 为了效率,我们使用一个全局的差分数组,并在每次check时清零(用循环将每个位置赋0,或者用memset,但是循环清零我们可以只清零需要的那部分:1到n+1,因为订单只用到n天,所以差分数组我们只需要1到n+1,清零长度为n+2)。 具体步骤: 定义全局数组:const int MAXN = 1000010; int diff[MAXN]; // 差分数组,大小为MAXN,索引从1到n+1 int r[MAXN]; // 每天的教室数量 int d[MAXN], s[MAXN], t[MAXN]; // 存储订单 在check(x)函数中: 将diff[1]到diff[n+1]清零(因为订单更新可能会用到n+1)。 然后对于每个订单i(从1到x): diff[s[i]] += d[i]; if (t[i] + 1 <= n) { diff[t[i] + 1] -= d[i]; } 然后计算前缀和sum=0,遍历每一天k(1到n): sum += diff[k]; if (sum > r[k]) { // 第k天需要的教室大于拥有的教室 return false; } 如果全部天数都满足,则返回true。 注意:这里我们使用全局数组,所以在check函数中清零是必须的,否则会受到上一次的影响。 但是,我们可以避免每次清零整个数组吗?因为每次check处理的订单数不同,我们可以用另一种方式:在每次check时,我们重新创建一个临时数组,然后计算前缀和。但是这样内存消耗较大。 或者,我们可以不全局清零,而是用减法撤销?但是这样并不简单,因为每次订单数量不同。 因此,我们决定使用全局数组,并在每次check时清零,但是只清零需要的位置(1到n+1),用循环。 代码示例: 注意:由于n和m最大为10^6,所以数组大小设为1000010。 输入输出使用scanf和printf,因为数据量较大。 完整代码: </think>以下是解决「借教室」问题的 C++ 实现。该方法使用**差分数组**和**二分答案**技术优化订单处理,时间复杂度为 \(O((n + m) \log m)\),适用于大规模数据。 ### 解决方案代码 ```cpp #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 1000010; int n, m; int r[MAXN]; // 每天可用教室数量 int d[MAXN], s[MAXN], t[MAXN]; // 订单信息 long long diff[MAXN]; // 差分数组(使用long long防溢出) // 检查前x个订单是否可以满足 bool check(int x) { memset(diff, 0, sizeof(diff)); // 重置差分数组 for (int i = 1; i <= x; i++) { diff[s[i]] += d[i]; // 区间起点添加订单量 if (t[i] + 1 <= n) { diff[t[i] + 1] -= d[i]; // 区间终点后一天减去订单量 } } long long current = 0; // 当前每天的借用总量 for (int i = 1; i <= n; i++) { current += diff[i]; // 计算前缀和 if (current > r[i]) { return false; // 超过当天可用教室 } } return true; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &r[i]); } for (int i = 1; i <= m; i++) { scanf("%d%d%d", &d[i], &s[i], &t[i]); } int left = 1, right = m; // 二分订单编号 int ans = 0; while (left <= right) { int mid = (left + right) >> 1; if (check(mid)) { left = mid + 1; // 前mid个订单可满足 } else { ans = mid; // 记录第一个不满足的订单 right = mid - 1; } } if (ans == 0) { printf("0\n"); // 所有订单均满足 } else { printf("-1\n%d\n", ans); // 输出第一个不满足的订单 } return 0; } ``` ### 代码解释 1. **核心数据结构**: - `r[]`:存储每天初始可用教室数量 - `d[], s[], t[]`:存储订单的借用量、起始天和终止天 - `diff[]`:差分数组,用于高效处理区间修改 2. **`check(int x)` 函数**: - **差分数组构建**:遍历前 `x` 个订单,在 `s[i]` 处添加 `d[i]`,在 `t[i]+1` 处减去 `d[i]`(若未越界) - **前缀和计算**:逐天累加差分数组值,得到每天实际借用量 - **容量检查**:若某天借用量超过 `r[i]` 则返回 `false` 3. **二分查找逻辑**: - **初始化**:`left=1`, `right=m` - **二分过程**:若前 `mid` 个订单可满足,则向右搜索;否则向左搜索 - **结果判定**:若 `ans` 为 0 则全部满足;否则输出第一个失败订单 4. **时间复杂度**: - 每次 `check()` 操作:\(O(n + m)\) - 二分次数:\(O(\log m)\) - 总复杂度:\(O((n + m) \log m)\) 5. **空间复杂度**: - 使用 \(O(n + m)\) 的数组存储数据 - 差分数组额外占用 \(O(n)\) 空间 ### 注意事项 - **差分数组边界**:需检查 `t[i]+1` 不超过 `n`,防止数组越界 - **数据类型**:使用 `long long` 防止累加过程中的整数溢出 - **初始化清零**:每次 `check()` 需重置差分数组避免脏数据 - **输入输出**:使用 `scanf/printf` 提高效率 ### 算法分析 1. **差分技术**:将区间修改优化为 \(O(1)\) 操作 2. **二分答案**:利用订单处理的单调性(前k个满足则k-1个必满足) 3. **时间复杂度对比**: - 暴力模拟:\(O(nm)\)(超时) - 优化方案:\(O((n+m)\log m)\)(高效)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值