【解题报告】Codeforces Deltix Round, Summer 2021 | A ~ E

本文详细解读了Codeforces Deltix Round夏季2021的五道题目:A~E,包括操作序列的策略、位置调整、压缩括号序列的计数、数字猜测技巧和平衡子区间的最少操作。通过实例和代码展示了解题思路和算法实现。
摘要由CSDN通过智能技术生成

A:A Variety of Operations

题意

  • T T T 组样例,每组给你一个 c , d c,d c,d
    一开始你有 a = b = 0 a=b=0 a=b=0
    每一次操作,你可以选择一个正整数 k k k,然后做下述操作之一
    (1)两个数都加 k k k
    (2) a a a k k k b b b k k k
    (3) a a a k k k b b b k k k
    问你最少步数让 a = c , b = d a=c,b=d a=c,b=d
    若无法做到则输出 − 1 -1 1
  • 1 ≤ T ≤ 1 0 4 1\le T\le 10^4 1T104
    0 ≤ c , d ≤ 1 0 9 0\le c,d\le 10^9 0c,d109

思路

  • c = d = 0 c=d=0 c=d=0,则步数为 0 0 0
    c = d > 0 c=d>0 c=d>0,则步数为 1 1 1
    a b s ( c − d ) abs(c-d) abs(cd) 是奇数,则无法做到,因为两数同加或者一加一减,两数的差的奇偶性是不变的。
    a b s ( c − d ) abs(c-d) abs(cd) 是偶数,则两步即可,第一步都加到平均数,第二步一加一减即可。

代码

  • 时间复杂度: O ( T ) O(T) O(T)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
#define ll long long
const int MAX = 2e6+50;

int main()
{
    int t;scanf("%d",&t);
    while(t--){
        int ta,tb;
        scanf("%d%d",&ta,&tb);
        if(ta > tb)swap(ta,tb);
        if((tb - ta) % 2){
            puts("-1");
        }else{
            if(ta == tb && ta == 0)puts("0");
            else if(ta == tb)puts("1");
            else puts("2");
        }
    }
    return 0;
}

B:Take Your Places!

题意

  • 一开始给你一个长度为 n n n 的序列 a n a_n an
    你每次操作,可以选择相邻的两个下标 i , j i,j i,j,满足 ∣ i − j ∣ = 1 |i-j|=1 ij=1,然后交换 a i , a j a_i,a_j ai,aj
    问你最少操作次数,满足相邻两个数的奇偶性都不同
    若无法做到,则输出 − 1 -1 1

思路

  • 一共有四种最终的可能性:
    J O J ⋯ O J O J O ⋯ J O   J O ⋯ J O O J ⋯ O J JOJ\cdots OJ\\ OJO\cdots JO\\\ \\ JO\cdots JO\\ OJ\cdots OJ\\ JOJOJOJOJO JOJOOJOJ
    我们枚举每一种情况。
    现在假设第一个数从 J J J 开始
    我们遍历数组,找到第一个 J J J 的位置,那么第一个奇数移动到位置 1 1 1 一定是最少次数的。
    然后同理。第 i i i 个奇数移动到位置 2 i − 1 2i-1 2i1 是最优的
    第一个数是 O O O 也是同理。答案取最小值即可

代码

  • 时间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
#define ll long long
const int MAX = 1e5+50;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
int aa[MAX];

int main()
{
    int t;scanf("%d",&t);
    while(t--){
        int n;scanf("%d",&n);
        int even = 0,odd = 0;
        for(int i = 1;i <= n;++i){
            scanf("%d",&aa[i]);
            if(aa[i]&1)odd++;
            else even++;
        }
        if(abs(even-odd) > 1)puts("-1");
        else{
            ll ans = LINF;
            if((n+1)/2 == odd){
                // odd even ...
                int fir = 1;
                ll tmp = 0;
                for(int i = 1;i <= n;++i){
                    if(aa[i]%2 == 1){
                        tmp += abs(i-fir);
                        fir += 2;
                    }
                }
                ans = min(ans,tmp);
            }

            if((n+1)/2 == even){
                // even odd ...
                int fir = 1;
                ll tmp = 0;
                for(int i = 1;i <= n;++i){
                    if(aa[i]%2 == 0){
                        tmp += abs(i-fir);
                        fir += 2;
                    }
                }
                ans = min(ans,tmp);
            }
            printf("%lld\n",ans);
        }
    }
    return 0;
}

