P9474 [yLOI2022] 长安幻世绘

文章讲述了如何解决一个关于数列的编程问题,即在给定的元素中找出一个长度为m且元素互不相邻的子列,使其极差最小。通过排除动态规划和爆搜,提出了一种双指针策略和利用线段树维护前后缀长度的方法,以优化时间复杂度并实现AC代码。
摘要由CSDN通过智能技术生成

去洛谷看我的博客

题目大意

在元素互不相同的数列 a a a 中选出一个长度为 m m m 的元素互不相邻的子列,使得子列的极差最小。

思路

爆搜、 d p dp dp 肯定是过不了的,所以我们考虑固定某个值,赛时想到了固定最大或者最小值,然后找到另一个值,但是除了 d p dp dp 没想到好做法,比赛结束了才知道正解居然是同时固定最大和最小值。

当我们固定最大和最小值时,先从大小考虑,在这中间的数一定都能选,那么我们就只用算出用这些数可以凑出多长的不相邻的序列。

但是无论是枚举最大和最小值,还是计算最长序列的长度,复杂度都过高,所以我们还需要进一步优化。

首先是选取最大和最小值上面,我们可以考虑先对原序列进行排序,然后用双指针逐步加数至满足序列长度达到要求,统计答案后再右移左指针,然后右移右指针直到新的序列长度也满足条件。

那么我们可以考虑只计算加入一个新数字,对最长序列长度的影响。

  • 若新加入的数字在原序列前后都没有选择的数字,那么加入进来,最长序列长度必定加 1 1 1

  • 若新加入的数字只有有一边有选择的数字,那么只要这连续的数字一共有偶数个,那么也可以让长度加 1 1 1

  • 若新加入的数字两边都有选择的数字,则两边数字个数都是偶数个,长度才能加 1 1 1

那么我们考虑用线段树维护前后缀长度,每次判断新加入的数字前面的后缀与后面的前缀即可。

AC 代码

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
struct node{int pre,suf,l,r,len;}t[400005];
struct deng{int v,id;}a[100005];
inline bool cmp(deng a,deng b){return a.v<b.v;}
int n,m,ans=0x3f3f3f3f,l,r,siz,qian,hou;
/*线段树部分*/
inline node pushup(node &p,node pl,node pr)
{
	p.len=pl.len+pr.len;
    p.pre=(pl.pre==pl.len)?pl.len+pr.pre:pl.pre;
    p.suf=(pr.suf==pr.len)?pr.len+pl.suf:pr.suf;
    return p;
}
void build(int p,int l,int r)
{
    t[p].l=l,t[p].r=r,t[p].len=t[p].r-t[p].l+1;
    if(l==r) return;
    int mid=l+r>>1;
    build(p<<1,l,mid),build(p<<1|1,mid+1,r);
}
void update(int p,int x,int k)
{
    if(t[p].l==t[p].r){t[p].pre=t[p].suf=k;return;}
    int mid=t[p].l+t[p].r>>1;
    if(mid>=x) update(p<<1,x,k);
    else update(p<<1|1,x,k);
    pushup(t[p],t[p<<1],t[p<<1|1]);
}
node mer_l(int p,int x)//查询x左侧的后缀和
{
    if(t[p].r<=x) return t[p];
    int mid=t[p].l+t[p].r>>1;node k;
    if(mid>=x) return mer_l(p<<1,x);
    else return pushup(k,t[p<<1],mer_l(p<<1|1,x)); 
}
node mer_r(int p,int x)//查询x右侧的前缀和
{
    if(t[p].l>=x) return t[p];
    int mid=t[p].l+t[p].r>>1;node k;
    if(mid<x) return mer_r(p<<1|1,x);
    if(mid>=x) return pushup(k,mer_r(p<<1,x),t[p<<1|1]);
}
int main()
{
    scanf("%d%d",&n,&m);
    build(1,1,n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i].v),a[i].id=i;
    sort(a+1,a+n+1,cmp);
    for(l=1;l<=n;l++)
    {
        while(siz<m&&r<n)//右指针右移至最长长度满足题意为止
        {
            r++,update(1,a[r].id,1);//代表该数可选
/*计算前后缀*/
            qian=(a[r].id==1)?0:mer_l(1,a[r].id-1).suf;
            hou=(a[r].id==n)?0:mer_r(1,a[r].id+1).pre;
/*判断序列能否增加长度*/
            if(!hou&&!qian) siz++;
            else if(!hou&&qian&&qian%2==0||hou&&!qian&&hou%2==0) siz++;
            else if(hou&&qian&&hou%2==0&&qian%2==0) siz++;
        }
        if(siz==m)//满足条件
        {
            if(ans>a[r].v-a[l].v) ans=min(ans,a[r].v-a[l].v);//更新答案
            update(1,a[l].id,0);//把这个数去掉
            qian=(a[l].id==1)?0:mer_l(1,a[l].id-1).suf;
            hou=(a[l].id==n)?0:mer_r(1,a[l].id+1).pre;
            if(!hou&&!qian) siz--;
            else if(!hou&&qian&&qian%2==0||hou&&!qian&&hou%2==0) siz--;
            else if(hou&&qian&&hou%2==0&&qian%2==0) siz--;//同上,不过这里是计算去掉是否影响长度
        }
    }
    printf("%d",ans);
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值