牛客周赛 Round 122

A ICPC Problems

B Chess

很像周赛120的马,不过这个题是象,但是思路也差不太多

首先 的时候,只能在初始位置,所以只有一个位置

然后考虑 的情况,注意到象和马很大一个区别是马由于每次跳 ,棋盘够大时可以跳到任意位置,但象由于每次跳的是 ,所以每次跳动横纵坐标的奇偶性都无法改变,所以我们统计所有横纵坐标都为奇数的位置(数量记为 )(因为从 开始的奇数的数量一定大于等于偶数),

又因为每次跳 决定了象不能去到距离为 的点位,也就是说这 个位置中只有一半能取到,因此答案就是

C Sequence Cost

很典的贪心,主要证明最优策略比较麻烦,但猜个结论还是挺容易猜到的

我们不妨先研究要进行至少一次操作的情况下怎么做最优,这样再跟一次操作都不做(很容易统计答案)比较取最小即可

很容易能想到既然要做操作的话,那么如果能一次性把整个序列全部变为最小值的话,第二步拿走整个序列的时候花费肯定最小,不过这时这一次操作的花费就是整个序列的最大值,这样是不是最优呢?

也就是我们猜测:**只做一次操作:选择区间 ,花费最大值(记为 )的代价,将序列中所有数变为最小值(记为 ),然后再拿走这个序列,总花费 **是最优策略,下面来证明这个结论(事实上贪心很多题都是先猜再证的)(下文把此结论简称为结论)

比较同样只做一次操作的情况:

  • 如果选择区间包括 并且不为 ,那么第一步操作花费相同,但至少一个数没有被包含在区间里,其中可能存在大于 的数,拿走序列时花费可能更大,所以此方案比结论一定不更优

  • 如果不包括 ,我们假设第一步花费为 ,那么此时总花费至少为 (最好情况下最大值在边上,这个区间可以最大化,操作后序列为 ),而 ,因此此方案依然不比结论优

再考虑做多于一次操作的情况:

  • 如果有至少一次操作选择区间 ,显然不如结论

  • 如果没有一次操作选择区间 ,那么每一次选择同样有两种可能:包含 和不包含 ,而只要包含 ,那一定不如把区间扩张到 (因为相同的花费能将更多的数变小)

  • 如果没有一次操作包含 ,最后拿走序列时的花费一定大于等于 ,而操作的花费一定大于等于 (至少两次操作),总花费大于等于 ,依然不比结论优

所以我们就证明了如果要操作,结论一定是最优的,因此对每次询问的数列统计一次不操作的结果( )和按照结论操作的结果(),输出较小即可

D Digital Deletion

算个数学题?不过更像模拟一点

首先搞清楚 这个东西的定义:集合中没出现过的最小正整数

我们先思考如果我们明确一个集合的所有元素要怎么求这个集合的 :显然先排序,再用一个指针指向当前没出现过的最小正整数(从 开始),接着扫描数列直到出现空缺为止(如指针指向 但数列中 过了就是

再来看这个题。本题中集合中元素是由数列的所有子序列的和构成的,显然这个集合元素可能非常多,我们难以直接求这个集合的所有元素,不能用常规方法找

可以明确的是如果一个数 已经比 大,那么比 更大的数显然也能被删掉而不受影响。因此我们先把序列从小到大排序,找到能被删掉的边界,后面的就都能被删掉了。而找边界就是在找第一个对 无贡献的数,所以我们可以先找序列的

怎么求 呢?我们假设已经求出 前 个数的 ,思考 的贡献 。烧烤一下发现,对于前 个数, 一定有至少一种拼凑的方法。 所以加上 后新增的区间就是 ,只要这个区间中出现 ,此时本来没出现过的 就出现在了集合中,** 成功将 更新为了 **(由于 此前没出现过,而前 个数也没有比 更大的,因此 必然没出现过)

答案显而易见了,对每一个 ,判断一下 前 个数的 是否包含在区间 中,等效于判断 是否等于 或者是否大于 ,如果是的话答案加一,如果不是更新

稍加优化,碰到 就统计进答案,之后非 的部分一直更新 直到遇到 ,那么后面的 个数都能删掉。由于 ,当 之后也可以直接停止,因为后面的肯定都比

附个🐎

typedef long long lld;
const int maxn=2e5+5;
int a[maxn];
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n,ans=0;
        lld now=1;//存储当前Mex的值
        scanf("%d",&n);
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        sort(a+1,a+n+1);
        for(int i=1;i<=n;i++){
            if(a[i]==0){//0直接累计进答案
                ans++;
                continue;
            }
            if(now>1e9) break;
            else if(a[i]>now){
                ans+=n-i+1;
                break;
            }
            now+=a[i];//更新Mex
        }
        printf("%d\n",ans);
    }
    return 0;
}

