Codeforces Hello 2024 A~F1

A.Wallet Exchange(思维)

题意:

AliceBob各自拥有a,ba,ba,b枚硬币,他们决定以Alice为先手开始比赛,比赛中每人在每轮需按顺序执行操作1和操作2:

  • 操作1:交换两人手上拥有的硬币数量,或什么都不做

  • 操作2:丢掉手上的一枚硬币(如果此时手上没有硬币则输掉比赛)

问:假设两人都极为聪明,那么谁会赢下这场比赛?

分析:

实际上对于操作1,比赛两方均会选择硬币较多的一堆,因此,可以将硬币视为只有一堆,共nnn枚,其中n=a+bn = a + bn=a+bAliceBob按顺序从这堆硬币中丢弃硬币。

此时若硬币数量nnn为偶数时,必然是后手丢掉最后一枚硬币,即Bob获胜,反之,先手Alice获胜。

代码:

#include<bits/stdc++.h>

using namespace std;

void solve() {
    int a, b;
    cin >> a >> b;
    if ((a + b) % 2 == 0) {
        cout << "Bob" << endl;
    } else {
        cout << "Alice" << endl;
    }
}

int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

B.Plus-Minus Split(贪心)

题意:

给出一个包含nnn个字符的字符串sss,字符串中仅包含字符-+,其中-代表−1-11+代表111

你可以进行以下步骤来计算罚分:

  1. 将字符串sss分为若干子子串b1,b2,...,bkb_1, b_2, ..., b_kb1,b2,...,bk

  2. 对于字符串ccc产生的罚分为p(c)=∣c1+c2+...+cm∣⋅mp(c) = |c_1 + c_2 + ... + c_m| \cdot mp(c)=c1+c2+...+cmm

  3. 产生的总罚分为p(s)=p(b1)+p(b2)+...+p(bk)p(s) = p(b_1) + p(b_2) + ... + p(b_k)p(s)=p(b1)+p(b2)+...+p(bk)

问:最小可能的罚分是多少?

分析:

为了最小化罚分,需要尽可能多的将正负号进行匹配,即若干子拆出的子串中正负号出现次数相同,但字符串中正负号数量并不一定相同,那么剩下的正号或负号依然会产生罚分,此时为了最小化罚分,必然选择这些正负号自身为一个子串,假设存在aaa+++号,bbb−-号,那么最小罚分为a+b−2×min(a,b)a + b - 2 \times min(a, b)a+b2×min(a,b)

代码:

#include<bits/stdc++.h>

using namespace std;

void solve() {
    int n;
    string s;
    cin >> n >> s;
    int add = 0, sub = 0;
    for (int i = 0; i < n; i++) {
        if (s[i] == '-') sub++;
        else add++;
    }
    cout << n - 2 * min(add, sub) << endl;
}

int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

C.Grouping Increases(贪心)

题意:

给出一个包含nnn个数字的数组aaa,你需要按以下要求计算罚分:

  1. 将数组aaa分为两个子序列s,ts, ts,t(可能有个子序列为空)。

  2. 对于一个包含mmm个数字的数组bbb,产生的罚分p(b)=∑i=1m−1f(i)p(b) = \sum\limits_{i = 1}^{m - 1}f(i)p(b)=i=1m1f(i),其中f(i)f(i)f(i)为满足bi<bi+1b_i < b_{i + 1}bi<bi+1则为111,不满足则为000

  3. 总罚分为p(s)+p(t)p(s) + p(t)p(s)+p(t)

问:最小可能的罚分是多少?

分析:

由于对于子序列的罚分计算只考虑相邻的两个数字,那么只需要维护两个子序列最后的元素。

bbb为两个子序列中最后元素中的较小值,ccc为较大值,并按以下要求检查aia_iai该加入哪个子序列:

  • 如果ai≤ba_i \le baib,那么aia_iai将会加入最后元素较小的序列,即b=aib = a_ib=ai

  • 否则,再检查是否满足ai≤ca_i \le caic,如果满足,则将aia_iai加入最后元素较大的序列,即c=aic = a_ic=ai

  • 如果以上两种情况均不满足,那么此时必然产生一点罚分,并且为了使之后的罚分尽可能小,当前数字aia_iai必然会加入尾部元素较小的序列,即b=aib = a_ib=ai

