比赛题目训练系列11 (2020ICPC·小米 网络选拔赛第二场)

比赛题目训练系列11 (2020ICPC·小米 网络选拔赛第二场)

训练网址

A. 2020

  • 给定一个只含有0,1,2的字符串,现在问你可以把字符串最多分成多少个互不相交的子序列,其中每一个子序列是2020.
  • 最开始很容易想到一个错误的思路,就是找到一个2,2后面紧跟一个0,0后面紧跟一个2,2后面再找紧跟一个0. 但是这样的做法不对。因为一个2020的第二个0可能给下一个2020的第一个2用,更好一些。比如 20220200 20220200 20220200 这个字符串。
    在这里插入图片描述
#include<iostream>
using namespace std;
const int maxn = 100010;
char str[maxn];
int N, used[maxn], vis1[maxn], vis2[maxn], vis3[maxn];
bool check(int x)
{
    fill(used + 1, used + N + 1, 0);
    fill(vis1 + 1, vis1 + N + 1, 0);
    fill(vis2 + 1, vis2 + N + 1, 0);
    fill(vis3 + 1, vis3 + N + 1, 0);
    int num = 0, cnt = 0;
    for(int i = 1; i <= N; i++){
        if(num >= x) break;
        if(str[i] == '2') {
            used[i] = 1;
            vis1[i] = 1;
            num++;
        }
    }
    if(num < x) return false;

    num = 0, cnt = 0;
    for(int i = 1; i <= N; i++){
        cnt += vis1[i];
        if(num >= x) break;
        if(!used[i] && str[i] == '0' && cnt > 0){
            cnt--, num++;
            used[i] = 1;
            vis2[i] = 1;
        }
    }
    if(num < x) return false;

    num = 0, cnt = 0;
    for(int i = 1; i <= N; i++){
        cnt += vis2[i];
        if(num >= x) break;
        if(!used[i] && str[i] == '2' && cnt > 0){
            cnt--, num++;
            vis3[i] = 1;
            used[i] = 1;
        }
    }
    if(num < x) return false;

    num = 0, cnt = 0;
    for(int i = 1; i <= N; i++){
        cnt += vis3[i];
        if(num >= x) break;
        if(!used[i] && str[i] == '0' && cnt > 0){
            cnt--, num++;
        }
    }
    if(num < x) return false;

    return true;
}
int main()
{
    while(cin >> N){
        scanf("%s", str + 1);
        int lb = 0, ub = N / 4;
        while(ub > lb){
            int mid = (lb + ub + 1) / 2;
            if(check(mid)) lb = mid;
            else ub = mid - 1;
        }
        printf("%d\n", lb);
    }
    return 0;
}

C. Data Structure Problem

  • 题意:
    在这里插入图片描述
  • 题解:
    在这里插入图片描述
  • 线段树维护b数组的区间和, a i − s i a_i - s_i aisi 的最大值,以及懒标记(下面会解释)。
  • 如果是第一个操作,那么就单点修改 a i − s i a_i - s_i aisi 的值。
  • 如果是第二个操作,先单点修改 b 数组的值(同时维护区间和的信息),然后从 x x x N N N 区间每个 a i − s i a_i - s_i aisi 都减去 b x b_x bx 增大的值。
  • 第三个操作查询即可。即 s x + m a x ( 0 , m a x { a i − s i ∣ x ≤ i ≤ n } ) s_x + max(0, max\{a_i - s_i | x \le i \le n\}) sx+max(0,max{aisixin}).
