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();
}
}
浙公网安备 33010602011771号