C:Compressed Bracket Sequence

题意

  • 给你一个压缩过的括号序列 c n c_n cn
    其中奇数位置 c i c_i ci 表示有 c i c_i ci 个左括号
    其中偶数位置 c i c_i ci 表示有 c i c_i ci 个右括号
    现在问你,有多少个子区间是合法的括号序列(子区间是解压后的子区间)
  • 1 ≤ n ≤ 1000 1\le n\le 1000 1n1000
    1 ≤ c i ≤ 1 0 9 1\le c_i\le 10^9 1ci109

思路

  • 看到范围肯定默认 O ( n 2 ) O(n^2) O(n2) 去做。
    我们肯定考虑有多少个子区间的左端点是在 c i c_i ci 这里开始的,然后暴力 f o r for for 过去。
    c i = z u o c_i=zuo ci=zuo ,我们枚举 j = i + 1   t o   n j=i+1\ to\ n j=i+1 to n
    d u o = 0 duo=0 duo=0
  • 如果 j j j 是奇数,那么我们 d u o = d u o + c j duo=duo+c_j duo=duo+cj,表示我们必须要把这些多的左括号匹配完之后,才能多一些合法的子区间满足左端点是在 c i c_i ci 这里取到的
  • 如果 j j j 是偶数,那么我们要优先把 d u o duo duo 的左括号匹配完。
    如果 d u o duo duo 的左括号匹配完了,那么就把 z u o zuo zuo 与剩下的 c j c_j cj 去匹配。
    如果 d u o duo duo 的左括号匹配完了,且 j ≠ i + 1 j\ne i+1 j=i+1,那么方案数要多一个 (想一想为什么)
    如果 z u o zuo zuo 的左括号匹配完了,且 a j a_j aj 的有括号还有剩余,那么后面都是非法的情况了。

代码

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
#define ll long long
const int MAX = 1e3+50;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;

int aa[MAX];

int main()
{
    int n;scanf("%d",&n);
    for(int i = 1;i <= n;++i)scanf("%d",&aa[i]);
    ll ans = 0;
    for(int i = 1;i <= n;i+=2){
        ll zuo = aa[i];
        ll duo = 0;
        for(int j = i+1;j <= n;++j){
            if(j&1){
                duo += aa[j];		// 多余的左括号要匹配完
            }else{
                ll tmp = aa[j];
                ll mn = min(tmp,duo);		// 优先匹配多余的左括号
                tmp -= mn;duo -= mn;
                if(duo == 0){
                    mn = min(tmp,zuo);

                    ans += mn;				// 然后拿 a_i 的左括号与剩下的 a_j 的右括号匹配
                    if(j != i+1)ans++;		// 想一想,为什么

                    zuo -= mn;
                    tmp -= mn;
                    if(tmp)break;			// 右括号多了,肯定后面都是非法子区间了
                }
            }
        }
    }
    printf("%lld",ans);
    return 0;
}

D:Take a Guess

题意

  • 给你一个 n , k n,k n,k
    他有一个隐藏的序列 a n a_n an,但是不告诉你,你需要猜出其中的第 k k k 小的数是多少
    你最多有 2 n 2n 2n 次操作,每次可以询问:
    (1) a n d   i   j and\ i\ j and i j,系统给你返回 a i & a j a_i\&a_j ai&aj,即位与运算
    (2) o r   i   j or\ i\ j or i j,系统给你返回 a i ∣ a j a_i|a_j aiaj,即位或运算
    最后你需回答 f i n i s h   x finish\ x finish x 表示第 k k k 小的数字是 x x x
  • 3 ≤ n ≤ 1 0 4 3\le n\le 10^4 3n104
    1 ≤ k ≤ n 1\le k\le n 1kn
    0 ≤ a i ≤ 1 0 9 0\le a_i\le 10^9 0ai109

