『线段树简单运用』

本文深入探讨线段树的应用,通过农场分配与旅馆预订两道经典例题,解析线段树如何高效处理区间查询与更新问题。文章详细讲解了线段树的构建、查询、更新机制及懒惰传播技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

『线段树简单运用』

更新了例题部分
更新了总结部分


线段树的基础博客见这篇『线段树 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\)函数,该如何修改

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


转载于:https://www.cnblogs.com/Parsnip/p/10503087.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值