#include<iostream>
using namespace std;
const int maxn = 200010;
typedef long long ll;
ll a[maxn], b[maxn], s[maxn];
int N, M;
struct node
{
    int l, r;
    ll sum, a_s, add;
}tr[maxn * 4];
void pushup(int u)
{
    tr[u].a_s = max(tr[2 * u].a_s, tr[2 * u + 1].a_s);
    tr[u].sum = tr[2 * u].sum + tr[2 * u + 1].sum;
}
void pushdown(int u)
{
    auto & rt = tr[u], &left = tr[2 * u], & right = tr[2 * u + 1];
    if(rt.add){
        left.add += rt.add, left.a_s -= rt.add;
        right.add += rt.add, right.a_s -= rt.add;
        rt.add = 0;
    }
}
void build(int u, int l, int r)
{
    tr[u] = {l, r};
    tr[u].add = 0;
    if(l == r){
        tr[u].sum = b[l], tr[u].a_s = a[l] - s[l];
    }
    else{
        int mid = (l + r) / 2;
        build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
        pushup(u);
    }
}
void modify1(int u, int x, int add)
{
    if(tr[u].l == x && tr[u].r == x){
        tr[u].a_s += add;
    }
    else{
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) / 2;
        if(x <= mid) modify1(2 * u, x, add);
        else modify1(2 * u + 1, x, add);
        pushup(u);
    }
}
void modify2(int u, int x, int y)
{
    if(tr[u].l == x && tr[u].r == x){
        tr[u].sum = y;
    }
    else{
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) / 2;
        if(x <= mid) modify2(2 * u, x, y);
        else modify2(2 * u + 1, x, y);
        pushup(u);
    }
}
void modify3(int u, int l, int r, int add)
{
    if(l <= tr[u].l && tr[u].r <= r){
        tr[u].a_s -= add;
        tr[u].add += add;
    }
    else{
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) / 2;
        if(l <= mid) modify3(2 * u, l, r, add);
        if(r > mid) modify3(2 * u + 1, l, r, add);
        pushup(u);
    }
}

ll query1(int u, int l, int r)
{
    if(l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) / 2;
    ll sum = 0;
    if(l <= mid) sum += query1(2 * u, l, r);
    if(r > mid) sum += query1(2 * u + 1, l, r);
    return sum;
}
ll query2(int u, int l, int r)
{
    if(l <= tr[u].l && tr[u].r <= r){
        return tr[u].a_s;
    }
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) / 2;
    ll res = -1e18;
    if(l <= mid) res = max(res, query2(2 * u, l, r));
    if(r > mid) res = max(res, query2(2 * u + 1, l, r));
    return res;
}
int main()
{
    while(cin >> N >> M){
        for(int i = 1; i <= N; i++){
            scanf("%lld", &a[i]);
        }
        for(int i = 1; i <= N; i++){
            scanf("%lld", &b[i]);
            s[i] = b[i] + s[i - 1];
        }
        build(1, 1, N);
        while(M--){
            int op, x;
            ll y;
            scanf("%d", &op);
            if(op == 1){
                scanf("%d%lld", &x, &y);
                modify1(1, x, y - a[x]);
                a[x] = y;
            }
            else if(op == 2){
                scanf("%d%lld", &x, &y);
                modify2(1, x, y);
                modify3(1, x, N, y - b[x]);
                b[x] = y;
            }
            else{
                scanf("%d", &x);
                ll s_x = query1(1, 1, x);
                //printf("Im here01\n");
                ll a_s = query2(1, 1, x);
                ll res = max(s_x, s_x + a_s);
                //printf("Im here02\n");
                printf("%lld\n", res);
            }
        }
    }

    return 0;
}