思路

  • 首先观察到,如果有两个不知道的数字,我们知道了他们的与,或的结果是解不出他们的值的
    然后也注意到 n ≥ 3 n\ge 3 n3,说明我们至少要三个数字去搞。
  • 考虑有三个未知的数字,我们知道了他们任意两个之间的,那么是否可以重建出他们三个数呢?
    由于位运算的关系,我们只要单独考虑其中的每一位即可。有四种本质不同的情况
    (1)三个数字这一位分别是 0   0   0 0\ 0\ 0 0 0 0,此时 0 0 0 个与运算为 1 1 1 0 0 0 个或运算为 1 1 1
    (2)三个数字这一位分别是 0   0   1 0\ 0\ 1 0 0 1,此时 0 0 0 个与运算为 1 1 1 2 2 2 个或运算为 1 1 1
    (3)三个数字这一位分别是 0   1   1 0\ 1\ 1 0 1 1,此时 1 1 1 个与运算为 1 1 1 3 3 3 个或运算为 1 1 1
    (4)三个数字这一位分别是 1   1   1 1\ 1\ 1 1 1 1,此时 3 3 3 个与运算为 1 1 1 3 3 3 个或运算为 1 1 1
  • 首先,如果 a i & a j = x a_i\&a_j=x ai&aj=x,那么表示 a i , a j a_i,a_j ai,aj 本身肯定是 x x x 的超集,我们直接把做下述操作即可:
    a i ∣ = x a_i|=x ai=x
    a j ∣ = x a_j|=x aj=x
    然后考虑这四种情况中,情况 1 , 3 , 4 1,3,4 1,3,4 通过这个处理已经是处理对了,只剩下情况 2 2 2 还没有对
    什么时候是情况 2 2 2 ?就是这一位有 2 2 2 个或运算为 1 1 1 的时候。此时根据哪两组数或运算为 1 1 1,我们便可以解出其中那个为 1 1 1 的数字
  • 然后我们发现,我们可以使用 6 6 6 次操作来成功获得 3 3 3 个未知的数字。
    但是如果 n % 3 ≠ 0 n\%3\ne 0 n%3=0,那么剩下多出来的一个或者两个数字岂不是不大好处理?
  • 我们想一下。假设我们已经获得了一个数字 x x x ,现在想知道另一个未知数字 y y y
    且我们知道这两个数字的 运算值,我们是否能获得那个位置数字?
    假设与运算答案为 c c c,那么根据前面的思考,当然要先令 y = c y=c y=c
    假设或运算答案为 d d d,我们仍然拆位去做。
    假设第 i i i 位,数字 x , y x,y x,y 的值是这样子的:
    (1) 0   0 0\ 0 0 0
    (2) 0   1 0\ 1 0 1
    (3) 1   0 1\ 0 1 0
    (4) 1   1 1\ 1 1 1
    容易想到,对于情况 4 4 4,我们答案就已经对了。对于情况 1 , 3 1,3 1,3,我们不需要任何操作。
    只有情况 2 2 2 是特殊的,就是这一位的 0 0 0,且这一位的 1 1 1,且 x x x 的这一位不是 1 1 1,那么我们令 y y y 的这一位为 1 1 1
  • 这样我们就可以通过两次运算,得到一个未知数字。
    所以 n n n 个未知数字只需要 2 n 2n 2n 次查询即可。

代码

  • 时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn) (要排个序)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
#define ll long long
const int MAX = 1e4+50;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;

