P9474 [yLOI2022] 长安幻世绘

发布时间 2023-08-22 20:04:24作者: One_JuRuo

题目大意

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

思路

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

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

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

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

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

  • 若新加入的数字在原序列前后都没有选择的数字,那么加入进来,最长序列长度必定加 \(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;
}