今天学点更难的。
1.区间修改
区间修改,就是修改一整段的值。第一种方法是进行循环单点修改,但是这样复杂度显然太大……为了能节省复杂度,我们有一种方法,就是使用lazy标记,我们修改一个区间的时候,并不需要把区间中的所有值即刻修改,只要在访问的时候修改即可。那么我们对于修改的区间打上lazy标记,记录要修改的值即可。
要注意一下!如果是求和的话一定要在下放lazy标记的时候乘以区间长度!(题目数据太水没意识到这点的蒟蒻泪流满面……)不过取最大最小值不需要。
看一下代码
void down(int x,int l,int r)//区间修改无需每次都修改到叶节点,所以我们用lazy标记记录延迟修改 {
int mid = (l+r) >> 1; if(lazy[x]) { tree[x<<1] += lazy[x] * (mid-l+1); tree[x<<1|1] += lazy[x] * (r-mid); lazy[x<<1] += lazy[x]; lazy[x<<1|1] += lazy[x]; lazy[x] = 0; } } void add(int p,int l,int r,int kl,int kr,int val) { if(kl == l && kr == r) { tree[p] = val*(r-l+1); lazy[p] = val; return; } down(p,l,r); int mid = (l+r) >> 1; if(kr <= mid) add(p<<1,l,mid,kl,kr,val); else if(kl > mid) add(p<<1|1,mid+1,r,kl,kr,val); else { add(p<<1,l,mid,kl,mid,val); add(p<<1|1,mid+1,r,mid+1,kr,val); } tree[p] = tree[p<<1] + tree[p<<1|1]; }//区间修改,当区间完全覆盖的时候直接修改,否则先下放标记之后二分修改 //注意修改的时候,整个区间的值要修改为乘积,但是lazy标记只是改变的值即可
要注意修改其实有两种方法,第一种是严格的划分,必须kl==l&&kr==r才修改。这种修改需要把访问的区间分割。(我称之为精准扶贫)
第二种方法就是不改变需要访问的区间,只要当前区间被包含在访问区间中就直接累加它的值。这种方法在递归的时候条件不同,只要kl<=mid | | kr>mid就直接在新的区间之内计算即可。(代码在线段树初步(1)中有)
注意区间修改等等不只局限于求和,还有取min/max之类的操作,上道例题试一下。
售票系统
【问题描述】
某次列车途经C个城市,城市编号依次为1到C,列车上共有S个座位,铁路局规定售出的车票只能是坐票,即车上所有的旅客都有座,售票系统是由计算机执行的,每一个售票申请包含三个参数,分别用O、D、N表示,O为起始站,D为目的地站,N为车票张数,售票系统对该售票申请作出受理或不受理的决定,只有在从O到D的区段内列车上都有N个或N个以上的空座位时该售票申请才被受理,请你写一个程序,实现这个自动售票系统。
【输入格式】
第一行包含三个用空格隔开的整数C、S和R,其中1<=C<=60000,1<=S<=60000,1<=R<=60000,C为城市个数,S为列车上的座位数,R为所有售票申请总数。接下来的R行每行为一个售票申请,用三个由空格隔开的整数O,D和N表示,O为起始站,D为目的地站,N为车票张数,其中1<=0<D<=C,1<=N<=S,所有的售票申请按申请的时间从早到晚给出。
【输出格式】
共有R行,每行输出一个“YES”或“NO”,表示当前的售票申请被受理或不被受理。
【输入样例】
4 6 4
1 4 2
1 3 2
2 4 3
1 2 3
【输出样例】
YES
YES
NO
NO
这道题我们把城市之间的路看成一条条线段,那么我们的任务就是用一棵线段树维护每条路上的车票是否够用。因为我们要保证每条都够用所以必须在所有节点中取最小值,与需求值比较即可。
上代码看一下。
#include <cstdio> #include <iostream> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar(' ') using namespace std; typedef long long ll; const int N = 100005; int c,s,r; struct Seg { int v,lazy; }t[N<<2]; int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } void pushdown(int p) { if(t[p].lazy) { t[p<<1].v += t[p].lazy; t[p<<1|1].v += t[p].lazy; t[p<<1].lazy += t[p].lazy; t[p<<1|1].lazy += t[p].lazy; t[p].lazy = 0; } } void build(int p,int l,int r) { if(l == r) { t[p].v = s; return; } int mid = (l+r)>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); t[p].v = min(t[p<<1].v,t[p<<1|1].v);//建造的时候即取小 } int query(int p,int l,int r,int kl,int kr) { if(kl == l && kr == r) return t[p].v; pushdown(p); int mid = (l+r) >> 1; if(kr <= mid) return query(p<<1,l,mid,kl,kr); else if(kl > mid) return query(p<<1|1,mid+1,r,kl,kr); else return min(query(p<<1,l,mid,kl,mid),query(p<<1|1,mid+1,r,mid+1,kr)); }//使用的是精准扶贫的分割方法 void modify(int p,int l,int r,int kl,int kr,int val) { if(kl == l && kr == r) { t[p].v += val,t[p].lazy += val; return; } pushdown(p); int mid = (l+r) >> 1; if(kr <= mid) modify(p<<1,l,mid,kl,kr,val); else if(kl > mid) modify(p<<1|1,mid+1,r,kl,kr,val); else modify(p<<1,l,mid,kl,mid,val),modify(p<<1|1,mid+1,r,mid+1,kr,val);//同样是精准扶贫 t[p].v = min(t[p<<1].v,t[p<<1|1].v);//每次修改返回的时候需要更新 } int main() { c = read(),s = read(),r = read(); build(1,1,c); while(r--) { int o,d,n; o = read(),d = read(),n = read(); d--;//因为我们要计算的是区间,所以-- if(query(1,1,c,o,d) >= n) { printf("YES "); modify(1,1,c,o,d,-n);//因为座位变少所以-- } else printf("NO "); } return 0; }
2.区间合并
蒟蒻的智商要不够用了!这个东西感觉有点难理解……
我们有时要求最长连续区间是多少,那我们怎么用线段树维护呢?
在建树的时候我们把节点可用区间长度,其左子树可用区间长,右子树可用区间长都设置为其控制的范围长(递归建树的话就是r-l+1)。之后其实区间修改是最相似的,主要不同在于下放标记,返回修改和查询值。
先说下放标记。lazy标记此时有几种状态,可以分别代表未使用,占用和未占用(未占用和未使用是两个概念)也可以有更多的情况。如果他不是未使用,那么我们在下放标记的时候就把其左右儿子的状态设为与之相同。如果是占用,那么其左右子全部可用的区间长度都为0,如果不占用,那么就修改为区间的长度的一半。(这段似乎很难理解……蒟蒻在这里卡壳了……)
返回时候的修改呢?咋修改?这段看代码的注释吧,不过一定要取max。
查询值的话……这次使用的是包含法的查询。注意可能出现的区间位置有3,一是全在左子树中,一是全在右子树中,另一种是左子树的右区间加上右子树的左区间。
算法其实就这么多,但说句实话不好理解。
看一道例题,上一下代码吧。
Hotel有N(1 ≤ N ≤ 50,000)间rooms,并且所有的rooms都是连续排列在同一边,groups需要check in 房间,要求房间的编号为连续的r..r+Di-1并且r是最小的;visitors同样可能check out,并且他们每次check out都是编号为Xi ..Xi +Di-1 (1 ≤ Xi ≤ N-Di+1)的房间,题目的输入有两种样式:
1 a : groups需要check in a间编号连续的房间
2 a b : visitors check out 房间,其中房间编号是 a…a+b-1
要求对于每次request,输出为groups分配数目为a的房间中编号最小的房间编号
【输入格式】
Line 1: 两个整数: N M
Lines 2..M+1: (a) check-in 查询: 1 Di ,表示需要Di个房间;
(b) check-out: 2 Xi Di ,表示退掉从Xi开始的Di个房间;
【输出格式】
对于每次check in 查询,若能满足,输出可分配的房间最小号码,否则输出0
【输入样例】10 6
1 3
1 3
1 3
1 3
2 5 5
1 6
【输出样例】
1
4
7
0
5
(题目描述有点病请见谅)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cstdlib> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar(' ') using namespace std; const int M = 100005; struct seg { int lazy,lsum,msum,rsum; }t[M<<2]; //lazy:-1表示当前没有覆盖标记,1表示均覆盖为不可行,0表示均覆盖为可行 //lsum:该区间从左起连续的可用区间长度的最大值 //msum:该区间中连续的可用区间长度的最大值 //rsum:该区间从右起连续的可用区间长度的最大值 int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } void pushup(int p,int m)//m表示区间的长度 { t[p].lsum = t[p<<1].lsum,t[p].rsum = t[p<<1|1].rsum; if(t[p].lsum == m-(m>>1)) t[p].lsum += t[p<<1|1].lsum; /*如果左孩子全部为可用区间,那么加上右孩子的左端 因为m是要修改的区间的长度,t[p].lsum == m-(m>>1)说明左孩子全部可用,那么就加上右孩子的左端*/ if(t[p].rsum == m>>1) t[p].rsum += t[p<<1].rsum; /*同上*/ t[p].msum = max(max(t[p<<1].msum,t[p<<1|1].msum),t[p<<1].rsum + t[p<<1|1].lsum); /*该区间的可用区间可能是:左孩子最大的可用区间、右孩子最大的可用区间,和跨越左右孩子加在一起的可用区间*/ } void pushdown(int p,int m) { if(t[p].lazy != -1)//表示还没有动过 { t[p<<1].lazy = t[p<<1|1].lazy = t[p].lazy; if(t[p].lazy == 1)//如果处于占用状态 { t[p<<1].msum = t[p<<1].lsum = t[p<<1].rsum = 0; t[p<<1|1].msum = t[p<<1|1].lsum = t[p<<1|1].rsum = 0; //那么这个节点的可用区间和它的左右孩子的可用区间全部为0 } else//表示处于空闲状态 { t[p<<1].msum = t[p<<1].lsum = t[p<<1].rsum = m-(m>>1); t[p<<1|1].msum = t[p<<1|1].lsum = t[p<<1|1].rsum = m>>1; //那么就把这个节点的可用区间长度修改为线段长度的左右两半 } t[p].lazy = -1; /*千万不要忘记将rt清为-1*/ } } int query(int w,int l,int r,int p) { if(l == r) return l; pushdown(p,r-l+1); int mid = (l + r) >> 1; if(t[p<<1].msum >= w) return query(w,l,mid,p<<1);/*由于要找最左边的区间,按照左孩子、跨越两者、有孩子的顺序查找*/ if(t[p<<1].rsum + t[p<<1|1].lsum >= w) return mid - t[p<<1].rsum + 1; return query(w,mid+1,r,p<<1|1); } void update(int kl,int kr,int o,int l,int r,int p)//o表示当前是开房还是退房 { if(kl <= l && kr >= r) { t[p].lazy = o; if(o == 1) t[p].msum = t[p].lsum = t[p].rsum = 0; else t[p].msum = t[p].lsum = t[p].rsum = r-l+1; return; } pushdown(p,r-l+1);//这里是l和r,不要写成kl和kr int mid = (l+r) >> 1; if (kl <= mid) update(kl,kr,o,l,mid,p<<1); if (kr > mid) update(kl,kr,o,mid+1,r,p<<1|1); pushup(p,r-l+1); } void build(int l,int r,int p) { t[p].msum = t[p].lsum = t[p].rsum = r-l+1;//一开始能使用的区间长度都是其所控制的区间长 t[p].lazy = -1; if (l == r) return; int mid = (l+r) >> 1; build(l,mid,p<<1); build(mid+1,r,p<<1|1); } int n,m,op; int main() { n = read(),m = read(); build(1,n,1); rep(i,0,m-1) { op = read(); if(op == 1)//开房 { int w = read();//代表要开房的数量 if (t[1].msum < w) printf("0 "); /*如果根的可用区间已经小于w,那么一定是找不到长度为w的可用区间*/ else { int p = query(w,1,n,1); printf("%d ",p); update(p,p+w-1,1,1,n,1);//从p开始的房间被占用 } } else//退房 { int u = read();int v = read(); update(u,u+v-1,0,1,n,1); } } return 0; } /* 10 6 1 3 1 3 1 3 1 3 2 5 5 1 6 */