总结
还是掉分。加上上一场已近掉了100分。虽然的确有其他的原因,但是也说明了现在的学习方式是错误的。后边应该每周就一场力扣,一场cf。不然补题的时间太多了,应该花些时间去系统的练习一个板块的题目。先试一试效果怎么样。比赛地址:https://codeforces.com/contest/2028
补题和反思
A:Alice’s Adventures in ‘‘Chess’’
老实话,现在想起来第一题看完就该放弃这场的。可惜没有。
反思和做法:
这个题目第一眼看上去,真的是懵的。想过是不是要考虑a,b和行动的关系。会不会有什么互质啥的,就一定能够到达。但是又想到是第一题不该这么难才对。
后边看了数据范围才发现直接模拟就行了。因为n,a和b都只有10。那么就还是有一个就是什么时候停止了。第一时间的想法是,如果走的和(a,b)的距离太大就可以停止了。试了一试,错了。因为有可能最终又回到起点(0, 0),然后一直循环。最后没什么想法。直接模拟100次。
代码:
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
int main() {
int t;
cin >> t;
while (t -- ) {
int n, a, b;
cin >> n >> a >> b;
string str;
cin >> str;
int x = 0, y = 0, flag = false;
for (int j = 0; j < 100 * n; j ++ ) {
int i = j % n;
if (str[i] == 'E') {
x += 1;
}
else if (str[i] == 'N') {
y += 1;
}
else if (str[i] == 'W'){
x -= 1;
}
else {
y -= 1;
}
if (a == x && b == y) {
flag = true;
break;
}
}
if (flag) {
cout << "Yes" << endl;
}
else {
cout << "No" << endl;
}
}
return 0;
}
B:Alice’s Adventures in Permuting
虽然写的时候,写了很久将近一个小时。但是也反映了我的数学这个板块真的有点薄弱了。当然代码能力也不是很行(笑)。
做法和反思:
拿到这个题目之后,我就知道可能要遭罪了。因为数据范围是1e18,那必须是O(1)或则是 O(logN) 的时间复杂度。可以说是相当不擅长这种需要分情况讨论,而且要把每种情况想的十分清楚的题目。
正好再好好分析分析,希望下次遇见类似的别还是这么抓瞎。
看到题目之后,我先想过怎么样才会输出-1。正好样例中两个例子,看完之后我觉得应该是当 b 等于 0 的时候,就会返回-1。但是提交之后错了,又再详细看了样例解释,才反应过来。
首先对于n个数字,我们应该需要将其变为0 ~ n -1的一个排列。也就是最大数字是n - 1。
当b = 0时,此时 a 数组是 一个长为 n,且所有元素均为 c 的数组。对于这个数组,当n足够大时,其能生成的最大数字是 c + 1。因为之后,数组的最大值是c + 1 但是MEX是c + 2。操作之后,但是这是 数组的MEX 又变成了 c + 1,而最大值是c+2。这样就进入了循环。例如[1,0,0]→[2,0,0]→[1,0,0]→[2,0,0]。
这里给了我启发,当b = 0时,应该讨论 n - 1 和 c + 1的关系。
- 当n - 1 > c + 1时。会陷入上述的循环过程中,返回-1.
- 当n - 1 < c 时。那么我们只需要操作n次即可。
- 当c <= n - 1 <= c + 1时。我们只需要只需要操作 n - 1次。因为会跳过MEX=c。
当b != 0 时,a数组就是c,b + c,2*b + c … (n - 1)*b + c 。我的想法就是,先将c之前的数字填上。然后再考虑剩下的数字怎么做。分类讨论:
- 当c > n - 1 时。我们直接操作n次即可。
- 当c = n - 1 时。我们需要操作n - 1次即可。
- 当 c < n - 1时。我们可以先操作 c 次,将问题变为把 b + c, 2*b + c,…变为c+1,c+2,c+3…。注意这里只有(n - c - 1)个数字了。这里我们可以发现每b个数,我们就可以少操作一次。所以需要操作 c + (n - c - 1) % b + (n - c - 1) / b * (b - 1)。
代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int t;
cin >> t;
while (t -- ) {
long long n, b, c;
cin >> n >> b >> c;
if (b == 0) {
if (n - 1 > c + 1) {
cout << -1 << endl;
}
else {
cout << n - (n - 1 >= c) << endl;
}
continue;
}
long long ans;
if (c > n - 1) {
ans = n;
}
else if (c == n - 1) {
ans = n - 1;
}
else {
ans = c + (n - c - 1) % b + (n - c - 1) / b * (b - 1);
}
cout << ans << endl;
}
return 0;
}
C:Alice’s Adventures in Cutting Cake
这个想出来还挺快,就是写代码太慢了。结束后几分钟写出来了。
做法
简单来说就是前后缀分解。如果你看了这里又知道什么是前后缀分解,那么可以试着重新再想一想这个题目。
我们可以预处理一个后缀数组post,其中post[i]表示,从post[i] ~ n能够分出 i 个符合要求的蛋糕。和一个前缀数组pre,其中pre[i],表示从1~pre[i],可以分出 i 个符合要求的蛋糕。
处理结束后,如果post的长度小于 m,那么表示无法分出 m 个符合要求的蛋糕。返回-1即可。
然后我们枚举前边分多少个蛋糕 i,那么后边需要分出 m - i个蛋糕。如果pre[i] < post[m - i],那么留给爱丽丝的就是(pre[i] 和 post[m - i]),这个范围中 a数组的和。可以通过前缀和实现O(1)的计算。最终返回爱丽丝能够得到的最大值即可。
这里我们可以一边计算pre,一边统计答案。但是写起来感觉比较麻烦。因为我就只这么写的(哭)。代码中我习惯从0开始。
代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int t;
cin >> t;
while (t -- ) {
int n, m, v;
cin >> n >> m >> v;
vector<long long> a(n), sum(n + 1, 0);
for (int i = 0 ; i < n; i ++ ) {
cin >> a[i];
sum[i + 1] = sum[i] + a[i];
}
vector<int> post;
long long now = 0;
for (int i = n - 1; i >= 0; i -- ) {
now += a[i];
if (now >= v) {
post.push_back(i);
now = 0;
}
}
if (post.size() < m) {
cout << -1 << endl;
continue;
}
now = 0;
long long ans = sum[post[m - 1]];
for (int i = 0, j = 0; i < n; i ++ ) {
now += a[i];
if (now >= v) {
now = 0;
j ++ ;
if (j == m) {
ans = max(ans, sum[n] - sum[i + 1]);
break;
}
else {
int idx = post[m - j - 1];
// cout << idx << endl;
if (idx > i) {
// cout << sum[idx] - sum[i + 1] << endl;
ans = max(sum[idx] - sum[i + 1], ans);
}
}
}
}
cout << ans << endl;
}
return 0;
}
D:Alice’s Adventures in Cards
这个题属于后边写的,没思路。看到题解。不过题解写的有点难懂,看都看了好久才明白。我的想法错的很远,所以没什么好说的。不过这个题目加深了我对dp的认识。因为之前写的题目都比较裸,所以没发现,原来 dp 状态和转移真是五花八门。而且很多时候,转移方式没事那么直接,还需要一些贪心啥的来简化。
为什么这么做?
省流就是dp,用dp[i]表示当爱丽丝手中牌是 i 时,能否跳到n。
具体而言。从后向前做,每次分别判断在 q,k,j中是否有一个是否能够跳到n。如果可以,那么dp[i] = true。
那么这里就有一个问题了:如何判断 能否从 i 跳到 n?
我们拿q举例。如果能从在q中,从 i 跳到 n,我们需要找到一个 j,j > i && q[ j ] < q[ i ]。并且dp[ j ] = true,那么我们就可以从 i 跳到 n。
这里又牵扯出一个问题:对于 i,我该怎么找到这个 j ?
同样那q举例。我们是从后往前遍历的,所以,i 之前计算的所有dp[ j ] = true 的 j,都是满足 j > i && dp[ j ] = true 这两个条件的。所以只需要在这些 j 中找到满足q[j] < q[ i ]即可。但是如果每次都遍所有 j, 这样时间复杂度会是O(N^2)的。所以我们需要记录一个值,能够代表所有满足条件的 j。这里因为需要 q[ j ] < q[ i ]。那么我们只需要记录满足条件中的 最小的q[ j ] = minP 即可。那么我们只需要通过 q[ i ] 和 minP的大小关系,即可判断 在 q 中 i 能否跳到 n。
同样又有一个问题:如何维护 minP?
我们需要注意因为又q,k,j三个数组,所以我们也有三个minP,用 minP[i] 来区分。我们可以注意到,三个数组中,如果存在一个数组能够在 i 跳到 n。我们要用q[ i ] 和 minP[0],k[ i ] 和 minP[1],j[ i ] 和 minP[2],分别取一个min。来更新minP。为什么呐?因为我们能在 三个 数组中任意切换。只要有一个能够到 n,那么对于 i 之前的位置,只要能够到达 i,就能跳到 n。
这算是一部分,剩下就是一个dp,和记录路径的问题了。我们需要记录每个 i,是在那个 数组上 交换的,和交换之后的值。最终输出路径即可。
代码:
这里值得一提的是,我将dp这个数组略去了,因为我们最终只需要判断 1 是否能够跳到n。因为我们是逆序遍历的,所以最终的flag,就是1能不能跳到 n。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int main() {
int t;
cin >> t;
while (t -- ) {
int n;
cin >> n;
vector<vector<int>> op(3, vector<int>(n));
for (int i = 0; i < 3; i ++ ) {
for (int j = 0; j < n; j ++ ) {
cin >> op[i][j];
}
}
int flag;
vector<pair<int, int>> minP(3), f(n + 1);
for (int i = 0; i < 3; i ++ ) {
minP[i] = {op[i][n - 1], n};
}
for (int i = n - 2; i >= 0; i -- ) {
flag = false;
for (int j = 0; j < 3; j ++ ) {
if (op[j][i] > minP[j].first) {
f[i + 1] = {j, minP[j].second};
flag = true;
break;
}
}
if (!flag) {
continue;
}
for (int j = 0; j < 3; j ++ ) {
if (minP[j].first > op[j][i]) {
minP[j] = {op[j][i], i + 1};
}
}
}
string str = "QKJ";
if (flag) {
cout << "Yes" << endl;
int now = 1;
vector<pair<int, int>> ans;
while (now < n) {
ans.push_back(f[now]);
now = f[now].second;
}
cout << ans.size() << endl;
for (auto [x, y] : ans) {
cout << str[x] << " " << y << endl;
}
}
else {
cout << "No" << endl;
}
}
return 0;
}