Codeforces Round #769 (Div. 2) A -D

https://codeforces.com/contest/1632

A

  • 如果一个串存在一个回文子串长度大于1,那么输出NO,否则输出YES
  • 暴力可以做,但是有更好的想法,注意到这是一个01串,如果长度大于2,那么一定存在这样一个回文串,因为无论是001还是010都满足这一性质;如果长度是1,那么就是YES;如果长度是2,那么就判断前两个字符是否相同即可
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        string s;
        cin >> s;
        if(n == 1 || (n == 2 && s[0] != s[1])) cout << "YES\n";
        else cout << "NO\n";
    }
    return 0;
}

B

  • 随意排布 [ 0 , n − 1 ] [0,n-1] [0,n1],求里面相邻两项的异或最大值的最小值
  • 考虑最高位的1,因为这一位一定能和一个最高位是0的位置进行匹配,那么异或出来的值最少是最高位二进制所对应的数,那么怎么能够让这个数成立为这个最小值呢?显然只需要把这个数前面放个0,其他位置顺序不变即可,因为这个数之前的最高位两两异或一定是0,这个数之后的最高位两两异或一定也是0,总没有这个位置大,所以这就是最大中的最小
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        for(int i=1;i<n;i++){
            if(i == (1 << (31 - __builtin_clz(n - 1)))) cout << 0 << ' ';
            cout << i << " \n"[i == n - 1];
        }
    }
    return 0;
}

C

两个数 a , b a,b a,b,有三种操作, a = a + 1 , b = b + 1 , a = a ∣ b a=a+1,b=b+1,a=a|b a=a+1,b=b+1,a=ab,可以选择其中一个,问最少多少次操作能够让 a = b a=b a=b

  • 考虑到 a ∣ b ≥ m a x ( a , b ) a|b\geq max(a,b) abmax(a,b),所以这种操作一定要放在最后进行而且最多只能有一次,所以现在考虑怎么进行加法操作
  • 最多的操作次数是 b − a b-a ba
  • 考虑在这些次数之内,不停对 a , b a,b a,b分别进行加法操作,如果达到目标那么就更新答案
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int a, b;
        cin >> a >> b;
        int ans = b - a;
        for(int i=a;i<=a+ans;i++){
            if(i == b){
                ans = min(ans, i - a);
            }else if((i | b) == b){
                ans = min(ans, i - a + 1);
            }
        }
        for(int i=b;i<=b+ans;i++){
            if((i | a) == i){
                ans = min(ans, i - b + 1);
            }
        }
        cout << ans << '\n';
    }
    return 0;
}
  • 看tourist代码和直播视频,发现他的方法很巧妙,他使用一个 n e w _ a new\_a new_a,每次加上它的最后一个二进制1,从而不停地逼近 b b b,在这之间取最小值
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int a, b;
        cin >> a >> b;
        int ans = b - a;
        int new_a = a;
        while(new_a <= b){
            int x = new_a - a;
            for(int y=0;y<=b-a;y++){
                ans = min(ans, x + y + 1 + ((x + a) | (y + b)) - (y + b));
                //x是每次需要补给a多少,y是每次需要补给b多少,1是异或,最后是进行完这些操作之后a还和b差多少
            }
            new_a += (new_a & (-new_a));
        }
        cout << ans << '\n';
    }
    return 0;
}

D

如果存在一个区间 g c d gcd gcd等于区间长度,那么这个区间会使观众感到厌烦,问对于这个数组的所有前缀最少需要改动多少个元素能让每个非空前缀都不会让观众感到厌烦

  • 有一种直观的思路是我们枚举每一个左端点,然后去找距离他最远的满足区间gcd等于区间长度的位置,这样的位置只能有一个,然后我们把这个数变成一个大质数即可,因为我们知道越往后前缀gcd就会越小,而且前缀gcd最多有log个,因为每次最少都需要除以2
  • 那么我们枚举每一个左端点,然后右端点二分得到每一条线段,然后从左往右依次处理即可
  • 利用线段树或者 s t st st表进行区间 g c d gcd gcd的快速获取
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n);
    vector<vector<int> > ST(n, vector<int>(30));
    for(int i=0;i<n;i++){
        cin >> a[i];
        ST[i][0] = a[i];
    }
    for(int j=1;j<30;j++){
        for(int i=0;i+(1<<(j - 1))<n;i++){
            ST[i][j] = __gcd(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);
        }
    }
    vector<pair<int, int> > seg;
    function<int(int, int)> Get = [&](int l, int r){
        int sz = r - l + 1;
        int t = 31 - __builtin_clz(sz);
        return __gcd(ST[l][t], ST[r - (1 << t) + 1][t]);
    };
    for(int i=0;i<n;i++){
        int l = i;
        int r = n - 1;
        while(l <= r){
            int mid = ((r - l) >> 1) + l;
            if(Get(i, mid) >= mid - i + 1){
                l = mid + 1;
            }else{
                r = mid - 1;
            }
        }
        if(Get(i, l - 1) == l - 1 - i + 1) seg.push_back(make_pair(i, l - 1));
    }
    vector<int> ans(n + 1);
    int covered = -1;
    for(auto &i : seg){
        if(i.first > covered){
            covered = i.second;
            ans[i.second] = 1;
        }
    }
    for(int i=1;i<n;i++){
        ans[i] += ans[i - 1];
    }
    for(int i=0;i<n;i++){
        cout << ans[i] << " \n"[i == n - 1];
    }
    return 0;
}
  • 这题的关键在于我们要知道前缀gcd满足 a [ i − 1 ] ≥ a [ i ] a[i-1]\geq a[i] a[i1]a[i]这样的关系,这样我们在枚举左端点二分右端点的时候由于这个值越来越小,所以不会存在一个比他小的值在前面,第一个点的右端点如果发生了改动那么这之间所有的数都不用再看了,因为不可能有符合条件的区间
  • tourist解法依然是如此飘逸,他使用一个tuple记录所有位置的后缀gcd,因为无论是前缀gcd还是后缀gcd最多只有log个,那么我们去记录当前位置的所有后缀gcd和对应的起始位置和结束位置,如果发现某个后缀满足条件,那么更新右端点为一个大质数,同时左面都不用看了,原因同上,这种方法很巧妙,不用任何数据结构
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n);
    for(int i=0;i<n;i++) cin >> a[i];
    int ans = 0;
    int covered = -1;
    vector<array<int, 3>> s;
    for(int i=0;i<n;i++){
        for(auto &t : s){
            t[2] = __gcd(t[2], a[i]);
        }
        s.push_back({i, i, a[i]});
        int cnt = 0;
        for(int j=0;j<(int)s.size();j++){
            if(cnt > 0 && s[j][2] == s[cnt - 1][2]){
                s[cnt - 1][1] = s[j][1];
            }else{
                s[cnt++] = s[j];
            }
        }
        s.resize(cnt);
        for(auto &t : s){
            int at = i - t[2] + 1;//左端点
            if(t[0] <= at && at <= t[1]){
                if(at > covered){
                    covered = i;
                    ans += 1;
                }
            }
        }
        cout << ans << " \n"[i == n - 1];
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值