int aa[MAX];
int tmp[5];
int idx = 0;
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i = 1;i <= 3;++i){
        for(int j = i + 1;j <= 3;++j){		// 先知道前面三个数字
            printf("and %d %d\n",i,j);
            fflush(stdout);
            int t;
            scanf("%d",&t);
            aa[i] |= t;
            aa[j] |= t;
        }
    }
    for(int i = 1;i <= 3;++i){
        for(int j = i + 1;j <= 3;++j){
            printf("or %d %d\n",i,j);
            fflush(stdout);
            int t;
            scanf("%d",&tmp[++idx]);
        }
    }
    for(int i = 0;i <= 30;++i){
        int now = (1<<i);
        int num = 0;
        int no = 0;
        for(int j = 1;j <= 3;++j){
            if(tmp[j] & now)num++;
            else no = j;
        }
        if(num == 2){
            if(no == 1)aa[3] |= now;
            else if(no == 2)aa[2] |= now;
            else if(no == 3)aa[1] |= now;
        }
    }
    for(int i = 4;i <= n;++i){		// 后面每个数字都可以拿第一个数字去比对
        int ad,huo;
        printf("and %d %d\n",1,i);
        fflush(stdout);
        scanf("%d",&ad);

        printf("or %d %d\n",1,i);
        fflush(stdout);
        scanf("%d",&huo);

        for(int j = 0;j <= 30;++j){
            int now = (1<<j);
            if(ad & now) aa[i] |= now;
            else if((huo & now) && !(aa[1] & now)) aa[i] |= now;

        }
    }
//    for(int i = 1;i <= n;++i){
//        show(aa[i]);
//    }
    sort(aa+1,aa+1+n);
    printf("finish %d",aa[k]);
    return 0;
}

E:Equilibrium

题意

  • 给你两个长度为 n n n 的序列 a n , b n a_n,b_n an,bn
    Q Q Q 个查询,每次问你让 [ L , R ] [L,R] [LR] 子区间变平衡的最少操作次数
  • 平衡 是指 ∀ i ∈ [ L , R ] \forall i\in[L,R] i[L,R],都有 a i = b i a_i=b_i ai=bi
  • 操作一次 是指选择这个子串范围内的一个长度为偶数的子序列
    L ≤ p o s 1 < p o s 2 < ⋯ < p o s k ≤ R L\le pos_1<pos_2<\cdots<pos_k\le R Lpos1<pos2<<poskR
    然后,若 i i i 是奇数,则令 a p o s i a_{pos_i} aposi 自增 1 1 1
    然后,若 i i i 是偶数,则令 b p o s i b_{pos_i} bposi 自增 1 1 1
  • 2 ≤ n , q ≤ 1 0 5 2\le n,q\le 10^5 2n,q105
    0 ≤ a i , b i ≤ 1 0 9 0\le a_i,b_i\le 10^9 0ai,bi109

思路

  • 多次区间查询,大概率是用线段树等数据结构去做。
  • 我们一开始没什么思路,只好去手膜样例:
    在这里插入图片描述
  • 我们第一个发现:如果 a L > b L a_L>b_L aL>bL,由于子序列的奇数位置 a a a 自增,子序列的偶数位置 b b b 自增,那么 b L b_L bL 自然是无法自增的,就无解了。
  • 第二个发现:如果 ∑ i = L p a i > ∑ i = L p b i \sum_{i=L}^p a_i > \sum_{i=L}^p b_i i=Lpai>i=Lpbi,那么也是无解的,原因同上。
  • 第三个发现:如果 ∑ i = L R a i ≠ ∑ i = L R b i \sum_{i=L}^R a_i \ne \sum_{i=L}^R b_i i=LRai=i=LRbi,那么也是无解的
    因为每次操作完之后 ∑ i = L R a i − ∑ i = L R b i \sum_{i=L}^R a_i-\sum_{i=L}^R b_i i=LRaii=LRbi 是永远不变的。最终要求都相同,自然差值为 0 0 0
  • 考虑到上述的很多发现都是一个 a a a 的从 L L L 开始的前缀和和 b b b L L L 开始的前缀和的要求
    我们自然想到去令 p r e [ i ] = p r e [ i − 1 ] + b [ i ] − a [ i ] pre[i]=pre[i-1]+b[i]-a[i] pre[i]=pre[i1]+b[i]a[i],做一个从 0 0 0 开始的前缀和
    如果我们想获取从 L L L 开始的前缀和,我们只要区间操作,即 p r e [ L ] ∼ p r e [ n ] pre[L]\sim pre[n] pre[L]pre[n] 都加上 p r e [ L − 1 ] pre[L-1] pre[L1]
    这是一个区间加,我们自然使用线段树去做。
    这样,第二个发现我们可以转变为区间 [ L , R ] [L,R] [L,R] 的最小值必须 > 0 >0 >0,即区间最小值。
    注意每次操作完,都要撤销上述区间加的操作。
  • 现在还有一个问题,就是怎么求最少操作次数。
    想到,由于我们是选择的子序列,下标可以不连续。
    如果 p r e [ x ] = c ( c > 0 ) pre[x]=c(c> 0) pre[x]=cc>0,那么就说明至少要有 c c c 次操作,来让 a x = b x a_x=b_x ax=bx
    所以,我们最少的操作次数就是 max ⁡ i = L   t o   R { p r e [ i ] } \max_{i=L\ to\ R}\{pre[i]\} maxi=L to R{pre[i]},就是一个区间最大值。

