Educational Codeforces Round 184 (Rated for Div. 2) A-E题解

Educational Codeforces Round 184 (Rated for Div. 2)
总评:非常非常非常好的题 不愧是教育场

A. Alice and Bob

小游戏,给定数组nums,A先说一个数a,B再说一个数b。
现在需要给B指定这个b的值,要求数组中距离b更近的元素数量最大化(距离相同时算A的)
比如数组[1,2,3,4,5] a=2,则显然b=3是最优解,可以有3个更近的元素。

小智力题:比如AB打赌猜测一群人的具体数量,谁猜的更近谁赢。
A猜测了100,则B应该用什么策略?
最优解,如果B觉得A猜的过多了,则应该选择99,否则应该是101
不存在A=100 B=120或者80之类的答案。

对于本题,只需要比较严格比a小的个数和严格比a大的个数
最后取a+1或者a-1

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    int n, A; cin >> n >> A;
    vector<i64> nums(n);
    for (int i = 0; i < n; i++) cin >> nums[i];
    sort(nums.begin(), nums.end());
    int L = 0, R = 0;
    for (int i = 0; i < n; i++) {
        if (nums[i] <= A) L++;
        if (nums[i] >= A) R++;
    }
    cout << (L < R ? A + 1 : A - 1) << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

B. Drifting Away

一个字符串数组,由三种字符组成<*>
如果位于<位置,则只能向数组左侧移动,>则向右
而如果是*则可以任意选择
初始位置可以任意指定,问最大可移动距离,如果可以循环 返回-1

考虑只有<> 什么情况下会出现循环?
显然出现><时,此时返回-1
如果不循环则意味着必然是<<...<<>>...>>的形式
最大移动次数就是两种符号个数的较大值。

如果有了*呢?它既可以表示>也可以表示<,要小心组合情况
循环的情况包括:
>< >* *< **四种

一个推论是如果不循环,则*只能位于中间,且只有一个

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    string s; cin >> s;
    int n = s.size();
    //检测是否循环
    for (int i = 0; i < n - 1; i++) {
        char L = s[i], R = s[i + 1];
        if (L != '<' && R != '>') {
            cout << -1 << endl;
            return;
        }
    }
    //否则取两者最大值
    int cL = 0, cR = 0;
    for (auto c : s) {
        if (c != '>') cL++;
        if (c != '<') cR++;
    }
    cout << max(cL, cR) << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

C. Range Operation

很educational的题目

给定一个数组,你可以执行以下操作至多一次
选择一个区间[L,R]把区间内的所有元素都替换为(L + R)
最大化数组和

暴力的做法,无非是枚举所有子数组,然后替换求和\(O(n^2)\)显然会超时。

一个明显的切入点是,枚举一端,然后能以较低复杂度\(O(1)\)或者\(O(logn)\)计算另一端所有选择的最大值。

直接思考有点困难。

考虑展开结果,要【学会推式子】

C的式子推导:

定义\(f(i,j)\)表示修改区间[i,j]时的最终数组和

设原数组总和为\(S\)

\(pre_i\)表示前\(i\)个元素的前缀和,根据题意

\(f(i, j) = S - (pre_j - pre_{i-1}) + (j - i + 1) * (j + i)\)

枚举\(i\)时,\(i\)相关值已知,变量为\(j\),调整一下式子

\(f(i, j) = \underbrace{(S + pre_{i-1} - i * i + i)}_{枚举i时可直接计算} + \underbrace{j^2 + j - pre_j}_{T_j}\)

核心是求 \(T_j=j^2+j-pre_j\) 的最大值

