(KDY)CSP-J模拟赛二补题报告
日期:2023年10月1日
文章目录
一、AC情况
第一题 | 第二题 | 第三题 | 第四题 |
---|---|---|---|
WA 70分(赛后AC) | TLE 10分(赛后AC) | TLE 50分(赛后AC) | WA 10分(赛后AC) |
总计130分。
二、赛中概况
第一题写了一个比较麻烦的代码,觉得应该会WA,于是放弃;
第二题想了一会没有更省时的方法,写了一个枚举,猜测会TLE;
第三题看了数据和题目,以为要用二分,但想不到方法,于是枚举,猜测TLE;
第四题,认为是DP,偏分WA。
三、解题报告
问题一:人员借调(transfer)
情况:
WA,赛后AC
题意:
一个人从A地到B地处理工作,一共n项工作,必须按顺序完成。A、B两地往返需要$400¥分钟。每项工作都用一个时间(分钟),当在B地连续工作时间超过 240 240 240分钟时,需要在A地滞留 10080 10080 10080分钟作为罚时。现在的策略为:当工作时长快到 240 240 240分钟时返回A地,再立刻回到B地。求完成所有工作的最少用时。
赛时思路:
按照题目所给策略进行模拟。
- 若在B地累计工作时长加上当前需工作时间仍然小于 240 240 240时,累加时间;
- 若在B地累计工作时长加上当前需工作时间超过或等于
240
240
240:
- 这个需工作时间超过或等于 240 240 240,即无法避免罚时,则累加时间并返回A地滞留并接受罚时,然后清空累计工作时长,返回B地;
- 需工作时间小于 240 240 240,即正常的合法情况,则回到A地并迅速前往B地,以清空工作时长。
此外,在考虑往返时间时,赛时觉得以 400 400 400计麻烦,就以A→B为 200 200 200,B→A为 200 200 200来计算。
赛时代码:
#include <bits/stdc++.h>
using namespace std;
int n, a[1010];
int main() {
freopen("transfer.in", "r", stdin);
freopen("transfer.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int cnt = 0, ans = 0, t = 1;
bool flag = 0;
while (t <= n) {
if (!flag) ans += 200;
flag = 1;
if (cnt + a[t] < 240) {
cnt += a[t];
ans += a[t];
t++;
} else {
if (a[t] >= 240) {
cnt = 0;
ans += a[t];
t++;
ans += 200 + 10080;
flag = 0;
} else {
cnt = 0;
ans += 200;
flag = 0;
}
}
}
if (flag) ans += 200;
printf("%d", ans);
fclose(stdin);
fclose(stdout);
return 0;
}
题解:
正确的思路将此题分类为两种进行讨论:
- 累加所有工作时间,即一直在B地工作,工作完后返回A地,如果超时接受罚时;
- 采用题意策略,即赛时思路做法,但可以优化,不许要把 400 400 400分成 200 + 200 200+200 200+200计算。
最后比较两种情况的耗时,选取最优时长。
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n, a[1010];
int main() {
scanf("%d", &n);
int c = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
c += a[i]; //情况一:累加工作时间
}
if (c >= 240) c += 10080;
c += 400;
int cnt = 0, ans = 400, t = 1;
while (t <= n) { //情况二:按题意策略计时
if (cnt + a[t] < 240) {
cnt += a[t];
ans += a[t];
t++;
} else {
cnt = 0;
ans += 400;
if (a[t] >= 240) {
ans += a[t] + 10080;
t++;
}
}
}
printf("%d", min(ans, c));
return 0;
}
问题二:计算(calc)
情况:
TLE,赛后AC
题意:
T T T组数据,每组三个正整数 n n n, m m m, k k k,查找从 n n n到 m m m中各位数字相加和等于 k k k的数,多个答案中选取各位数字相乘积最大的数(仍有重复,选取最小的)。
赛时思路:
每次输入 n n n, m m m, k k k,从 n n n到 m m m遍历,每次拆分数字并计算出其各位相加与相乘,保存最小的满足题意的乘积最大值。
赛时代码:
#include <bits/stdc++.h>
using namespace std;
int T, m, n, k;
int main() {
freopen("calc.in", "r", stdin);
freopen("calc.out", "w", stdout);
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &m, &n, &k);
int f = 0, x;
for (int i = m; i <= n; i++) {
int t = i, c1 = 0, c2 = 1;
while (t != 0) {
c1 += t % 10;
c2 *= t % 10;
t /= 10;
}
if (c1 == k && f < c2) {
x = i;
f = c2;
}
}
printf("%d %d\n", x, f);
}
fclose(stdin);
fclose(stdout);
return 0;
}
题解:
当遇到较大数据,且每种情况对应的答案是唯一的,答案会被重复利用时,考虑打表。
可以事先把所有的数字的各位数字和与积,存储到表里,每次输入数据是遍历查表,筛选相符与题意的答案,这样可以把原来的时间复杂度 Θ ( n 3 ) Θ(n^3) Θ(n3)降级为 Θ ( n 2 ) Θ(n^2) Θ(n2)。
AC代码:
#include <bits/stdc++.h>
using namespace std;
int a[5000020][5];
int T, n, m, k;
int main() {
for (int i = 1; i <= 5000000; i++) {
int t = i;
a[i][2] = 1;
while (t) { //拆分数字
a[i][2] *= t % 10; //a[i][1]存和
a[i][1] += t % 10; //a[i][2]存积
t /= 10;
}
}
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &k);
int maxn = INT_MIN, at;
for (int i = n; i <= m; i++) { //遍历查表
if (a[i][1] == k && a[i][2] > maxn) {
maxn = a[i][2];
at = i;
}
}
printf("%d %d\n", at, maxn);
}
return 0;
}
问题三:智能公交(transit)
情况:
TLE,赛后AC
题意:
共有 n n n个公交站台,编号从 1 1 1 ~ n n n ,相邻两个站台距离为1。有一辆智能公交车会在站台之间双向行驶往返。
若智能公交上没有乘客,那么智能公交就会停靠在 x x x站台。
有人要乘坐智能公交,公交会从 x x x站台到起点 a a a载上乘客,行驶到终点 b b b,再返回 x x x站台,则行驶距离为 ∣ x − a ∣ + ∣ a − b ∣ + ∣ b − x ∣ |x-a|+|a-b|+|b-x| ∣x−a∣+∣a−b∣+∣b−x∣。
现在有 m m m个人要依次乘坐智能公交,每个人都会等待智能公交停在 x x x站台之后呼叫公交。寻找 x x x,使公交总行驶距离最短。存在多个答案,选择编号较小的一个。
赛时思路:
从 1 1 1到 n n n枚举站点,对于每一个站点作为 x x x站台计算总行驶距离,选取最优的 x x x。
赛时代码:
#include <bits/stdc++.h>
using namespace std;
int n, m, a[500010][5];
int Abs(int n) {
return (n >= 0 ? n : (0 - n));
}
int main() {
freopen("transit.in", "r", stdin);
freopen("transit.out", "w", stdout);
scanf("%d%d", &n, &m);
int l = INT_MAX, r = INT_MIN;
for (int i = 1; i <= m; i++) {
scanf("%d%d", &a[i][1], &a[i][2]);
l = min(l, min(a[i][1], a[i][2]));
r = max(r, max(a[i][1], a[i][2]));
}
long long int minn = 0, at;
for (int i = l; i <= r; i++) {
long long int t = 0;
for (int j = 1; j <= m; j++) {
t += Abs(i - a[j][1]) + Abs(a[j][2] - a[j][1]) + Abs(a[j][2] - i);
}
if (minn == 0 || t < minn) {
minn = t;
at = i;
}
}
printf("%lld %lld", at, minn);
fclose(stdin);
fclose(stdout);
return 0;
}
题解:
从单个行驶情况考虑。
a a a是起点, b b b是终点, x x x有如下情况:
- a ≤ x ≤ b a≤x≤b a≤x≤b:行驶距离为 2 ∗ ( b − a ) 2 * (b - a) 2∗(b−a);
- x < a x<a x<a:行驶距离为 2 ∗ ( b − a ) + 2 ∗ ( a − x ) 2 * (b - a)+2 * (a-x) 2∗(b−a)+2∗(a−x);
- b < x b < x b<x:行驶距离为 2 ∗ ( b − a ) + 2 ∗ ( x − b ) 2 * (b - a)+2 * (x-b) 2∗(b−a)+2∗(x−b)。
若 a = 4 a=4 a=4, b = 6 b=6 b=6,则多出的每个店的多出的行驶距离为:
站台 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
左侧a[i] | 6 | 4 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
右侧b[i] | 0 | 0 | 0 | 0 | 0 | 0 | 2 | 4 | 6 | 8 |
从 a + 1 a+1 a+1和 b + 1 b+1 b+1开始向两侧,以 2 2 2为公差递增。
把每个站点的数据累加,两次前缀和与差分求出行驶距离,找到第一个 a [ i ] + b [ i ] a[i]+b[i] a[i]+b[i]最小值,即为所求站台编号。
然后遍历每个乘客,求出最终的行驶距离。
注意:数据过大,需要使用long long类型。
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int n, m, A[N], B[N];
long long int a[N], b[N]; //左侧、右侧累加多出的距离并求前缀和的数组,注意long long 类型
long long int Abs(long long int n) { //绝对值,用于求距离
return (n >= 0 ? n : (0 - n));
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &A[i], &B[i]);
a[A[i] - 1] += 2; //左、右标记
b[B[i] + 1] += 2;
}
for (int i = n - 1; i >= 1; i--) a[i] += a[i + 1]; //两次前缀和和差分求累计的多余距离
for (int i = 1; i <= n; i++) b[i] += b[i - 1];
for (int i = n - 1; i >= 1; i--) a[i] += a[i + 1];
for (int i = 1; i <= n; i++) b[i] += b[i - 1];
long long int minn = -1, at = 0;
for (int i = 1; i <= n; i++) {
if (minn == -1 || minn > a[i] + b[i]) {
minn = a[i] + b[i];
at = i; //存储最优站点编号
}
}
long long int ans = 0;
for (int i = 1; i <= m; i++) ans += Abs(at - A[i]) + Abs(B[i] - A[i]) + Abs(B[i] - at); //计算总行驶距离
printf("%lld %lld", at, ans);
return 0;
}
问题四:异或和(exclusive)
情况:
WA,赛后AC
题意:
多个集合中共有 n n n个数字,已知每个数字的大小和属于某个集合,在一个集合中选择一个数字,收益为这个数字的大小;选择多个数字,收益为这些数字的异或和。总收益为每个集合的收益之和。求最大总收益。
赛时思路:
骗分。
赛时代码:
#include <bits/stdc++.h>
using namespace std;
int p[2010][2010];
int n, m, a, b, ans;
int main() {
freopen("exclusive.in", "r", stdin);
freopen("exclusive.out", "w", stdout);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a >> b;
p[b][++p[b][0]] = a;
}
for (int i = 1; i <= 2005; i++) {
int t = 0;
for (int j = 1; j <= p[i][0]; j++) t = max(t, p[i][j]);
ans += t;
}
cout << ans;
// fclose(stdin);
fclose(stdout);
return 0;
}
题解:
n个数分为多个组,最多选m个数,使得收益最大。总收益=多个组的收益之和,每组收益=本组选出所有数的异或结果。
对于同一组中的数字,如果选一个数和选多个数抑或出的结果相同,不如只选1个数。也就是说,如果同一组能找到收益最大的数可能由1个数,2个3个数得到,那么肯定选少的。这样可以把能够选择的余地交给其他组,使得最后得到的收益和尽可能大
分组背包:每组只能选1个元素,使得在不超过容量m前提下价值最大 。
- 类似:每组不限选几个元素,最多选m个前提下所有组的收益和最大 ;
- 类比:分组背包限制是重量m,本题限制是数量m。分组背包决策是能否选第i组的第k个数,本题决策是能否选第i组的k个数字。因此,只要能求出第i组选k个数字的最大收益num[i][k]即可 。求num[i][k]需得出每个组选k个数的最大收益,那这个k应该尽可能小。
而所有数能够得到的异或结果范围唯一确定0-2047。前面全1异或后面全1得到全1。对于0来说需要两个相同的数异或得到,而0又不如只选这相同数的某一个,可不考虑 。由最长上升子序列做法可知,完全可以用以i为终点来作为划分 ,因此dp[i][j]即前i个数能够异或出1~2047所需要的最少个数 。由于0无法表示不能异或得到,即个数无限大都无法异或到 。
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n, m, dp[2005][2050], num[2005][2005], dpp[2050], zz[2005];
//dp[i][j]表示前i个数,得到收益j至少所需要的数字个数
//num[i][j]表示第i组选j个数最大的收益值
//dpp[j] 选j个数最大的收益值
int ve[2005][2005];
const int inf=0x3f3f3f3f;
int main() {
cin >> n >> m;//n个数字,最多选m个数字
for (int i = 1; i <= n; i++) {//输入n个数字
int x, y;//x是数字大小,y是所属集合编号
cin >> x >> y;
zz[y]++;//集合y的元素个数加一个
ve[y][zz[y]]=x;//y组的第zz[y]个元素是x
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 2047; j++) {
dp[i][j] = 1e9; //假设全不能异或得到即个数无限大
}
}
for (int zu = 1; zu <= 2000; zu++) {//遍历所有的组 ,一组一组的处理
if (zz[zu] != 0)//如果这一组有元素,需要处理
dp[1][ve[zu][1]] = 1;
//前1个数 能得到这一组的第一个数的收益值通过选择这一个数做到,最少个数1
for (int i = 2; i <= zz[zu]; i++) {//遍历这一组剩下的数字
// 前i个数,得到这个组的第i个数的收益可以通过直接选择这个数本身做到
dp[i][ve[zu][i]] = 1;
for (int j = 1; j <= 2047; j++) {//遍历所有可能的数字(收益值)
if (dp[i - 1][j] != 1e9) {
//如果前i-1个数字产生这个收益所用的最少数字个数存在(不为初始值)
//前i个数产生j这个数字(收益)的数字最少个数
//是前i个数和前i-1个数的使用数字个数的最小值
dp[i][j] = min(dp[i][j], dp[i - 1][j]);
int t=j^ve[zu][i];
dp[i][t] =min(dp[i][t],dp[i - 1][j] + 1);
//同时更新如果加入新数字ve[zu][i]后产生的收益结果
//前i个数字产生新数字t(收益)的数字使用个数
//是前i-1个数产生j的数字个数+1,和本来就有的数字个数的最小值
}
}
}
int t=zz[zu]; //这一组的总个数
//更新num[i][k]
for (int j = 1; j <= 2047; j++) {//遍历所有的收益
if (dp[t][j] != 1e9)
//若第t组异或出j的最少个数存在
//num数组记录这一组对应选至少这些数能够得到的收益j
num[zu][dp[t][j]] = j;
}
// memset(dp,0x3f,sizeof dp);
for (int i = 1; i <= zz[zu]; i++) {//dp数组重新初始化为下个组准备
for (int j = 1; j <= 2047; j++) {
dp[i][j] = 1e9;
}
}
}
for (int i = 1; i <= 2000; i++) {//遍历所有组
for (int j = m; j >= 1; j--) {//数量限制:能选的数字个数最多是m个
for (int k = 1; k <= zz[i];k++) { // 决策,是否选择该组第k个数
if (j >= k)//能从这个组选出k个数字
//j个数字产生的数字收益最大值要么不变,要么就是往前推k个数,
//从第i组选k个数字的最大收益累加
dpp[j] = max(dpp[j], dpp[j - k] + num[i][k]);
}
}
}
cout << dpp[m];//输出m个数字的最大收益
return 0;
}
总结
- 第一题 没有考虑到第二种情况,不周全;
- 第二题 没有想到简单的打表方法,要学会使用之前所学方法;
- 第三题 没有通过样例分析题目,发现题目的整洁方法;
- 第四题 dp,并学会骗分技巧。