【AcWing】蓝桥杯集训每日一题Day1|二分|差分|503.借教室(C++)

503. 借教室

503. 借教室 - AcWing题库
难度:简单
时/空限制:1s / 128MB
总通过数:8052
总尝试数:26311
来源:NOIP2012提高组
算法标签二分差分

题目内容

在大学期间,经常需要租借教室。
大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。
教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。 
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来 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 s t_{s} ts天租借教室(包括第 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,表示租借的数量,租借开始、结束分别在第几天。 
每行相邻的两个数之间均用一个空格隔开。
天数与订单均用从 11 开始的整数编号。

输出格式

如果所有订单均可满足,则输出只有一行,包含一个整数0。
否则(订单无法完全满足)输出两行,第一行输出一个负整数 −1,第二行输出需要修改订单的申请人编号。

数据范围

1 ≤ n , m ≤ 1 0 6 1≤n,m≤10^6 1n,m106
0 ≤ r i , d j ≤ 1 0 9 0≤r_{i},d_{j}≤10^9 0ri,dj109
1 ≤ s j ≤ t j ≤ n 1≤s_{j}≤t_{j}≤n 1sjtjn

输入样例:
4 3
2 5 4 3
2 1 3
3 2 4
4 2 4
输出样例:
-1
2
题目解析

数据范围在 1 0 6 10^6 106,时间复杂度要控制在 O ( n log ⁡ n ) O(n\log n) O(nlogn)/ O ( n ) O(n) O(n)
如何通过数据范围判断时间复杂度可以参考这篇文章
蓝桥杯暴力求解与高频考点-CSDN博客
如果直接模拟,不优化的话
![[Pasted image 20240309164127.png]]

可以从前往后依次遍历每个订单,可以开个数组,统计一下每一天一共需要多少教室
s … t s \dots t st天,每天需要 d d d个教室,可以枚举for循环一下,从s~t枚举一遍,cnt[i]+=d,每次加完之后,判断这一天需要教室数量是不是小于给定教室数量,如果是的话,表示满足;不是的话,表示不满足。
一共需要处理 1 0 6 10^6 106个订单,每个订单的区间长度最大 O ( n ) = 1 0 6 O(n)=10^6 O(n)=106,时间复杂度就是 1 0 6 ∗ 1 0 6 = 1 0 12 10^6*10^6=10^{12} 106106=1012,肯定会TLE(Time Limit Exceeded时间超限)。
如果代码不想超时,需要将时间复杂度控制在 1 0 8 10^8 108以内(1s是 1 0 8 10^8 108)

优化的话
如果按照题意进行动态地做,动态地给每一个区间加上同一个d,然后再去动态地判断这个区间内的某一个值是不是大于给定的值。

可以用线段树来做,可是线段树时间复杂度比较高,可能会TLE,线段树的题目n一般会给到 1 0 5 10^5 105。要使用比线段树常数更小的算法

1. 差分

如果把这个题变成一个判定性的问题,
判断一下能不能处理前k个订单,这个时候,这个题目就变成了给k个区间,每个区间加一个相同的数,加完之后需要计算一下每个数最终的值是多少
![[Pasted image 20240309164206.png]]

经典的模板题,相当于算法里的公式
797.差分
有一个标准做法,可以做到线性的时间复杂度 O ( n + m ) O(n+m) O(n+m)
判定性问题可以用差分来做

差分的原理

比如每一天可以用的教室数量是 r 1 … r n r_{1}\dots r_{n} r1rn,用数组r表示
要统计一下每天需要多少教室 a 1 … a n a_{1}\dots a_{n} a1an,用a数组来表示
初始的时候都是0,
给一个s~t的订单,给 a s … a t a_{s}\dots a_{t} asat之间的每个数全部加上一个d
先将a数组进行一个变形
a 0 = 0 a_{0}=0 a0=0
b 1 = a 1 − a 0 b_{1}=a_{1}-a_{0} b1=a1a0 b 2 = a 2 − a 1 b_{2}=a_{2}-a_{1} b2=a2a1以此类推,一直到 b n = a n − a n − 1 b_{n}=a_{n}-a_{n-1} bn=anan1
相当于对a数组进行一个差分变换
发现
如果给我们一个差分数组b,可以把原数组a反推回来
给原数组a,可以把差分数组b求出来
差分数组和原数组的信息量是一模一样的
差分数组推原数组
a 1 = b 1 + a 0 = b 1 a_{1}=b_{1}+a_{0}=b_{1} a1=b1+a0=b1
a 2 = b 2 + a 1 = b 1 + b 2 a_{2}=b_{2}+a_{1}=b_{1}+b_{2} a2=b2+a1=b1+b2
a n = b n + a n − 1 = b 1 + ⋯ + b n a_{n}=b_{n}+a_{n-1}=b_{1}+\dots+b_{n} an=bn+an1=b1++bn
原数组就是差分数组的前缀和数组

转换完之后
如果想对s~t,每个数加上一个d的话
如果要对原数组操作,要去修改 O ( n ) O(n) O(n)个数
但是这个操作对差分数组的影响就很少了
b 1 = a 1 − a 2 b 2 = a 2 − a 1 … b s − 1 = a s − 1 − a s − 2 b s = a s − a s − 1 b s + 1 = a s + 1 − a s … b t = a t − a t − 1 b t + 1 = a t + 1 − a t b t + 2 = a t + 2 − a t + 1 … \begin{array}{} b_{1}=a_{1}-a_{2} \\ b_{2}=a_{2}-a_{1} \\ \dots \\ b_{s-1}=a_{s-1}-a_{s-2} \\ b_{s}=a_{s}-a_{s-1} \\ b_{s+1}=a_{s+1}-a_{s} \\ \dots \\ b_{t}=a_{t}-a_{t-1} \\ b_{t+1}=a_{t+1}-a_{t} \\ b_{t+2}=a_{t+2}-a_{t+1} \\ \dots \end{array} b1=a1a2b2=a2a1bs1=as1as2bs=asas1bs+1=as+1asbt=atat1bt+1=at+1atbt+2=at+2at+1
这个操作只会对 a s … a t a_{s}\dots a_{t} asat将每个数全部加上d
因此
b 1 … b s − 1 b_{1}\dots b_{s-1} b1bs1都是不变的,因为 a 0 … a s − 1 a_{0}\dots a_{s-1} a0as1不变,所以差值也不变
b s b_{s} bs的话, a s a_{s} as加上一个d, a s − 1 a_{s-1} as1不变,所以 b s b_{s} bs加了一个d
b s + 1 … b t b_{s+1}\dots b_{t} bs+1bt,由于 a s … a t a_{s}\dots a_{t} asat每个数全部加了一个d,所以差值是不变的,所以b都是不变的
b t + 1 b_{t+1} bt+1的话, a t + 1 a_{t+1} at+1不变, a t a_{t} at加了一个d,所以差值会减一个d,所以 b t + 1 b_{t+1} bt+1减一个d
b t + 2 b_{t+2} bt+2开始到后面的,因为从 a t + 1 … a n a_{t+1}\dots a_{n} at+1an一直都不变,所以不变
不变 { b 1 = a 1 − a 2 b 2 = a 2 − a 1 … b s − 1 = a s − 1 − a s − 2 + d b s = a s − a s − 1 不变 { b s + 1 = a s + 1 − a s … b t = a t − a t − 1 − d b t + 1 = a t + 1 − a t 不变 { b t + 2 = a t + 2 − a t + 1 … \begin{array}{} \\ 不变\begin{cases} b_{1}=a_{1}-a_{2} \\ b_{2}=a_{2}-a_{1} \\ \dots \\ b_{s-1}=a_{s-1}-a_{s-2} \\ \end{cases}\\ +d\qquad b_{s}=a_{s}-a_{s-1} \\ 不变\begin{cases} b_{s+1}=a_{s+1}-a_{s} \\ \dots \\ b_{t}=a_{t}-a_{t-1} \\ \end{cases}\\ -d\qquad b_{t+1}=a_{t+1}-a_{t} \\ 不变\begin{cases} b_{t+2}=a_{t+2}-a_{t+1} \\ \dots \end{cases} \end{array} 不变 b1=a1a2b2=a2a1bs1=as1as2+dbs=asas1不变 bs+1=as+1asbt=atat1dbt+1=at+1at不变{bt+2=at+2at+1

对于原数组的操作,本来要操作 O ( n ) O(n) O(n)个数,对于差分数组的影响,只会影响 b s b_{s} bs b t + 1 b_{t+1} bt+1两个数,这样就可以将修改操作从 O ( n ) O(n) O(n)变成 O ( 1 ) O(1) O(1)
改变 O ( n ) O(n) O(n)个数,需要两层循环,现在只用改变两个数,就不用写循环了,可以把时间复杂度从 O ( n 2 ) O(n^2) O(n2)变成 O ( n ) O(n) O(n)
这样的话每个操作就不要对原数组操作,而是对差分数组进行操作,差分数组求完之后,再通过差分数组求出来原数组,相当于做一个变换

这样可以用 O ( n + m ) O(n+m) O(n+m)的时间复杂度,判断出前k个订单有没有出现矛盾

2. 二分

原问题
把所有的订单取出来,编号是1~m
先到先得,按照编号顺序依次处理每个订单

二段性

这些个能处理的订单,有一个二段性
比如说在这个问题里面,第k个订单是最后一个能处理的订单
从第k+1个订单开始就不能处理了
![[Pasted image 20240309185859.png]]

把前k个区间全部加到数组上,每一天需要的教室数量都是小于等于给定的教室数量,对于k前面的任何一个数x,同样能处理x个订单,显然是成立的
前x个订单严格比前k个订单少一些区间,相当于在某些数上少加了一些d,加上那些之后不会超过给定值,不加那些数同样不会超过给定值
最后一个能处理的订单是第k个订单的话,对于小于等于k等任何一个数x,前x个订单一定都是可以处理的

同理

第一个不能处理的订单是第k+1个订单的话,如果把前k+1个区间加到天数数组r上的话,一定会有某一天,它需要的教室数量大于给定值
对于大于等于k+1的任何一个x,1~x一定也都是不能处理的
![[Pasted image 20240309185935.png]]

因为前k+1个区间加完之后,在某一天已经大于给定值了,再加上一些额外的数,仍然会大于给定值


对于x来说,当x取1~k之间的数的时候,它是满足的
当x大于k的时候,是不满足的

具有二段性的话,就可以通过二分把这个边界二分出来,
789.数的范围

如何二分

比如最后一个能处理的订单是第k个订单,
左右边界是1~m
每次二分一个中点mid
如果1~mid,能满足的话,说明最后一个能满足的订单在mid或者在mid的右边,所以答案会落在 [ m i d , m ] [mid,m] [mid,m]这个区间,
![[Pasted image 20240309191741.png]]

就可以删掉左边的区间,把二分的左端点L设成mid
![[Pasted image 20240309191817.png]]

再二分中点
取一个mid,如果处理mid这个订单会出现矛盾,说明1~mid是无法处理的,说明最后一个能处理的订单在mid的严格左边
![[Pasted image 20240309190835.png]]

因此相当于把右边的区间删掉,把右端点R置成mid-1
![[Pasted image 20240309190918.png]]

每次将搜索范围缩小一半,缩小完之后保证答案一定在搜索范围内,最后当范围内只有一个数的时候,它就是答案

二分模板用哪个,取决于mid属于左半边区间还是右半边区间,取整的问题,二分的话,整数不一定能够刚好整除,奇数的话是上取整还是下取整,如果模板用错可能会有死循环

代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;
//算前缀和,10^6个10^9,有可能会爆int,需要用到long long

const int N = 1000010;

int n, m;
int w[N];
//用w存r,不用r,因为二分的时候会用到变量r,防止变量冲突
int d[N], s[N], t[N];
LL b[N];

//检查前mid订单能不能满足
bool check (int mid)
{
	memset(b, 0, sizeof b); //先把差分数组初始化为0,不能让上次检查的结果,影响下次检查的过程
	for (int i = 1; i <= mid; i++) //依次处理每一个订单
	{
		b[s[i]] += d[i];
		b[t[i] + 1] -= d[i];
	}

	LL s = 0;  //用一个前缀和来计算当前的每一a_i是多少
	for (int i = 1; i <= n; i++) //以此类推每一天
	{
		s += b[i]; //每次更新一下前缀和
		if (s > w[i]) return false;  //发现当前教室数量大于给定教室数量,return false
	}

	return true;  //否则return true
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int 1 = 1; i <= n; i++) scanf("%d", &w[i]);
	//依次读入每天可以用的教室数量
	for (int i = 1; i <= m; i++) scanf("%d%d%d", &d[i], &s[i], &t[i]);
	//依次读入m个订单d,s,t,订单数量和起始时间

	//二分
	int l = 0; r = m;   //0表示一个订单都不能满足
	while (l < r)
	{
		//求中点
		int mid = l + r + 1 >> 1; //因为l=mid,+1用上取整;如果r=mid,用下取整
		if (check(mid)) l = mid;  //如果发现[1,mid)可以满足,表示答案应该在[mid,m]
		else r = mid -1;
	}

	if (r == m) puts("0"); //如果发现所有订单都能满足,输出0
	else printf("-1\n%d\n", r + 1); //否则第一行输出-1,第二行输出第一个不能满足的订单编号

	return 0;
}

最近更新

  1. TCP协议是安全的吗?

    2024-03-10 11:10:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-10 11:10:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-10 11:10:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-10 11:10:04       20 阅读

热门阅读

  1. 【DevOps基础篇之k8s】Kubernetes API服务认证/授权

    2024-03-10 11:10:04       20 阅读
  2. ZooKeeper和Diamond有什么不同

    2024-03-10 11:10:04       22 阅读
  3. 学习Android的第二十六天

    2024-03-10 11:10:04       21 阅读
  4. 产品经理简历话术整理

    2024-03-10 11:10:04       25 阅读
  5. 登录失败重试次数安全设计方案

    2024-03-10 11:10:04       18 阅读
  6. 【设计模式】外观模式

    2024-03-10 11:10:04       21 阅读
  7. C++11

    C++11

    2024-03-10 11:10:04      18 阅读