『线段树简单运用』

『线段树简单运用』

<更新提示>

<第一次更新> 更新了例题部分
<第二次更新>更新了总结部分


<正文>

线段树的基础博客见这篇『线段树 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\)串的长度。

那么它们可以这样维护:

\[Max(x)=\max\begin{cases}\max\{Max(2x),Max(2x+1)\}\\rmax(2x)+lmax(2x+1)\end{cases} \\ \ \\lmax(x)=\max\begin{cases}lmax(2x)\\Max(2x)+lmax(2x+1)\ (Max(2x)=len(2x)) \end{cases} \\ \ \\rmax(x)=\max\begin{cases}rmax(2x+1)\\Max(2x+1)+rmax(2x)\ (Max(2x+1)=len(2x+1)) \end{cases} \]

由于是区间修改,所以我们还是要用\(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\)函数,该如何修改

这就是基础线段树应用题的要点了,当然,很多时候线段树是作为其他算法的辅助工具的,这更要求我们熟练掌握线段树的代码实现。


<后记>

posted @ 2019-03-09 21:17  Parsnip  阅读(315)  评论(0编辑  收藏  举报