洛谷 P1083 借教室
https://www.cnblogs.com/violet-acmer/p/9721160.html
一、暴力简述
首先我们不难看出,这道题--并不是一道多难的题,因为显然,第一眼看题目时便很容易地想到暴力如何打:枚举每一种订单,然后针对每一种订单,对区间内的每一天进行修改(做减法),直到某一份订单使得某一天剩下的教室数量为负数,即可得出结果。
先小小的评析一下吧:凡是能打出几近正解的暴力题,都不是难题!
但是,显然枚举形式的暴力会很慢,期望的时间复杂度约为O(m \times n)O(m×n)。
二、思想详述
让我们开动脑筋想一下:每张订单其实就可以看作是一个区间(操作),左右区间分别为开始时间和结束时间,所以这不就是一个区间操作吗——首选线段tree啦!
先介绍一种好理解、好实现的算法:差分数组。
在介绍差分之前,需要介绍前缀和思想
我们有一组数(个数小于等于一千万),并且有一大堆询问——给定区间l、r,求l、r之间所有数之和(询问个数小于等于一千万)
此处暴力肯定不行啊(O(NQlength)),那么我们来观察前缀和是怎么做的:用sum[i]来存储前i个数的和,然后用sum[r]-sum[l-1]来表示l~r之间所有数的和。(l-1原因是l~r只看要包含l)而sum数组便可以通过简单的递推求出来
代码核心:
1 for(int i=1;i<=n;i++) 2 { 3 cin>>a[i]; 4 sum[i]=sum[i-1]+a[i]; 5 } 6 for(int i=1;i<=q;i++) 7 { 8 cin>>l>>r; 9 cout<<sum[r]-sum[l-1]<<" "; 10 }
而所谓的差分数组,即是前缀和数组的逆运算:
我们给定前i个数相邻两个数的差(1<=i<=n),求每一项a[i](1<=i<=n)。
此时无非就是用作差的方式求得每一项,此时我们可以有一个作差数组diff,diff[i]用于记录a[i]-a[i-1],然后对于每一项a[i],我们可以递推出来:
1 for(int i=1;i<=n;i++) 2 { 3 cin>>diff[i]; 4 a[i]=diff[i]+a[i-1]; 5 } 6 for(int i=1;i<=n;i++) 7 { 8 cout<<a[i]; 9 }
到这儿,我们可以看出来,前缀和是用元数据求元与元之间的并集关系,而差分则是根据元与元之间的逻辑关系求元数据,是互逆思想(qwq但是有时元数据和关系数据不是很好辨别或者产生角色反演啊)。
但是,理解了前缀和&差分,并不代表肯定能做到模板题。
三、关于答案二分
一般来说,二分是个很有用的优化途径,因为这样会直接导致减半运算,而对于能否二分,有一个界定标准:状态的决策过程或者序列是否满足单调性或者可以局部舍弃性。
而在这个题里,因为如果前一份订单都不满足,那么之后的所有订单都不用继续考虑;
而如果后一份订单都满足,那么之前的所有订单一定都可以满足,符合局部舍弃性,所以可以二分订单数量。
四、正解
首先,要明白如为什么要用区间差分而不是区间前缀和:因为这个题每次操作针对的对象都是原本题目中给的元数据,而不是让求某个关系,所以采用差分。
其次,要知道差分会起到怎样的作用:因为diff数组决定着每个元数据的变化大小、趋势,所以,当我们想要针对区间操作时,前缀和可以转化成对diff数组操作:
1 diff[l[i]] += d[i]; 2 diff[r[i]+1] -= d[i];//d[i]是指第 i 天要借的教室数
因为后面的元数据都由之前的diff数组推导出来,所以改变diff[i]就相当于改变 i 之后的每一个值,并通过重新减去改变的量,达到操作区间的目的。
then,我们需要想明白策略:从第一份订单开始枚举,直到无法满足或者全枚举完结束。
最后,一点提示,我下面的标程是通过比大小来判断是否满足,而不是作差判负数——能不出负数就别出负数。
以上解析来自大佬%%%%%%%:https://www.luogu.org/problemnew/solution/P1083
差分数组讲解--大佬博客:http://www.cnblogs.com/widsom/p/7750656.html
AC代码1:差分数组
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 const int maxn=1e6+50; 6 7 int n,m; 8 struct Node1 9 { 10 int l,r; 11 int d; 12 }ord[maxn];//订单 l : 开始时间 r : 结束时间 d : 每天需要的房间数 13 int rest[maxn];//rest[i] : 第 i 可以提供的房间数 14 int diff[maxn];//差分数组 15 int need[maxn];//need[i] : 第 i 天需要的房间数 16 17 bool isOk(int x) 18 { 19 memset(diff,0,sizeof(diff)); 20 for(int i=1;i <= x;++i) 21 { 22 diff[ord[i].l] += ord[i].d; 23 diff[ord[i].r+1] -= ord[i].d; 24 } 25 for(int i=1;i <= n;++i) 26 { 27 need[i]=need[i-1]+diff[i]; 28 if(need[i] > rest[i]) 29 return false; 30 } 31 return true; 32 } 33 34 int main() 35 { 36 scanf("%d%d",&n,&m); 37 for(int i=1;i <= n;++i) 38 scanf("%d",rest+i); 39 for(int i=1;i <= m;++i) 40 scanf("%d%d%d",&ord[i].d,&ord[i].l,&ord[i].r); 41 42 if(isOk(m)) 43 { 44 printf("0\n"); 45 return 0; 46 } 47 int l=1,r=m; 48 while(l < r)//二分查找答案 49 { 50 int mid=l+((r-l)>>1); 51 if(isOk(mid)) 52 l=mid+1; 53 else 54 r=mid; 55 } 56 printf("%d\n%d\n",-1,r); 57 }
线段树解法:
看题解前要确保会线段树区间更新的模板题(懒惰标记)以及用线段树解决RMQ问题的模板题。
变量解释:
rest[ i ] : 第 i 天可以提供的房间数
flag : 判断某订单是否满足条件,初始为false
定义的线段树结构体:
1 struct Node1 2 { 3 int l,r; 4 int val; 5 int lazy; 6 int mid(){ 7 return l+((r-l)>>1); 8 } 9 bool isEqual(){ 10 return l == r ? true:false; 11 } 12 }segTree[4*maxn];
l,r : 左右区间
lazy : 懒惰标记,此处不再是表示懒惰的次数,而是表示在当前节点懒惰的值
val : 如果节点v是非叶节点,则其存储的是左右儿子中的val值较小的值,叶节点存储的是第 i 天可以提供的房间数
题解:
线段树的节点存储的是区间最小值,每次区间更新时判断懒惰的区间是否满足条件 (订单需要的房间数 <= 可以提供的房间数),如果不满足,令 flag = true,结束更新操作。
因为区间更新操作是顺次执行第一份订单到最后一份订单,所以第一个使flag == true的订单一定是第一个不满足条件的订单,输出此订单编号。
具体细节看代码。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 #define ls(x) ((x)<<1) 6 #define rs(x) ((x)<<1 | 1) 7 const int maxn=1e6+50; 8 9 int n,m; 10 int rest[maxn]; 11 bool flag; 12 //==============线段树============ 13 struct Node1 14 { 15 int l,r; 16 int val; 17 int lazy; 18 int mid(){ 19 return l+((r-l)>>1); 20 } 21 bool isEqual(){ 22 return l == r ? true:false; 23 } 24 }segTree[4*maxn]; 25 void pushUp(int pos)//向上更新,左右孩子的最小值 26 { 27 segTree[pos].val=min(segTree[ls(pos)].val,segTree[rs(pos)].val); 28 } 29 void buildTree(int l,int r,int pos) 30 { 31 segTree[pos].l=l,segTree[pos].r=r; 32 segTree[pos].lazy=0; 33 if(segTree[pos].isEqual()) 34 { 35 segTree[pos].val=rest[l]; 36 return ; 37 } 38 int mid=l+((r-l)>>1); 39 buildTree(l,mid,ls(pos)); 40 buildTree(mid+1,r,rs(pos)); 41 pushUp(pos); 42 } 43 void pushDown(int pos)//向下更新 44 { 45 if(segTree[pos].lazy > 0)//不能再懒惰了 46 { 47 int lazy=segTree[pos].lazy; 48 segTree[ls(pos)].lazy += lazy; 49 segTree[rs(pos)].lazy += lazy; 50 segTree[ls(pos)].val -= lazy; 51 segTree[rs(pos)].val -= lazy; 52 segTree[pos].lazy=0; 53 } 54 } 55 void update(int l,int r,int val,int pos) 56 { 57 if(l == segTree[pos].l && r == segTree[pos].r) 58 { 59 segTree[pos].lazy += val; 60 if(segTree[pos].val < val)//判断是否满足条件 61 { 62 flag=true;//如果不能提供足够的房间,令 flag = true ,结束更新 63 return ; 64 } 65 segTree[pos].val -= val; 66 pushUp(pos>>1); 67 return ; 68 } 69 pushDown(pos); 70 int mid=segTree[pos].mid(); 71 if(r <= mid) 72 update(l,r,val,ls(pos)); 73 else if(l > mid) 74 update(l,r,val,rs(pos)); 75 else 76 { 77 update(l,mid,val,ls(pos)); 78 update(mid+1,r,val,rs(pos)); 79 } 80 pushUp(pos); 81 } 82 //================================ 83 84 int main() 85 { 86 scanf("%d%d",&n,&m); 87 for(int i=1;i <= n;++i) 88 scanf("%d",rest+i); 89 buildTree(1,n,1); 90 flag=false; 91 for(int i=1;i <= m;++i) 92 { 93 int d,s,t; 94 scanf("%d%d%d",&d,&s,&t); 95 update(s,t,d,1); 96 if(flag) 97 { 98 printf("%d\n%d\n",-1,i);//输出第一个使 flag = true 的订单编号 99 return 0; 100 } 101 } 102 printf("0\n"); 103 return 0; 104 }