E Block Array

算个冻柜吧应该

噼里啪啦写一堆发现样例都过不了,原来连续出现次数可以比元素更大。。。

好数组的基础是块。判断一坨是不是块,直接用一个变量 记录当前以 为止连续出现的相等元素的数目,显然当 的时候, 可以作为一个块的终点。

再考虑找到块之后怎么找好数组数量。这里直接用动态规划的思想递推。定义一个数组 表示以 结尾的块作为最后一块时有几个好数组。显然结尾块前面有 个连续的块时,就有 个好数组。递推公式 就是最后的答案。

说实话感觉比D题简单 ?!!

注意初始化!!!

typedef long long lld;
const int maxn=5e5+5;
int a[maxn],k[maxn];
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        int now=1,len=1;
        lld ans=0;
        while(now<=n){
            k[now]=0;//初始化
            if(a[now]<=len) k[now]=k[now-a[now]]+1;
            if(a[now]==a[now+1]) len++;
            else len=1;
            now++;
        }
        for(int i=1;i<=n;i++) ans=ans+k[i];
        printf("%lld\n",ans);
    }
    return 0;
}

F Sequence Covering

又是给一个数列,又是一道贪心

首先明确字典序:从前往后依次比较元素,只要一处比出大小关系就停止

显然前面的元素越大越好

我们假设前 个元素已经搞到最大,现在还剩余 的成本。由于前面的越大越好,我们显然就会去找 这个区间里的最大值。由于操作区间 花费为 的神奇特性,如果有多个最大值时分成几段操作能节省成本(比如直接操作 这个区间花费 ,但是操作 两个区间只花费 ,如果 都是最大值采取第二种方法更新更优),因此我们定义 为区间中第一个最大值。考虑以下情况:

  • 如果 ,则 这个区间就都更新为 ,至于 后面的,由于查找最大值区间变为了 ,最大值有可能因此变大,所以我们将 更新为 更新为
  • 如果 更新花费 ,用 更新花费 (只需更新到 ),没区别,因此归入上一种情况
  • 如果 ,直接将 这个区间覆盖为 (因为摸不到后面的更大的数了), 直接更新为 结束循环

这样基本的思路就确定了,但如果每次通过遍历的方式查找区间最大值,时间效率为 ,会超市,因此我们需要优化一下查询区间最大值的方法。当然方法很多,我这里使用了线段树优化。

思路整体比较简单,但前面的题如果没想明白容易时间不够,导致几种情况没想清楚。

代码如下:

const int maxn=2e5+5;
int a[maxn];
int t[maxn<<4],ls[maxn<<4],rs[maxn<<4],cnt,root;//注意线段树空间开销
//t[i]表示编号为i的区间的第一个最大值的下标,ls[i],rs[i]分别表示编号为i的区间的左儿子,右儿子
//cnt用于分配编号,root存根节点(事实上就是1)
void build(int &x,int l,int r){//建树
    x=++cnt;
    if(l==r){
        t[x]=l;
        return ;
    }
    int mid=(l+r)>>1;
    build(ls[x],l,mid);
    build(rs[x],mid+1,r);
    if(a[t[ls[x]]]>=a[t[rs[x]]]) t[x]=t[ls[x]];//左儿子最大值等于右儿子时优先使用左儿子确保t[x]存储第一个最大值
    else t[x]=t[rs[x]];
}
int Query(int x,int l,int r,int L,int R){
    if(L<=l&&r<=R) return t[x];
    int mid=(l+r)>>1,ll=0,rr=0;
    if(L<=mid) ll=Query(ls[x],l,mid,L,R);
    if(R>mid) rr=Query(rs[x],mid+1,r,L,R);
    if(a[ll]>=a[rr]) return ll;
    return rr;
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n,k;
        scanf("%d%d",&n,&k);
        a[0]=-1e9-5;//注意初始化
        cnt=0;//同上
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        build(root,1,n);
        int now=1;
        while(k&&now<=n){
            int x=Query(1,1,n,now,min(now+k,n));
            if(a[x]<a[now-1]){
                for(int i=now;i<=now+k-1;i++) a[i]=a[now-1];
                k=0;
            }else{
                for(int i=now;i<=x;i++) a[i]=a[x];
                k-=x-now;
                now=x+1;
            }
        }
        for(int i=1;i<=n;i++) printf("%d ",a[i]);
        printf("\n");
    }
    return 0;
}