【Acwing 周赛复盘】第89场周赛复盘(2023.2.4)
周赛复盘 ✍️
本周个人排名:1302/2232
AC情况:2/3
周赛当天晚上,博主出门在外,未实时参加,是在之后定时自测的(终于把春节欠的债还完了😂)
这也是博主参加的第四次周赛,感觉做题比之前三次都顺畅了不少(也可能是这次的难度较低),20min里 AC 了 2 题,估计现场参加的话能排 270 名左右,对自己来说还是有进步的。🍉
T1 签到题,枚举即可。✅
T2 直接模拟,时间复杂度 O ( n 3 ) O(n^3) O(n3),勉强通过。(后来看y总直播讲解貌似也是直接 O ( n 3 ) O(n^3) O(n3) 暴力过,因为题目数据范围较小)✅
T3 一眼DP,但是数学层面的性质较难想到,导致状态转移方程始终差一点,未能AC(经过y总讲解后明白了如何从数学层面推导相关性质,值得积累)❌
希望未来也能继续努力,紧跟大佬们的步伐,继续加油 💪
周赛信息 📚
时间:2023年 2 月 4 日 19:00-20:15
竞赛链接:https://www.acwing.com/activity/content/2854/
y总直播间:http://live.bilibili.com/21871779
y总录播讲解视频:【AcWing杯 - 第 89 场周赛讲解】
题目列表 🧑🏻💻
题目名称 | 原题链接 | 视频讲解 | 难度 |
---|---|---|---|
4803. 满足的数 | 原题链接 | 视频链接 | 简单 🟢 |
4804. 构造矩阵 | 原题链接 | 视频链接 | 中等 🟡 |
4805. 加减乘 | 原题链接 | 视频链接 | 困难 🔴 |
题解 🚀
【题目A】满足的数
【题目描述】
给定 n n n 个不超过 5 5 5 的正整数 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an。
不妨设 S = a 1 + a 2 + ⋯ + a n S=a_1+a_2+\dots+a_n S=a1+a2+⋯+an。
请你统计,一共有多少个不同的整数 x x x 能够同时满足以下所有条件:
- 1 ≤ x ≤ 5 1 \le x \le 5 1≤x≤5
- ( S + x ) m o d ( n + 1 ) ≠ 1 (S+x)mod(n+1) \ne 1 (S+x)mod(n+1)=1
【输入】
第一行包含整数 n n n。
第二行包含 n n n 个正整数 a 1 , a 2 , … , a n a_1,a_2,\dots ,a_n a1,a2,…,an。
【输出】
一个整数,表示满足条件的 x x x 的数量。
【数据范围】
前 3 3 3 个测试点满足 1 ≤ n ≤ 2 1 \le n \le 2 1≤n≤2。
所有测试点满足 1 ≤ n ≤ 100 1 \le n \le100 1≤n≤100, 1 ≤ a i ≤ 5 1 \le ai \le 5 1≤ai≤5。
【输入样例1】
1
1
【输出样例1】
3
【输入样例2】
1
2
【输出样例2】
2
【输入样例3】
2
3 5
【输出样例3】
3
【原题链接】
https://www.acwing.com/problem/content/4806/
【题目分析】
签到题,枚举 + 模拟即可。
【复盘后的优化代码】✅
#include<bits/stdc++.h>
using namespace std;
int n, a, s;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a;
s += a;
}
int cnt = 0;
for (int i = 1; i <= 5; i++) {
if ((s + i) % (n + 1) != 1)
cnt++;
}
cout << cnt << endl;
return 0;
}
【周赛现场 AC 代码】
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int s[N], a[N], n;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
int cnt = 0;
for (int i = 1; i <= 5; i++) {
if ((s[n] + i) % (n + 1) != 1)
cnt++;
}
cout << cnt << endl;
return 0;
}
【代码对比分析】
空间复杂度 进行了部分优化。
【题目B】构造矩阵
【题目描述】
给定一个 m m m 行 n n n 列的 01 01 01 矩阵 B B B。
请你构造一个 m m m 行 n n n 列的 01 01 01 矩阵 A A A。
矩阵的行编号为 1 ∼ m 1∼m 1∼m,列编号为 1 ∼ n 1∼n 1∼n。
B i j B_{ij} Bij 表示矩阵 B B B 中第 i i i 行第 j j j 列的元素。
要求,构造的矩阵 A A A 满足,对于每一个 B i j B_{ij} Bij:
- 如果 B i j = 0 B_{ij}=0 Bij=0,则矩阵 A A A 中第 i i i 行的 n n n 个元素以及第 j j j 列的 m m m 个元素总共 n + m − 1 n+m−1 n+m−1 个元素 必须 全都为 0 0 0。
- 如果 B i j = 1 B_{ij}=1 Bij=1,则矩阵 A A A 中第 i i i 行的 n n n 个元素以及第 j j j 列的 m m m 个元素总共 n + m − 1 n+m−1 n+m−1 个元素 不能 全都为 0 0 0。
【输入】
第一行包含两个整数 m , n m,n m,n。
接下来 m m m 行,每行包含 n n n 个整数 0 0 0 或 1 1 1,其中第 i i i 行第 j j j 列的整数表示 B i j B_{ij} Bij。
【输出】
如果不存在满足条件的矩阵
A
A
A,则输出一行 NO
即可。
否则,第一行输出 YES
,接下来
m
m
m 行,每行输出
n
n
n 个整数,用来表示矩阵
A
A
A。
如果答案不唯一,则输出任意合理答案均可。
【数据范围】
前 5 5 5 个测试点满足 1 ≤ m , n ≤ 5 1 \le m,n \le 5 1≤m,n≤5。
所有测试点满足 1 ≤ m , n ≤ 100 1 \le m,n \le 100 1≤m,n≤100。
【输入样例1】
2 2
1 0
0 0
【输出样例1】
NO
【输入样例2】
2 3
1 1 1
1 1 1
【输出样例2】
YES
1 1 1
1 1 1
【输入样例3】
2 3
0 1 0
1 1 1
【输出样例3】
YES
0 0 0
0 1 0
【原题链接】
https://www.acwing.com/problem/content/4807/
【题目分析】
周赛时看到数据范围较小,且暂时没有想到更好的方法,就直接模拟了(时间复杂度 O ( n 3 ) O(n^3) O(n3),思路详见代码注释),想着等周赛结束后,再复盘进行优化。没想到y总讲解时,也是直接用了 O ( n 3 ) O(n^3) O(n3) 的做法,弄巧成拙,2333。😂
这也告诉我在比赛时,如果没有太好的优化思路,先把当前的想法实现是最重要的,不要眼高手低。(除非当前思路无法 AC,否则不要死磕优化,毕竟比赛期间拿分最重要。优化可以等复盘时再仔细思考)
当然,本题硬要优化也不是不可以,但是会有特判,感觉实际写起来也没简化多少。
【周赛现场 AC 代码】
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int a[N][N], b[N][N], m, n;
// 把a的第i行和第j列置为0
void setV(int row, int col) {
for (int i = 1; i <= n; ++i) a[row][i] = 0;
for (int i = 1; i <= m; ++i) a[i][col] = 0;
}
// 根据b[i][j]的值,判断当前矩阵a是否满足条件
bool check(int row, int col) {
// b[i][j] = 1,判断a中第i行和第j列是否存在1,有1返回true
if (b[row][col]) {
for (int i = 1; i <= n; ++i) {
if (a[row][i] == 1)
return true;
}
for (int i = 1; i <= m; ++i) {
if (a[i][col] == 1) {
return true;
}
}
return false;
}
// b[i][j] = 0无需判断,因为a的第i行第j列在之前已经被置为0
return true;
}
int main() {
ios::sync_with_stdio(false); //cin读入优化
cin.tie(0);
// 读入矩阵b,同时把矩阵a全部置为1
cin >> m >> n;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> b[i][j];
a[i][j] = 1;
}
}
// 如果b[i][j]=0,就把a的第i行和第j列置为0
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (!b[i][j]) {
setV(i, j);
}
}
}
// 判断矩阵a是否满足条件
bool flag = true;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (!check(i, j)) {
flag = false;
goto end;
}
}
}
// 输出结果
end:
if (flag) {
cout << "YES" << endl;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
cout << a[i][j] << " ";
}
cout << endl;
}
} else {
cout << "NO" << endl;
}
return 0;
}
【题目C】加减乘
【题目描述】
规定两种数字操作:
- 加减操作,将数字加 1 1 1 或减 1 1 1,代价为 x x x。
- 乘法操作,将数字乘 2 2 2,代价为 y y y。
每种操作的使用次数不限。
请你计算,通过上述操作,将 0 0 0 变为 n n n,所需花费的最小代价。
【输入】
共一行,包含三个整数 n , x , y n,x,y n,x,y。
【输出】
一个整数,表示最小代价。
【数据范围】
前
5
5
5 个测试点满足
1
≤
n
≤
100
1 \le n \le 100
1≤n≤100。
所有测试点满足
1
≤
n
≤
1
0
7
1\le n \le 10^7
1≤n≤107,
1
≤
x
,
y
≤
1
0
9
1 \le x,y \le 10^9
1≤x,y≤109。
【输入样例1】
8 1 1
【输出样例1】
4
【输入样例2】
8 1 10
【输出样例2】
8
【原题链接】
https://www.acwing.com/problem/content/description/4808/
【题目分析】
一眼DP,但是略有难度,思路如下(详解见y总视频讲解 视频链接 ):
- 将 0 0 0 → n n n 的过程转化为 n n n → 0 0 0 的过程
- 分析、推导出两个重要性质
- 加法和减法不能连续出现
- 加法不能连续出现
- 有了上面两个性质,就可以推导状态转移方程了
- n n n 为奇数时, d p [ i ] = min ( d p [ i − 1 ] + x , d p [ ( i + 1 ) / 2 ] + x + y ) dp[i] = \min (dp[i - 1] + x, dp[(i + 1)/2] + x + y) dp[i]=min(dp[i−1]+x,dp[(i+1)/2]+x+y)
- n n n 为偶数时, d p [ i ] = min ( d p [ i / 2 ] + y , d p [ i − 1 ] + x ) dp[i] = \min (dp[i/2] + y, dp[i-1] + x) dp[i]=min(dp[i/2]+y,dp[i−1]+x)
- 边界条件: n = 1 n = 1 n=1 时, d p [ 1 ] = x dp[1] = x dp[1]=x
附:
加法操作不能连续的证明
状态转移方程:
【复盘后的优化代码】✅
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e7 + 10;
LL dp[N]; // 内存计算:8Byte * 1e7 = 80MB < 256MB
int n, x, y;
int main() {
cin >> n >> x >> y;
// 求最小值,将dp数组置为一个较大值
memset(dp, 0x3f, sizeof(dp));
dp[1] = x;
for (int i = 2; i <= n; i++) {
if (i & 1)
dp[i] = min(dp[i - 1] + x, dp[i + 1 >> 1] + x + y);
else
dp[i] = min(dp[i / 2] + y, dp[i - 1] + x);
}
cout << dp[n] << endl;
return 0;
}
【周赛现场未 AC 代码】❌
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int x, y, n;
LL sum, dp[N];
int main() {
cin >> n >> x >> y;
dp[1] = x;
for (int i = 2; i < N; ++i) {
if (i & 1) {
dp[i] = (LL) dp[i - 1] + x;
} else {
dp[i] = min((LL) dp[i - 1] + x, (LL) dp[i / 2] + y);
}
}
cout << dp[n] << endl;
return 0;
}
【代码对比总结】
对比代码,可以发现周赛现场的代码和 AC 代码就差临门一脚,但是经过y总分析后,两者其实天差地别。
因为自己比赛时,没有从数学层面推导出相关性质,不知道该如何证明,所以始终不知道如何处理 -1 的操作(从 0 0 0 → n n n 的角度)
因为状态转移方程如果写成 d p ( i ) = min ( d p ( i / 2 ) + y , d p ( i − 1 ) + x , d p ( i + 1 ) + x ) , i 为偶数 dp(i) = \min(dp(i/2)+y,dp(i-1)+x,dp(i+1)+x),i为偶数 dp(i)=min(dp(i/2)+y,dp(i−1)+x,dp(i+1)+x),i为偶数
在推导 d p ( i ) dp(i) dp(i) 的时候用到了 d p ( i + 1 ) dp(i+1) dp(i+1),就会使程序陷入了死循环。
所以博主在周赛现场单纯去掉了 d p ( i + 1 ) dp(i+1) dp(i+1),保留了另外剩下两个,想碰碰运气。💦
但是事实证明,虽然这在 i i i 为偶数时是恰好正确的,但是在奇数时,就是完全错误的,因此始终不能 AC。
说明之后还是要多积累,多学习数学方面的证明,让代码有理可依。