代码:

#include<bits/stdc++.h>

using namespace std;

int a[200005];

void solve() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) cin >> a[i];
    int b = 1e9, c = 1e9, ans = 0;
    for (int i = 0; i < n; i++) {
        if (b > c) {
            swap(b, c);
        }
        if (a[i] <= b) {
            b = a[i];
        } else if (a[i] <= c) {
            c = a[i];
        } else {
            b = a[i];
            ans++;
        }
    }
    cout << ans << endl;
}

int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

D.01 Tree(思维)

题意:

有一棵包含nnn个叶节点的树,保证每个非叶节点均包含两个子节点,同时,对于所有非叶节点,走向子节点的边中均包含权值,且其中一条权值为000,另一条权值为111

你忘记了原本的树是什么样子的,只记得根节点到这nnn个叶节点的权值之和,按dfs序给出这些叶节点的权值,问是否存在满足这些节点权值的树。

分析

由于每个非叶节点与子节点之间的边权分别为0,10, 10,1,那么属于同一个父节点的两个节点的权值之差必然为111,且权值较小的一个与父节点的权值一致,由此可知如果给出的叶节点合法,那么必然存在且仅存在一个叶节点的权值为000

同时,由于叶节点以dfs序给出,那么属于同一个父节点的两个叶节点在dfs序中也必然相邻,为了便于处理,使用静态双向链表将1∼n1 \sim n1n这些节点按顺序连在一起。

由于非叶节点的权值与两个子节点的权值较小者相同,那么如果同时删除它的两个子节点(这两个子节点均为叶节点),该节点也会变成叶节点,为了便于处理,可以在操作时仅删除权值较大的节点,将另一个子节点视为删除后新产生的叶节点。

然后考虑从叶节点开始向上删除节点,按以下步骤执行:

  • 删除的节点,它在dfs序中相邻的两个节点中,必然存在一个节点的权值比它小111(树上仅会存在一个权值为000的节点,其他节点权值均大于000),此时可以将节点视为待删除节点,放入大根堆中等待删除

  • 当从大根堆取出节点后,就需要对该节点进行删除操作了,由于该节点存储在静态双向链表上,那么需要同时修改前后节点的指针。

  • 然后检查当前节点删除后,左右两边相邻的节点能否进入待删除状态,如果能,就将节点也加入堆中。

删除完成后,如果仅剩下一个权值为000的节点未被删除,那么就表示存在满足要求的树,否则,找不到满足要求的树。

代码:

#include<bits/stdc++.h>

using namespace std;

int n, a[200005], pre[200005], nxt[200005], vis[200005];
struct Node{
    int id, val;
    bool operator < (const Node &o) const {
        if (val != o.val) return val < o.val;
        return id > o.id;
    }
};

priority_queue<Node> Q;

int check(int x) {
    return x >= 1 && x <= n && (a[pre[x]] == a[x] - 1 || a[nxt[x]] == a[x] - 1);
}