代码

  • 时间复杂度: O ( ( n + q ) log ⁡ n ) O((n+q)\log n) O((n+q)logn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
#define ls (p<<1)
#define rs (p<<1|1)
#define md ((l+r)>>1)
#define ll long long
const int MAX = 1e5+50;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
ll aa[MAX],bb[MAX];
ll pre[MAX];
ll tree0[MAX*4],tree1[MAX*4];
ll lazy[MAX*4];
void push_up(int p){
    tree0[p]=min(tree0[ls],tree0[rs]);
    tree1[p]=max(tree1[ls],tree1[rs]);
}
void build(int p,int l,int r){
    lazy[p] = 0;
    if(l == r){
        tree0[p] = tree1[p] = pre[l];
        return;
    }
    build(ls,l,md);
    build(rs,md+1,r);
    push_up(p);
}
void add(int p,int l,int r,ll k){
    lazy[p] += k;
    tree0[p] += k;
    tree1[p] += k;
}
void push_down(int p,int l,int r){
    add(ls,l,md,lazy[p]);
    add(rs,md+1,r,lazy[p]);
    lazy[p] = 0;
}
void update(int p,int l,int r,int ux,int uy,ll k){
    if(ux <= l && uy >= r){
        add(p,l,r,k);
        return;
    }
    push_down(p,l,r);
    if(ux <= md)update(ls,l,md,ux,uy,k);
    if(uy >  md)update(rs,md+1,r,ux,uy,k);
    push_up(p);
}
ll query_mn(int p,int l,int r,int qx,int qy){
    ll res = LINF;
    if(qx <= l && r <= qy){
        return tree0[p];
    }
    push_down(p,l,r);
    if(qx <= md)res = min(res,query_mn(ls,l,md,qx,qy));
    if(qy >  md)res = min(res,query_mn(rs,md+1,r,qx,qy));
    return res;
}
ll query_mx(int p,int l,int r,int qx,int qy){
    ll res = -LINF;
    if(qx <= l && r <= qy)return tree1[p];
    push_down(p,l,r);
    if(qx <= md)res = max(res,query_mx(ls,l,md,qx,qy));
    if(qy >  md)res = max(res,query_mx(rs,md+1,r,qx,qy));
    return res;
}
int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;++i)scanf("%lld",&aa[i]);
    for(int i = 1;i <= n;++i)scanf("%lld",&bb[i]);

    for(int i = 1;i <= n;++i){
        pre[i] = pre[i-1] + (bb[i] - aa[i]);
    }
    build(1,1,n);

    for(int i = 1;i <= m;++i){
        int ta,tb;
        scanf("%d%d",&ta,&tb);
        ll tmp = pre[ta-1];
        update(1,1,n,ta,tb,-tmp);
        ll mn = query_mn(1,1,n,ta,tb);
        ll mx = query_mx(1,1,n,ta,tb);

        update(1,1,n,ta,tb,tmp);

        if(mn < 0 || pre[tb] - pre[ta-1] != 0)puts("-1");
        else printf("%lld\n",mx);

    }
    return 0;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值