洛谷P1083 [NOIP2012 提高组] 借教室 && 差分学习笔记

传送门:P1083 [NOIP2012 提高组] 借教室

"八骏日行三万里,穆王何事不重来。"
可惜啊,他再也没有回来……

题目大意:

给你每天能够租借的教室数量 和 几份租借申请
每份申请包含 租界时间(从第几天到第几天)和 每天需要租借的教室数量
问你能否满足所有的租借要求,如果不能,驳回一份最前的不能满足要求的申请

思路:

  • 暴力:首先,这道题目的暴力很好想,就是从 1 开始遍历每一种订单,判断是否合法
    复杂度:O(nm),铁定爆炸

  • 线段树:蒟蒻第一眼看到这道题首先想到的是线段树,维护一个区间最小值,判断能否租借
    每次有租借请求就做一个区间减法
    但是一个黄题的话就有点。。。杀鸡焉用宰牛刀的感觉。其实是懒,而且要知道线段树复杂度还是很高的
    具体可以看一下这个大佬的博文

  • 至于正解:首先我们要明确一个知识点,那就是:

差分!

  • 引入:
    首先,给你一个问题:
    给出 n 个数,再给出 Q 个询问,每个询问给出 l, r ;
    要求你在 l 到 r 上每一个值都加上 x,而只给你 O(n) 的时间范围,怎么办?

  • 思路:
    还是用上面这个题目,假如要在 l 和 r 上全都加一个 x,很显然,这个 O(n) 是不可避免的。
    既然这样,那我们考虑在询问中我们不去来加,而是做一个标记,最后一起加上。 有一点线段树lazy_tag的思想

  • ps:以上摘自https://www.zybuluo.com/Junlier/note/1232395

  • 这就是差分
    差分即相邻两个数的差。
    我们可以这样去想:
    对于一个序列 a[ ]={1,3,4,7};
    我们构建一个差分数组 cf,cf[i] 表示 a[i] - a[i-1],则 cf[ ]={1,2,1,3};
    这时,对于求某个数,比如:

a[1] = cf[1];
a[2] = cf[1] + cf[2];
a[3] = cf[1] + cf[2] + cf[3];
a[4] = cf[1] + cf[2] + cf[3] + cf[4];``
  • 这时例如我们想要让 第 1 个数 和 第 3 个数 之间所有数 +3
    发现:cf[1]+=3,但是 1~3 的区间内的数的两两间的差值没有变,a[3] 与 a[4] 之间的差值(cf[4])-3,a[4] 与之后的数差值不变
    特殊 ——> 一般
    对于在 l 到 r 的区间内 +x,其实相当于 cf[l]+x,cf[r+1]-x。
    最后输出时,按照上面的方法顺序求解就好了。(区间减法亦然)

  • 顺便提一嘴,前缀和是用元数据求元与元之间的并集关系,而差分则是根据元与元之间的逻辑关系求元数据,是互逆思想。——摘自 皎月半洒花【小花】的题解

至此,我们就可以来探讨一下正解了 QWQ

那就是:差分 + 二分答案

  • 一般来说,二分是个很有用的优化途径,因为这样会直接导致减半运算,而对于能否二分,有一个界定标准:状态的决策过程或者序列是否满足单调性或者可以局部舍弃性。 而在这个题里,因为如果前一份订单都不满足,那么之后的所有订单都不用继续考虑;而如果后一份订单都满足,那么之前的所有订单一定都可以满足,符合局部舍弃性,所以可以二分订单数量。 ——摘自 皎月半洒花【小花】的题解

  • 二分查找の魔鬼细节,请移步

所以,每次二分一下,直到无法满足或者全枚举完结束。

代码:

不开 long long 见祖宗

#include<bits/stdc++.h>

using namespace std;

#define int long long

const int maxn=1001000;

int m,n;
int r[maxn];
int rr[maxn];
int d[maxn],s[maxn],t[maxn];
int cf[maxn];

bool check(int x)
{
	memset(cf,0,sizeof(cf));
	for(int i=1;i<=x;i++)
	{
		cf[s[i]]+=d[i];
		cf[t[i]+1]-=d[i];
	}
	for(int i=1;i<=n;i++)
	{
		rr[i]=rr[i-1]+cf[i];
		if(rr[i]>r[i])return 0;
	}
	return 1;
 } 

signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>r[i];
	for(int i=1;i<=m;i++)
		cin>>d[i]>>s[i]>>t[i]; 
	int left=1,right=m;
	while(left<right)
	{
		int mid=left+right>>1;
		if(check(mid))left=mid+1;
		else right=mid; //因为要查找的是右边界 
	}
	if(left<m)cout<<-1<<endl<<left<<endl;
	else cout<<0<<endl;
	return 0;
}

后记:

这是一篇写了两个半小时的博文。。。
写的时候确实是挺煎熬的
但我觉得这很有意义
学会了自己之前不会的知识,了解了新的思路,更重要的是能 +rp
另外,这确实是一道非常好的题目!

posted @ 2024-08-18 19:17  lazy_ZJY  阅读(5)  评论(0编辑  收藏  举报