void solve() {
    cin >> n;
    int zero = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        pre[i] = i - 1;
        nxt[i] = i + 1;
        vis[i] = 0;
        if (a[i] == 0) zero++;
    }
    /*a[0]和a[n + 1]取-1时可能会导致a[1]=0或a[n]=0的情况入队,因此取不影响答案的-2*/
    a[0] = a[n + 1] = -2;
    if (zero == 0) {
        cout << "NO" << endl;
        return;
    }
    for (int i = 1; i <= n; i++) {
    /*找开始时就能进入待删除状态的节点*/
        if (check(i)) {
            vis[i] = 1;
            Q.push(Node{i, a[i]});
        }
    }
    while (!Q.empty()) {
        Node u = Q.top();
        Q.pop();
        /*相当于双向链表中删点,需将前后连起来*/
        nxt[pre[u.id]] = nxt[u.id];
        pre[nxt[u.id]] = pre[u.id];
        /*检查左右的节点能否进入待删除状态*/
        if (vis[nxt[u.id]] == 0 && check(nxt[u.id])) {
            vis[nxt[u.id]] = 1;
            Q.push(Node{nxt[u.id], a[nxt[u.id]]});
        }
        if (vis[pre[u.id]] == 0 && check(pre[u.id])) {
            vis[pre[u.id]] = 1;
            Q.push(Node{pre[u.id], a[pre[u.id]]});
        }
    }
    /*由于权值为0的节点必然不会被删除,那么只需要判断剩余的节点数量.
    如果为1,那么剩下的节点权值必然为0*/
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (vis[i] == 0) {
            cnt++;
        }
    }
    if (cnt == 1) cout << "YES" << endl;
    else cout << "NO" << endl;
}

int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

E.Counting Prefixes(组合数学,思维)

题意

有一个仅含有−1-11111的数组aaa,以及它对应的的前缀和数组排序以后的结果ppp,问你原本的aaa数组有多少种可能性。答案需要对998244353998244353998244353取模。

思路

有两种明显无解的情况是比较好想到的:

  1. 如果ppp数组仅有一个不为−1,1-1,11,1的值,那么一定无解。
  2. 如果ppp数组中最小的元素都比111大,或者最大的元素都比−1-11小,那也是不可能的。

对于其他情况,我们先ppp将它恢复成未排序的状态,称这个未排序的前缀和数组为preprepre。假设某一个元素对应位置的前缀和为sss,我们可以写作pre[i]=spre[i]=spre[i]=saaa数组在它之后的元素要么是111,要么是−1-11,即:pre[i]=s+1pre[i]=s+1pre[i]=s+1pre[i]=s−1pre[i]=s-1pre[i]=s1。此时我们会发现:题目中的第333个样例显然是无解的,因为−1-111,21,21,2都不可能相邻出现。

下面,我们开始对这个问题的常规情况进行分析。我们假设pn≥0p_n\ge0pn0,若它小于000,我们可以考虑这个ppp数组元素全取相反数的结果,结论仍然是成立的(相当于所有的111变成−1-11−1-11变成111)。

这样以来我们可以想到:

  1. 先去构建一个先全为111的序列,使得1的个数为pnp_npn个,此时前缀和数组中含有从111pnp_npn这些元素,每个元素出现一次。例如,若pn=3p_n=3pn=3,则此时我们先构建1 1 1
  2. 如果还需要其他的pnp_npn,可以视为任意一个前缀和刚好为pnp_npn的位置插入一组(-1,1),此时会多出一个,例如1 1 1 -1 1,会有222个位置对应的前缀和都为333。下次插入时,我们可以把当前序列看做1 1 1 _ (-1 1) _ ,两个下划线对应的位置都可以进行插入、插入kkk次以后,前缀和数组中有k+1k+1k+1pnp_npn,若pn−1>0p_n-1>0pn1>0,则pn−1p_n-1pn1也有k+1k+1k+1个(最开始的全111序列中自带一个),否则有kkk个。
  3. pnp_npn插入完毕以后,我们可以看一下pn−1p_n-1pn1还差多少个。假如说,我们现在已经有xxxpn−1p_n-1pn1了,那说明目前我们的序列中有xxx−1-11,但我们需要的pn−1p_n-1pn1可能有yyy个。其中若y<xy < xy<x,则说明无解;否则我们可以在xxx−1-11的后面,再次加入(-1,1)。但要注意,每一次加入都会使得−1-11多一个,因此我们可以这样推导插入的方案数:

按照这样的顺序,我们依次去插入pn−1,pn−2p_n-1,p_n-2pn1,pn2,一直控制到p1p_1p1即可。这个过程中可以补一个起始(或终止位置)的000,便于计算。