也就是枚举\(i\)的过程中,需要得到\(j∈[i,n\)的所有\(T_j\)的最大值。

预处理每个\(T_j\),区间最大值可以简单用线段树暴力处理。

考虑到\(j\)所在区间右端点始终为\(n\),可预处理后缀最大值。

关键点:关于\(j\)的式子\(T_j\)不能再与\(i\)有关,比如\(T_j=i*j\)之类

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    int n; cin >> n;
    vector<i64> nums(n);
    for (int i = 0; i < n; i++) cin >> nums[i];
    vector<i64> pre(n + 1);
    for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + nums[i - 1];

    i64 total = pre[n];

    vector<i64> g(n + 1);
    for (int r = 1; r <= n; r++) {
        g[r] = 1LL * r * r + r - pre[r];
    }

    vector<i64> maxG(n + 2);
    maxG[n + 1] = -1e18;
    for (int r = n; r >= 1; r--) {
        maxG[r] = max(g[r], maxG[r + 1]);
    }

    i64 maxDelta = 0; 

    for (int l = 1; l <= n; l++) {
        i64 Cl = l - 1LL * l * l + pre[l - 1];
        i64 best = maxG[l] + Cl;
        if (best > maxDelta) {
            maxDelta = best;
        }
    }
    //前面省略了S 因为是固定值 最后加
    i64 ans = total + maxDelta;
    cout << ans << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

上面这个做法很有普适性。
下面介绍个更需要【观察力】的方案
还是从这个式子出发
\(f(i, j) = S - (pre_j - pre_{i-1}) + (j - i + 1) * (j + i)\)
考虑到每个计算都包含S,因此可以直接删掉,得到的是变化量,最后加回S即可

\(f(i, j) = - (pre_j - pre_{i-1}) + (j - i + 1) * (j + i)\)

=>

\(f(i, j) = j * j + j - pre_j - ((i - 1) * (i - 1) + (i - 1) - pre_{i-1})\)

定义\(S(i)=i*i+i-pre_i\)

\(f(i, j) = \underbrace{j \cdot j + j - pre_j}_{S(j)} - \underbrace{((i - 1) \cdot (i - 1) + (i - 1) - pre_{i-1})}_{S(i-1)}\)


\(f(i, j) = S(j)-S(i-1)\)
注意我们枚举右区间,这里也就是枚举\(j\),则\(S(j)\)是枚举时固定值
为了最大化,需要找到左侧\(S(i-1)\)的最小值
事实上 甚至可以边枚举边计算,同时维护一个左侧最小值即可。

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    int n; cin >> n;
    vector<i64> nums(n);
    for (int i = 0; i < n; i++) cin >> nums[i];
    vector<i64> pre(n + 1);
    for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + nums[i - 1];
    i64 total = pre[n];
    i64 minL = 0;
    i64 ans = total;
    for (int i = 1; i <= n; i++) {
        i64 v = nums[i - 1];
        i64 g = 1LL * i * i + i - pre[i]; //S[i]
        minL = min(minL, g);
        ans = max(ans, g - minL + total);
    }
    cout << ans << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

D1. Removal of a Sequence (Easy Version)

这个也确实很Educational

给定自然数列1,2,3...1e12
每轮操作中,移除y的整数倍的整数倍位置上的数字
执行x轮
输出最后剩余数字中第k位
比如y=3 x=2 k=5
初始
1,2,3,4,5,6,7,8,9,10...
第一轮操作后
1,2,4,5,7,8,10...
第二轮操作后
1,2,5,7,10...
故剩余数字中第5个为10

【约瑟夫环】变种

这里首先需要介绍二分的解法。

重要观察:
最后剩余数字的第k位上的数字N,也就是原始数据里的第N个
经过x轮删除后只剩了k个。
需要充分理解每轮删除y的整数倍的位置的含义。
假设当前总共有N个数字,一轮操作后剩余个数为
\(N-\frac{N}{k}\)
执行x轮,检测是否剩余够k个即可。

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    i64 x, y, k; cin >> x >> y >> k;
    i64 mx = 1e12;
    auto check = [&](i64 t)->bool {
        for (int i = 0; i < x; i++) {
            t -= t / y;
        }
        return t >= k;
    };
    i64 low = 1, high = mx;
    while (low <= high) {
        i64 mid = low + (high - low) / 2;
        if (check(mid)) high = mid - 1;
        else low = mid + 1;
    }
    cout << (low > mx ? -1 : low) << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

再次强调,要充分理解
原始序列是第N个位置的数字就是N本身
对应到操作完成后的序列中第k个数字

下面介绍另一个思路
经过x轮操作后剩下了k个数,那么原始数据有多少个数据?

假设C表示当前操作后剩余的数字个数
显然在最后的时候有C=k

那么在倒数第二轮呢?
C个数据是从前一轮的数中,删除掉了y的整数倍后的结果
也就是每y个数字保留(y-1)个数字

假设有g组y变成(y-1),则最终删掉的个数就是g个

那么删掉了多少组呢?粗略的说是C/(y-1)
也就是C能包含多少组(y-1)就应该删掉了多少组

但是一个隐蔽的问题,如果C恰好能整除(y-1)
这一组显然是不能统计的,因为这次删除只会对>C的个数生效。

更精确的应该是(C-1)/(y-1) 不妨设前一轮是B 则

\(B = C + \frac{C-1}{y-1}\)

经过x轮递推操作后,最后得到的就是原始数据中有多少个数字。

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    i64 x, y, k; cin >> x >> y >> k;
    //注意y=1每轮都是删除所有数字
    //下面的除数是(y-1)
    if (y == 1) {
        cout << -1 << endl;
        return;
    }
    i64 mx = 1e12;
    i64 ans = k;
    for (int i = 0; i < x; i++) {
        ans = ans + (ans - 1) / (y - 1);
        if (ans > mx) {
            cout << -1 << endl;
            return;
        }
    }
    cout << ans << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

D2. Removal of a Sequence (Hard Version)

同D1描述一样,但是x<=1e12
因此必然无法通过枚举x来处理,二分和递推都需要枚举x

但是用方法2的递推公式可以加速处理。

新增知识点 【分块除法】

举个简单例子 k = 100 y = 100
根据方法2的递推公式

\(N = N + \frac{N-1}{y-1}\) 初始N=k

则一轮操作 原始值 N = 101
二轮 N = 102
三轮 N = 103
...

可以发现这个增长是非常缓慢且有规律的
要经过99轮后
N = 199

之后每轮N增加2
...

因此我们可以预先计算出当前累加值在多少轮保持不变

直接得到下一个大的周期的N值。

当前增加的值add
\(add = \frac{N-1}{y-1}\)

如果add增加1 则对应的N(不妨设为M)应该是

\(M = (add + 1) * (y - 1) + 1\)

从N增加到M需要的轮数是

\((M - N + add - 1) / add\)

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
    i64 x, y, k; cin >> x >> y >> k;
    if (y == 1) {
        cout << -1 << endl;
        return;
    }
    i64 mx = 1e12;
    i64 N = k; //初始
    for (i64 i = 0; i < x; ) {
        i64 add = (N - 1) / (y - 1); //当前轮增加量
        //每次增加0 也不能再有变化,这种现象对应k小y大的情况
        //比如删除10000的倍数 但是只求第10个数
        if (add == 0) break; 
        i64 M = (add + 1) * (y - 1) + 1; //下一块的N值 加到这个值之后add变为add+1
        i64 cnt = (M - N + add - 1) / add;
        cnt = min(x - i, cnt);
        N += cnt * add;
        if (N > mx) {
            cout << -1 << endl;
            return;
        }
        i += cnt;
    }

    cout << N << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t-- > 0) {
        solve();
    }
}

