『线段树简单运用』
『线段树简单运用』
<更新提示>
<第一次更新> 更新了例题部分
<第二次更新>更新了总结部分
<正文>
线段树的基础博客见这篇『线段树 Segment Tree』,这里通过两道例题来深入理解。
农场分配
Description
Farmer John最近新建立了一个农场,并且正在接受奶牛的畜栏分配请求,有些 畜栏会看到农场美妙的风景。😃
农场由N (1 <= N <= 100,000) 个畜栏组成,编号为1..N,畜栏i可以最多容纳C_i只奶牛 (1 <= C_i <= 100,000)。奶牛i希望得到连续的一段畜栏,表示为一段区间 (A_i,B_i) 。 这样的话奶牛可以在这段牛棚里面转悠。(当然,这段畜栏必须要有足够的空间)
给出M (1 <= M <= 100,000) 个请求,请求出不超过畜栏承载量的情况下,最多可以满足的请求数。
考虑下面展示的一个农场:
编号 1 2 3 4 5
容量 | 1 | 3 | 2 | 1 | 3 |
奶牛1 (1, 3)
奶牛2 (2, 5)
奶牛3 (2, 3)
奶牛4 (4, 5)
FJ 不能够同时满足4头奶牛的请求,否则3,4号畜栏就会有太多的奶牛。
考虑到奶牛2的请求需要一个区间包含3号和4号畜栏,我们尝试这样一种方案,让1,3,4号奶牛 的请求都得到满足,这样没有畜栏超出容量的限制,因此,对于上述情况的答案就是3,三头奶牛 (1,3,4号)的要求可以得到满足。
Input Format
-
第1行:两个用空格隔开的整数:N和M
-
第2行到N+1行:第i+1行表示一个整数C_i
-
第N+2到N+M+1行: 第i+N+1行表示2个整数 A_i和B_i
Output Format
- 第一行: 一个整数表示最多能够被满足的要求数
Sample Input
5 4
1
3
2
1
3
1 3
2 5
2 3
4 5
Sample Output
3
解析
这就是一道线段树的简单运用吧。做过\(USACO\)"线段覆盖"的同学应该都知道,这两道题的本质其实都是一样的,是一个贪心。策略是将线段按右端点排序,依次尝试加入,如果可以加入,那么加入一定能构成最优解。
证明就不详细说了,大致可以这样理解:我们只在乎每一条线段在什么时候结束,而不在乎在什么时候开始,加入后,这一条线段将对之后的的所有线段没有任何影响,而影响它的是前面的线段,所以每一次先加入能加入的线段一定是最优解。
把它转换成线段树可以解决的问题:对于每一次查询当前线段能否加入,只需要查询当前线段所覆盖区间的最小值,若最小值大于\(1\),则可以加入。加入后,对这个区间做一个减\(1\)的区间减法即可。这个就可以利用线段树维护了。
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
inline void read(int &k)
{
int x=0,w=0;char ch;
while(!isdigit(ch))w|=ch=='-',ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
k=(w?-x:x);return;
}
const int N=100000+20,M=100000+20,INF=0x7f7f7f7f;
int n,m,a[N],ans=0;
struct section{int l,r;}sec[M];
struct SegmentTree
{
int l,r,Min,lazytag;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define Min(x) tree[x].Min
#define lazytag(x) tree[x].lazytag
}tree[N*4];
inline bool cmp(section a,section b){return a.r<b.r;}
inline void input(void)
{
read(n),read(m);
for(int i=1;i<=n;i++)
read(a[i]);
for(int i=1;i<=m;i++)
read(sec[i].l),read(sec[i].r);
}
inline void build(int p,int l,int r)
{
l(p)=l,r(p)=r;
if(l==r){Min(p)=a[l];return;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
Min(p)=min(Min(p*2),Min(p*2+1));
}
inline void spread(int p)
{
if(lazytag(p))
{
Min(p*2)+=lazytag(p);
Min(p*2+1)+=lazytag(p);
lazytag(p*2)+=lazytag(p);
lazytag(p*2+1)+=lazytag(p);
lazytag(p)=0;
}
}
inline void modify(int p,int l,int r,int d)
{
if(l(p)>=l&&r(p)<=r)
{
Min(p)+=d;
lazytag(p)+=d;
return;
}
spread(p);
int mid=(l(p)+r(p))/2;
if(l<=mid)modify(p*2,l,r,d);
if(r>mid)modify(p*2+1,l,r,d);
Min(p)=min(Min(p*2),Min(p*2+1));
}
inline int query(int p,int l,int r)
{
if(l(p)>=l&&r(p)<=r)return Min(p);
spread(p);
int mid=(l(p)+r(p))/2;
int res=INF;
if(l<=mid)res=min(res,query(p*2,l,r));
if(r>mid)res=min(res,query(p*2+1,l,r));
return res;
}
inline void solve(void)
{
sort(sec+1,sec+m+1,cmp);
for(int i=1;i<=m;i++)
{
int Min=query(1,sec[i].l,sec[i].r);
if(Min)
{
modify(1,sec[i].l,sec[i].r,-1);
ans++;
}
}
}
int main(void)
{
input();
build(1,1,n);
solve();
printf("%d\n",ans);
return 0;
}
hotel
Description
奶牛们最近的旅游计划,是到苏必利尔湖畔,享受那里的湖光山色,以及 明媚的阳光。作为整个旅游的策划者和负责人,贝茜选择在湖边的一家著名的 旅馆住宿。这个巨大的旅馆一共有N (1 <= N <= 50,000)间客房,它们在同一层 楼中顺次一字排开,在任何一个房间里,只需要拉开窗帘,就能见到波光粼粼的 湖面。
贝茜一行,以及其他慕名而来的旅游者,都是一批批地来到旅馆的服务台, 希望能订到D_i (1 <= D_i <= N)间连续的房间。服务台的接待工作也很简单: 如果存在r满足编号为r..r+D_i-1的房间均空着,他就将这一批顾客安排到这些 房间入住;如果没有满足条件的r,他会道歉说没有足够的空房间,请顾客们另 找一家宾馆。如果有多个满足条件的r,服务员会选择其中最小的一个。
旅馆中的退房服务也是批量进行的。每一个退房请求由2个数字X_i、D_i 描述,表示编号为X_i..X_i+D_i-1 (1 <= X_i <= N-D_i+1)房间中的客人全部 离开。退房前,请求退掉的房间中的一些,甚至是所有,可能本来就无人入住。
而你的工作,就是写一个程序,帮服务员为旅客安排房间。你的程序一共 需要处理M (1 <= M < 50,000)个按输入次序到来的住店或退房的请求。第一个 请求到来前,旅店中所有房间都是空闲的。
Input Format
-
第1行: 2个用空格隔开的整数:N、M
-
第2..M+1行: 第i+1描述了第i个请求, 如果它是一个订房请求,则用2个数字 1 D_i描述,数字间用空格隔开;
如果它是一个退房请求,用3个以空格隔开的数字2、X_i、D_i描述
Output Format
- 对于每个订房请求,输出1个独占1行的数字:如果请求能被满足,输出满足条件的最小的r;如果请求无法被满足,输出0
Sample Input
10 6
1 3
1 3
1 3
1 3
2 5 5
1 6
Sample Output
1
4
7
0
5
解析
这也是一道线段树的经典题,但是就不是简单的线段树模板了。
假设我们维护了一个\(0/1\)序列作为旅馆的房间是否入住(\(1\)为入住,\(0\)为空房),我们先提炼出题中要求我们的操作:
- 1.查询序列中连续至少\(len\)个\(0\)的最小左端点
- 2.区间变0
- 3.区间变1
仔细思考一下,我们其实需要维护一个量\(Max(x)\)表示线段树中节点\(x\)所代表区间的最长连续全\(0\)串的长度。为了更新维护\(Max(x)\),我们就要再新建两个变量\(lmax(x)\),\(rmax(x)\)代表节点\(x\)所代表区间的左端最长连续全\(0\)串的长度,右端最长连续全\(0\)串的长度。
那么它们可以这样维护:
由于是区间修改,所以我们还是要用\(lazytag\)标记的。因为有变\(0\)和变\(1\)的操作,所以\(lazytag\)需要有三种状态:\(-1\)代表不需要更改,\(0\)代表有变\(0\)操作的延迟标记,\(1\)代表有变\(1\)操作的延迟标记。然后在每一次询问和更改访问到时再下传标记即可。
对于这个特殊的询问,我们需要改一下原来模板中的\(query\)函数,改为返回左端点的位置,递归询问时,按照左,中,右的顺序询问即可得到合法的最小答案。
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
inline void read(int &k)
{
int x=0,w=0;char ch;
while(!isdigit(ch))w|=ch=='-',ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
k=(w?-x:x);return;
}
const int N=50000+20,M=50000+20;
int n,m;
struct SegmentTree
{
int l,r,lazytag,lmax,rmax,Max;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define lazytag(x) tree[x].lazytag
#define lmax(x) tree[x].lmax
#define rmax(x) tree[x].rmax
#define Max(x) tree[x].Max
}tree[N*4];
inline void update(int p)
{
Max(p)=max( max(Max(p*2),Max(p*2+1)) , rmax(p*2)+lmax(p*2+1) );
lmax(p)=lmax(p*2);
if(Max(p*2)==r(p*2)-l(p*2)+1)
lmax(p)=Max(p*2)+lmax(p*2+1);
rmax(p)=rmax(p*2+1);
if(Max(p*2+1)==r(p*2+1)-l(p*2+1)+1)
rmax(p)=Max(p*2+1)+rmax(p*2);
}
inline void build(int p,int l,int r)
{
l(p)=l,r(p)=r;lazytag(p)=-1;
if(l==r)
{
lmax(p)=rmax(p)=Max(p)=r(p)-l(p)+1;
return;
}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
update(p);
}
inline void spread(int p)
{
if(lazytag(p)==1)
{
Max(p*2)=lmax(p*2)=rmax(p*2)=r(p*2)-l(p*2)+1;
Max(p*2+1)=lmax(p*2+1)=rmax(p*2+1)=r(p*2+1)-l(p*2+1)+1;
lazytag(p*2)=lazytag(p*2+1)=1;
}
if(lazytag(p)==0)
{
Max(p*2)=lmax(p*2)=rmax(p*2)=0;
Max(p*2+1)=lmax(p*2+1)=rmax(p*2+1)=0;
lazytag(p*2)=lazytag(p*2+1)=0;
}
lazytag(p)=-1;
}
inline void modify(int p,int l,int r,int d)
{
if(l<=l(p)&&r>=r(p))
{
Max(p)=lmax(p)=rmax(p)=(r(p)-l(p)+1)*d;
lazytag(p)=d;
return;
}
spread(p);
int mid=(l(p)+r(p))/2;
if(l<=mid)modify(p*2,l,r,d);
if(r>mid)modify(p*2+1,l,r,d);
update(p);
}
inline int query(int p,int len)
{
if(l(p)==r(p))return l(p);
spread(p);
int mid=(l(p)+r(p))/2;
if(Max(p*2)>=len)return query(p*2,len);
if(rmax(p*2)+lmax(p*2+1)>=len)return mid-rmax(p*2)+1;
if(Max(p*2+1)>=len)return query(p*2+1,len);
}
inline void input(void)
{
read(n),read(m);
}
inline void solve(void)
{
for(int i=1;i<=m;i++)
{
int index,l,r,len;
read(index);
if(index==1)
{
read(len);
if(Max(1)<len)
{
printf("0\n");
continue;
}
l=query(1,len);
r=l+len-1;
modify(1,l,r,0);
printf("%d\n",l);
}
if(index==2)
{
read(l),read(len);
r=l+len-1;
modify(1,l,r,1);
}
}
}
int main(void)
{
input();
build(1,1,n);
solve();
return 0;
}
总结
对于线段树一类的题目,一定是让你维护区间的特征信息的,这是线段树的特点。而对于如何解决这一类问题,我们思考的主要有如下几点。
- 1.是否具有区间可加性,能否用线段树维护
- 2.如果可以,该怎样设计并维护每一个线段的关键值(如何设计\(update\)函数)
- 3.是否涉及区间修改,如果需要区间修改,怎么利用\(lazytag\)标记(如何设计\(spread\)函数)
- 4.是否需要修改\(query\)函数,该如何修改
这就是基础线段树应用题的要点了,当然,很多时候线段树是作为其他算法的辅助工具的,这更要求我们熟练掌握线段树的代码实现。
<后记>