代码

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N = 5050;
ll c[N][N];
int p[N], x[N * 2], cnt[N * 2];
const ll mod = 998244353;

void get_c_matrix() {//返回值为int类型时会导致超时
    c[0][0] = 1;
    for (int i = 1; i <= 5000; ++i) {
        c[i][0] = 1;
        for (int j = 1; j <= i; ++j) {
            c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
        }
    }
}

ll get_c_value(int xx, int yy) {
    if (xx < 0 || yy < 0 || yy > xx)return 0;
    return c[xx][yy];
}

int main() {
    get_c_matrix();
    int T, n, flag;
    ll ans, res;
    cin >> T;
    while (T--) {
        cin >> n;
        //进行必要的清空
        memset(cnt, 0, sizeof(cnt));
        cnt[N] = 1; //在起始处补一个0
        ans = 0;
        flag = 1; //用于标记是否有类似于1 3这样跳着的、不相邻的情况
        for (int i = 1; i <= n; i++) {
            //统计每个元素出现的个数
            cin >> p[i];
            cnt[p[i] + N]++;
        }
        if (n == 1) {
            //如果只有一个元素,那么必然是1或者-1
            if (abs(p[1]) == 1) cout << 1 << endl;
            else cout << 0 << endl;
            continue;
        }
        if (p[1] > 1 || p[n] < -1) {
            //类似于 2 2 3 或者-3 -2 -2也是绝对不可能的
            cout << 0 << endl;
            continue;
        }
        int l = min(p[1] + N, N), r = max(p[n] + N, N), i;  //不要忘记你补了个0
        for (i = l; i <= r; i++) {
            //如果有跳着的、不连续的,比如2 2 4,也是不可能的
            if (!cnt[i]) {
                flag = 0;
                cout << 0 << endl;
                break;
            }
        }
        if (!flag)continue;
        for (i = r; i >= l; i--) {
            x[r - 1] = cnt[r];
            //注意全1序列已经会产生部分前缀和
            if (r > 0 + N)x[r - 1]++;
            if (r == i)x[r - 1]--;

            for (int j = r - 1; j >= l; j--) {
                //进行模拟
                x[j - 1] = cnt[j] - x[j];
                if (j - 1 >= i)x[j - 1]++;
                if (j - 1 >= 0 + N)x[j - 1]++;
            }
            if (x[l - 1] == 0) {
                res = 1;
                for (int j = r - 1; j >= l; j--) {
                    res = (res * get_c_value(cnt[j] - 1, cnt[j] - x[j])) % mod;
                }
                ans = (ans + res) % mod;
            }

        }
        cout << ans << endl;
    }
    return 0;
}

F1.Wine Factory (Easy Version)(后缀和,线段树)

题意:

nnn座水塔,其中第iii座包含aia_iai升的水,且拥有一个能消除bib_ibi升水的法师,同时相邻两座水塔之间拥有一道阀门,第iii座高塔与第i+1i + 1i+1座水塔之间的阀门允许通过cic_ici升水。

对于第i(i=1,2,...,n)i(i = 1, 2, ..., n)i(i=1,2,...,n)座塔,将会进行以下操作:

  1. 法师将水塔iii中最多bib_ibi升的水消除,并将消除的水转化为等量的红酒。

  2. 如果i≠ni \ne ni=n,那么至多cic_ici升水可以由阀门流到第i+1i + 1i+1座水塔。

题目将给出qqq次更新,每次给出四个数字p,x,y,zp, x, y, zp,x,y,z,即将ap=x,bp=y,cp=za_p = x, b_p = y, c_p = zap=x,bp=y,cp=z,并要求对于每次更新,输出最多能获得多少升红酒。

特殊条件:对于所有数据,有ci=1018,z=1018c_i = 10^{18}, z = 10^{18}ci=1018,z=1018

分析:

对于特殊条件,ci,zc_i, zci,z均为101810^{18}1018,由于∑i=1nmax(ai)=5×1014<1018\sum\limits_{i = 1}^{n}max(a_i) = 5 \times 10^{14} < 10^{18}i=1nmax(ai)=5×1014<1018,那么如果水塔中的水没有被全部消除,则剩下的水均能流到下一座高塔,即可以不考虑cic_ici这个限制因素。

