(KDY)CSP-J模拟赛二补题报告

(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地。求完成所有工作的最少用时。

赛时思路:
按照题目所给策略进行模拟。

  1. 若在B地累计工作时长加上当前需工作时间仍然小于 240 240 240时,累加时间;
  2. 若在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;
}

题解:

正确的思路将此题分类为两种进行讨论:

  1. 累加所有工作时间,即一直在B地工作,工作完后返回A地,如果超时接受罚时;
  2. 采用题意策略,即赛时思路做法,但可以优化,不许要把 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| xa+ab+bx

现在有 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 axb:行驶距离为 2 ∗ ( b − a ) 2 * (b - a) 2(ba)
  • x < a x<a xa:行驶距离为 2 ∗ ( b − a ) + 2 ∗ ( a − x ) 2 * (b - a)+2 * (a-x) 2(ba)+2ax
  • b < x b < x b<x:行驶距离为 2 ∗ ( b − a ) + 2 ∗ ( x − b ) 2 * (b - a)+2 * (x-b) 2(ba)+2xb

a = 4 a=4 a=4 b = 6 b=6 b=6,则多出的每个店的多出的行驶距离为:

站台12345678910
左侧a[i]6420000000
右侧b[i]0000002468

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,并学会骗分技巧。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值