E. Points Selection

AI泛滥的情况下CF2400的题 看了答案感觉还好啊
重点是要充分理解题意...

给定一个xy平面上的若干点,每个点有自己的权值
要求选择其中一部分点 记这些点的权值和为A
剩余点要用最小的矩形包围(注意边必须是水平垂直方向 且允许长宽为0) 记周长为B
最大化A+B

Educational的集大成者

  • 所谓最小的包围矩形,则必然四条边(可能重合)上必然各有一个点
  • 要求最大化 则矩形内和矩形外,以及同一条边上的其它点都应该归属A
  • 矩形的周长=所选点中(maxX-minX + maxY - minY) * 2
  • 需要考虑一个点出现在哪条边上
    如果出现在左边,则对周长的贡献是其中的-minY部分,这意味着剥离了其它点的关联。
  • 由于矩形可以缩为一条边或者一个点,故所有点都需要考虑位于0-4个边的各种情况
    四条边都是独立的,比如p1可能同时是矩形的左右上,这与x相同y不同的另一个点p2形成了一条线段。
  • 因此描述点位置的形式是一个状压集合
  • 整体状压DP
    dp[i][mask]表示枚举到前i个节点,矩形的mask边确定下的最大值
    mask是状压数据,用一个长度为4的二进制数据表示四条边是否选择。
    至多mask=15 表示四条边都已选择
    答案为dp[n][15]
    枚举每个点,可能不选,可能选做某些边
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 INF = LLONG_MAX / 2;

void solve() {
    int n; cin >> n;
    vector<i64> xs(n), ys(n), cs(n);
    for (int i = 0; i < n; i++) cin >> xs[i];
    for (int i = 0; i < n; i++) cin >> ys[i];
    for (int i = 0; i < n; i++) cin >> cs[i];
    vector<vector<i64>> dp(n + 1, vector<i64>(1 << 4, -INF));
    dp[0][0] = 0; //一个边都没确定显然是0,其它情况必须是-INF
    for (int i = 1; i <= n; i++) {
        //枚举边的状态,并计算可能从之前的状态转移过来
        for (int mask = 0; mask < (1 << 4); mask++) {
            i64 x = xs[i - 1], y = ys[i - 1], c = cs[i - 1];
            //不选当前点
            dp[i][mask] = max(dp[i][mask], dp[i - 1][mask] + c);
            //选当前点作为哪些边
            for (int k = 0; k < (1 << 4); k++) {
                //如果当前枚举的mask不能完全包含k 则不需要处理
                if ((mask | k) != mask) continue; 
                //再次强调 一个点可能是四条边里任意组合。以下累加变化量
                i64 d = 0;
                if (k & 1) d += y * 2; //上
                if (k & 2) d -= y * 2; //下
                if (k & 4) d -= x * 2; //左
                if (k & 8) d += x * 2; //右
                dp[i][mask] = max(dp[i][mask], dp[i - 1][mask ^ k] + d);
            }
        }
    }
    cout << dp[n][15] << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t;  cin >> t;
    while (t-- > 0) {
        solve();
    }
}
posted @ 2025-12-03 17:48  云上寒烟  阅读(0)  评论(0)    收藏  举报