先考虑每座水塔自身无法消除的水量:vi=ai−biv_i = a_i - b_ivi=aibi,当vi>0v_i > 0vi>0时,未被消除的水量必然需要由后面的法师来进行消除(如果可以的话),因此对于水塔kkk而言,无法消除的水量实际为f(k)=∑i=knvif(k) = \sum\limits_{i = k}^{n}v_if(k)=i=knvi。那么对于所有水塔来说,无法消除的水量即为max(f(1),f(2),...,f(n))max(f(1), f(2), ..., f(n))max(f(1),f(2),...,f(n))

使用后缀和建一棵线段树,维护区间上的最大值,并使用变量sumsumsum记录水塔的总水量。

每次更新水塔ppp时,由于线段树维护的是后缀和,那么对于所有(p+1)∼n(p + 1) \sim n(p+1)n上的点,均不会收到影响,只需要对区间1∼p1 \sim p1p进行更新,先减去原本的vpv_pvp,再加上当前更新后的新vp=x−yv_p = x - yvp=xy

那么最后的答案就是sum−max(f(1),f(2),...,f(n))sum - max(f(1), f(2), ..., f(n))summax(f(1),f(2),...,f(n)),其中max(f(1),f(2),...,f(n))max(f(1), f(2), ..., f(n))max(f(1),f(2),...,f(n))为线段树树根中记录的信息。

代码:

#include<bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 5e5 + 5e2;

LL n, q, a[N], b[N];
LL sum, T[N << 2], lazy[N << 2], c[N], v[N], sv[N];

void pushup(int x) {
    T[x] = max(T[x << 1], T[x << 1 | 1]);
}

void build(int l, int r, int x) {
    if (l == r) {
        T[x] = sv[l];
        return;
    }
    int mid = l + r >> 1;
    build(l, mid, x << 1);
    build(mid + 1, r, x << 1 | 1);
    pushup(x);
}

void pushdown(int x) {
    if (lazy[x]) {
        lazy[x << 1] += lazy[x];
        lazy[x << 1 | 1] += lazy[x];
        T[x << 1] += lazy[x];
        T[x << 1 | 1] += lazy[x];
        lazy[x] = 0;
    }
}

void update(int l, int r, int x, int ul, int ur, LL val) {
    if (l >= ul && r <= ur) {
        T[x] += val;
        lazy[x] += val;
        return;
    }
    pushdown(x);
    int mid = l + r >> 1;
    if (ul <= mid) update(l, mid, x << 1, ul, ur, val);
    if (ur > mid) update(mid + 1, r, x << 1 | 1, ul, ur, val);
    pushup(x);
}

void solve() {
    cin >> n >> q;
    sum = 0;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> b[i];
    for (int i = 1; i < n; i++)  cin >> c[i];
    for (int i = n; i >= 1; i--) {
        v[i] = a[i] - b[i];
        sv[i] = v[i] + sv[i + 1];
        sum += a[i];
    }
    build(1, n, 1);
    while (q--) {
        int p, x, y;
        LL z;
        cin >> p >> x >> y >> z;
        sum -= a[p];//减去原本的水量
        /*减掉原本的vp*/
        update(1, n, 1, 1, p, -v[p]);
        a[p] = x;
        b[p] = y;
        v[p] = x - y;//更新vp
        sum += a[p];//加上现在的水量
        /*加上现在的vp*/
        update(1, n, 1, 1, p, v[p]);
        /*出现负数说明所有水均能消除,此时需要减去的值为0,需取max*/
        cout << sum - max(0ll, T[1]) << endl;
    }
}

int main() {
    ios::sync_with_stdio(false);
    int Case = 1;
    while (Case--) {
        solve();
    }
    return 0;
}

学习交流

以下为学习交流QQ群,群号: 546235402,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值