D. Determinant

  • 题意:直接看吧
    在这里插入图片描述

  • 题目范围: n ≤ 1 0 5 n \le 10^5 n105.
    在这里插入图片描述

  • 这个题推公式即可。这个矩阵写出来之后,把第 i 行除以 a i a_i ai,把第 j 列除以 b j b_j bj,那么对角线会变成 x / a 1 b 1 , x / a 2 b 2 , . . . , x / a n b n x / a_1b_1,x/a_2b_2,...,x/a_nb_n x/a1b1,x/a2b2,...,x/anbn,其他元素全为1.

  • 这样子的话,就成了一道简单的线性代数的课后题。最后答案是 x n + x n − 1 ∑ i = 1 n a i b i x^n + x^{n-1} \sum\limits_{i=1}^{n}a_ib_i xn+xn1i=1naibi.

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010;
typedef long long ll;
const ll mod = 1e9 + 7;
ll a[maxn], b[maxn], x;
int N;
ll mod_pow(ll x, ll n)
{
    ll res = 1;
    while(n){
        if(n & 1) res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}
int main()
{
    while(scanf("%d", &N) == 1){
        scanf("%lld", &x);
        for(int i = 1; i <= N; i++) scanf("%lld", &a[i]);
        for(int i = 1; i <= N; i++) scanf("%lld", &b[i]);
        ll ans = 0;
        for(ll i = 1; i <= N; i++){
            ans = (ans + mod_pow(x, N - 1) * a[i] % mod * b[i] % mod) % mod;
        }
        ans = (ans + mod_pow(x, N)) % mod;
        printf("%lld\n", ans);
    }
    return 0;
}

F.

G. Shift and Reverse

  • 题意:直接截图,序列长度 n ≤ 1 0 5 n \le 10^5 n105.
    在这里插入图片描述
  • 其实你画一个 1 2 3 4 5 的序列,找一下规律就会发现,把序列绕成一个环,所有序列就是从某个元素出发,顺时针旋转得到一个字符串,逆时针旋转得到一个字符串。总共最多得到 2 n 2n 2n 个本质不同的字符串。
  • 因此,一种可行的解法是,用字符串哈希将这些所得的字符串记录下来。从 i 到 i + 1 这个状态就可以推导出字符串哈希值的变化。
  • 字符串哈希有一个很大的问题,就是两个不同的字符串可能哈希值相等。字符串哈希冲突的概率和生日悖论是一样的。有这么一个结论,当取模的值为 P P P,字符串长度为 P \sqrt P P 时,冲突的概率时 50%.
  • 因此,这道题的模数可以选择 4179340454199820289LL。
  • 用自然溢出的话,可以用 abba baab baab abba 这样的字符串卡掉。因此切记:自然溢出是不好的,不用自然溢出用大质数
  • 注意那个乘法的方法,挺牛逼的。
  • 以后用扩展欧几里得方法求逆元,那个 O ( log ⁡ n ) O(\log n) O(logn) 的复杂度是伪的,这个比费马小定理求逆元更快一些。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_set>
using namespace std;
typedef long long ll;
const int maxn = 100010;
const ll P = 131, mod = 4179340454199820289LL;
typedef long double llf;
ll p[maxn] = {1}, w[maxn];
int N;

ll add(ll x, ll y)
{
    return (x + y) % mod;
}
ll mul(ll x, ll y)
{
    ll tmp = x * y - ll(llf(x) * llf(y) / llf(mod)) * mod;
    return tmp < 0 ? tmp + mod : (tmp < mod ? tmp : tmp - mod);
}
ll del(ll x, ll y)
{
    return ((x - y) % mod + mod) % mod;
}

ll exgcd(ll a, ll b, ll& x, ll& y)
{
    if(!b){
        x = 1, y = 0;
        return a;
    }
    ll d = exgcd(b, a % b, y, x);
    y = del(y, mul(a / b, x));
    return d;
}
ll inv_P;
int main()
{
    ll _;
    exgcd(P, mod, inv_P, _);
    inv_P = (inv_P % mod + mod) % mod;
    for(int i = 1; i < maxn; i++){
        p[i] = mul(p[i - 1], P);
    }
    while(cin >> N){
        unordered_set<ll> S;
        for(int i = 1; i <= N; i++) scanf("%lld", &w[i]);
        ll res = 0;
        //逆时针的字符串环
        res = mul(w[1], P);
        for(int i = 2; i <= N; i++){
            res = add(res, mul(w[N - i + 2], p[i]));
        }
        S.insert(res);
        for(int i = 2; i <= N; i++){
            res = del(res, mul(w[i], p[N]));
            res = mul(res, P);
            res = add(res, mul(w[i], P));
            S.insert(res);
        }
        //顺时针的字符串环
        res = 0;
        for(int i = 1; i <= N; i++){
            res = add(res, mul(w[i], p[i]));
        }
        S.insert(res);
        for(int i = 2; i <= N; i++){
            res = del(res, mul(w[i - 1], P));
            res = mul(res, inv_P);
            res = add(res, mul(w[i - 1], p[N]));
            S.insert(res);
        }
        cout << S.size() << endl;
    }
    return 0;
}

  • 还有一种方法是:最开始答案是2n,如果顺时针走的话,相当于看看最小循环节是多少,即用 kmp,最小循环节是 i − n e [ i ] i - ne[i] ine[i]。 逆时针走的话,就是看看能否分成两个回文串。分成一个回文串就把答案 -1.

H. Knapsack

  • 01背包,数据范围是物品个数 n ≤ 2 e 5 n \le 2e5 n2e5,背包容量 M ≤ 2 e 5 M \le 2e5 M2e5,单个物品的重量 w ≤ 100 w \le 100 w100

  • 有一个结论是,当背包的剩余容量 > 100 时,那么我们可以贪心地选择 v w \frac{v}{w} wv 更大的数,此时这个物品一定会包含在最终的答案里面。不过为什么呀?

  • 当然上面这么思考是不对的。只是因为数据比较水。比如题目给了一大堆 v = 2 , w = 2 v = 2, w = 2 v=2,w=2 的物品,而给了一个 v = 3 , w = 3 v = 3, w = 3 v=3,w=3 的物品,背包容量是 M 是一个偶数,那么如果先选了 3 后必然凑不出来最优答案了。

  • 这道题的正解是一个看不懂的东西
    在这里插入图片描述

  • 另外比较 v 1 w 1 > v 2 w 2 \frac{v_1}{w_1} > \frac{v_2}{w_2} w1v1>w2v2,可以转化为 v 1 ∗ w 2 > v 2 ∗ w 1 v_1*w_2 > v_2*w_1 v1w2>v2w1 来比较。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn = 200010;
typedef long long ll;
struct P
{
    ll w, v;
}item[maxn];
bool cmp(const P& p1, const P& p2)
{
    //if (p1.v * p2.w == p2.v * p1.w) return p1.w < p2.w;
    return p1.v * p2.w > p2.v * p1.w;

}
int N, M;
//bool selected[maxn];
ll ans, f[110];
int main()
{
    while(cin >> N >> M){
        memset(f, 0, sizeof f);
        ans = 0;

        for(int i = 1; i <= N; i++){
            scanf("%lld%lld", &item[i].w, &item[i].v);
        }

        sort(item + 1, item + N + 1, cmp);
        int id;
        for(id = 1; id <= N; id++){
            if(M <= 100) break;
            ans += item[id].v, M -= item[id].w;
        }

        for(int i = id; i <= N; i++){
            for(int j = M; j >= item[i].w; j--){
                f[j] = max(f[j], f[j - item[i].w] + item[i].v);
            }
        }
        ans += f[M];
        printf("%lld\n", ans);
    }
    return 0;
}

I. Subsequence Pair

  • 给两个字符串,分别找到一个子序列,让第一个字符串的子序列的字典序小于第二个字符串的子序列;然后问两个子序列长度之和最大是多少。
  • 我们这样考虑:
  1. 如果子序列 s [ i ] < t [ i ] s[i] < t[i] s[i]<t[i],那么 s s s t t t i + 1 i+1 i+1 开始就不重要了。因为 s 的字典序一定大于 t
  2. 如果当前的 s [ i ] = t [ i ] s[i] = t[i] s[i]=t[i],那么一种可行的答案就是 t 再加上后面剩余的所有字符。
  • 因此,我们可以这样子,令 f ( i , j ) f(i,j) f(i,j) 就是最长公共子序列的长度,这样子可以把前两种类型的答案都计算到。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 2010;
int ans;
char s1[maxn], s2[maxn];
int f[maxn][maxn];
int main()
{
    while(scanf("%s", s1 + 1) == 1){
        scanf("%s", s2 + 1);
        int N = strlen(s1 + 1), M = strlen(s2 + 1);

        for(int i = 1; i <= N; i++){
            fill(f[i], f[i] + M + 1, 0);
        }
        ans = M;
        for(int i = 1; i <= N; i++){
            for(int j = 1; j <= M; j++){
                //如果当前是 s1[i] < s2[j] 的话,那么后面可以出现 s1[i + 1] > s2[j + 1]
                if(s1[i] < s2[j]) {
                    ans = max(2 * (f[i - 1][j - 1] + 1) + N - i + M - j, ans);
                }
                //如果当前是 s1[i] >= s2[j] 的话,那么此时就和最长公共子序列的做法是一样的。
                else if(s1[i] == s2[j]){
                    f[i][j] = f[i - 1][j - 1] + 1;
                    ans = max(2 * f[i][j] + M - j, ans);
                }
                else f[i][j] = max(f[i - 1][j], f[i][j - 1]);

            }
        }
        printf("%d\n", ans);
    }
    return 0;
}
/*
azz
bcd
*/
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值