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

这是一篇CSP - J模拟赛四的补题报告,涵盖AC情况、赛中概况、解题报告等内容。解题报告涉及四个问题及补充题,包括复读机、小可的矛与盾等,分析了赛时思路、存在问题及题解,最后总结做题策略,强调合理分配时间。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

日期:2023年10月3日


一、AC情况

第一题第二题第三题 Special Judge \tiny\color{FFFFFF}\colorbox{F39C11}{{{Special Judge}}} Special Judge第四题
AC 100分AC 100分WA 40分(赛后AC)WA 0分(赛后AC)

总计240分。

二、赛中概况

第一题写了一个模拟方法,调试几次就样例通过,造了几组数据也没有问题;

第二题想了一会,想到了简单的方法,迅速写完,通过大样例;

第三题看了数据和题目,肯定短时间拿不到全分,根据数据范围写了代码;

第四题,同第三题,但是时间不够,没有仔细调。

三、解题报告

问题一:复读机(repeater)

情况:

AC

题意:

  1. 复读定义:给定一个长度为 n n n的仅包含小写字母和数字的字符串,字母表示需要复读的消息,数字表示要复读的次数。
    例如: kdy3 \texttt{kdy3} kdy3表示将 kdy \texttt{kdy} kdy复读3遍,输出为: kdykdykdy \texttt{kdykdykdy} kdykdykdy

  2. 更复杂的复读模拟:这个字符串中可能包含多个数字,当多次出现数字时,例如 a5b2 \texttt{a5b2} a5b2,我们从左到右解析这个字符串, a5 \texttt{a5} a5表示将 a \texttt{a} a复读 5 遍,即原字符串变为 aaaaab \texttt{aaaaab} aaaaab,然后遇到数字 2 ,再将所有内容全部复读 2 遍,即 aaaabaaaab \texttt{aaaabaaaab} aaaabaaaab

赛时思路&题解:

把数字和字符分别存储,按照题意模拟即可。

AC代码:

#include <bits/stdc++.h>
using namespace std;
int a[500005];
string s, t[500005] = {""};
int main() {
    int q;
    cin >> q;
    while (q--) {
    	memset(a, 0, sizeof a);
    	int n;
    	cin >> n >> s;
    	int cnt = 0;
    	for (int i = 0; i < s.size(); i++) {
    		if (i > 0 && !isdigit(s[i]) && isdigit(s[i - 1])) cnt++;  //一组包含一个字符串和一个数字,跳转到下一组
    		if (!isdigit(s[i])) {   //字符
    			t[cnt] += s[i];
			} else {
				a[cnt] = a[cnt] * 10 + (s[i] - '0');  //数字
			}
		}
		cnt++;
		string ans = "";
		t[cnt] = "";
		for (int i = 0; i < cnt; i++) {
			ans += t[i];  //加上当前的字符串
			string f = ans;
			a[i]--;
			while (a[i]--) ans += f;   //复制前面的内容
			t[i] = "";
		}
		cout << ans << "\n";
	}
	return 0;
}

问题二:小可的矛与盾(spearshield)

情况:

AC

题意:

有n个人,编号从 1 1 1 n n n,第 x i x_i xi个人战斗力为 i i i ,有的人为矛,有的人为盾,两者防御力和本身的战斗力相同。选取一个编号 p o s pos pos将人分为两个阵营, [ 1 , p o s ] [1,pos] [1,pos]为第一阵营,只考虑矛的攻击力总和 w w w [ p o s + 1 , n ] [pos+1,n] [pos+1,n]为第二阵营,只考虑盾的防御力总和 v v v,请问对于所有的 p o s pos pos,要求 ∣ w − v ∣ |w-v| wv的值最小,请问 ∣ w − v ∣ |w-v| wv最小为多少。

赛时思路:

前缀和。

两个前缀和数组,第一个存矛的战斗力,第二个存盾的战斗力。遍历1~ n n n,用前缀和求出此时的前面的战斗力和后面的战斗力,保留相减的最小值。

赛时代码:

#include <bits/stdc++.h>
using namespace std;
char s[100010];
long long int mao[100010], dun[100010];
long long int Abs(long long int n) {   //绝对值
	return (n >= 0 ? n : (0 - n));
}
int main() {
	int n;
    cin >> n >> s;
    for (int i = 1; i <= n; i++) {
    	mao[i] = mao[i - 1];  //矛的前缀和
    	if (s[i - 1] == '0') mao[i] += i;
    	dun[i] = dun[i - 1];   //盾的前缀和
    	if (s[i - 1] == '1') dun[i] += i;
	}
	long long int minn = LLONG_MAX;
	for (int i = 1; i <= n; i++) {
		long long int pm = mao[i];  //以i为分界线矛的战斗力
		long long int pd = dun[n] - dun[i];  //以i为分界线盾的战斗力
		minn = min(minn, Abs(pm - pd));
	}
	printf("%lld", minn);
	return 0;
}

问题三:不合法字符串(illegality)

情况:

Special Judge \small\color{FFFFFF}\colorbox{F39C11}{{{Special Judge}}} Special Judge WA

题意:

现在给出若干个不合法的字符串 ,和一段较长字符串 ,用最少的*替换较长字符串中的若干字母使字符串中不存在不合法的字符串。

赛时思路:

根据数据范围:

20%的数据下: n = 1 n=1 n=1

暴力骗分。

遍历字符串,对于每一个字符作为起点,遍历若干个不合法字符串,查找是否合法。一旦不合法,将起点替换为*

赛时代码:

#include <bits/stdc++.h>
using namespace std;
int main() {
    freopen("illegality.in", "r", stdin);
    freopen("illegality.out", "w", stdout);
    int T, n;
	cin >> T;
	while (T--) {
		cin >> n;
		char s[12][20];
		char t[100010];
		for (int i = 1; i <= n; i++) cin >> s[i];
		cin >> t;
		for (int i = 0; i < strlen(t); i++) {
			for (int k = 1; k <= n; k++) {
				bool flag = 1;
				for (int j = i; j < i + strlen(s[k]); j++) {
					if (j >= strlen(t)) {
						flag = 0;
						break;
					}
					if (t[j] != s[k][j - i]) {
						flag = 0;
						break;
					}
				}
				if (flag) {
					t[i] = '*';
					break;
				}
				
			}
		}
		puts(t);
	} 
    fclose(stdin);
    fclose(stdout);
	return 0;
}

题解:

贪心策略。

每次选取不合法的字符子串中靠后的,保证它对后面的字符串影响尽可能的大。

问题四:虚假的珂朵莉树(kodori)

情况:

WA,赛后AC

题意:

在我的后园,可以看见墙外有两株树,一株是枣树,还有一株也是枣树。

一棵树,有 n n n个节点,根节点为1, 每个节点都有一个权值,设每个节点与根节点距离是这个节点的深度。

在这棵树上增加 m m m条虚假边,任意一条虚假边不会和原来的树边或其他虚假边重合(增加的虚假边不影响节点深度)。

进行 q 次操作:

  1. 操作1: 让结点 u u u的权值增加 k k k,并对与结点 u u u相邻的结点中,深度比结点 u u u小的结点重复操作1。

  2. 操作2: 让结点 u u u的权值增加 k k k,并对与结点 u u u相邻的结点中,深度比结点 u u u大的结点重复操作2。

求经过 q q q次操作之后,所有的节点的权值。(权值对 1 0 9 + 7 10^9+7 109+7取余)

赛时思路:

三个dfs:

  1. 求每个店的深度;
  2. 处理操作1;
  3. 处理操作2。

赛时代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5;
int head[N + 5], v[N + 5], nex[N + 5], e[N + 5], cnt, n;
int vis[N + 5], a[N + 5], d[N + 5];
void add(int x, int y) {
	v[++cnt] = y;	
	e[cnt] = a[y];
	nex[cnt]=head[x];	
	head[x]=cnt;	
}
void df(int x, int dep){ 
	vis[x]=1;
	for(int i=head[x];~i;i=nex[i]){
		int y=v[i];	
		if(vis[y])	continue;
		d[y] = dep;
		df(y, dep + 1);	
	}
}
int mb, k;
void dfs(int x){  
	vis[x]=1;
	if (d[x] < mb) e[x]+=k;
	for(int i=head[x];~i;i=nex[i]){
		int y=v[i];	
		if(vis[y])	continue;
		dfs(y);	
	}
}
void dfss(int x){  
	vis[x]=1;
	if (d[x] > mb) e[x]+=k;
	for(int i=head[x];~i;i=nex[i]){
		int y=v[i];	
		if(vis[y])	continue;
		dfss(y);	
	}
}
int main() {
	freopen("kodori.in", "r", stdin);
	freopen("kodori.out", "w", stdout);
	memset(head, -1, sizeof head);
	memset(vis, 0, sizeof vis);
	cnt = 0;
	int x, y, n, m, q;
	cin >> n >> m >> q;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i < n; i++) {
		cin >> x >> y;	
		add(x, y);
		add(y, x);	
	}
	for(int i = 1; i <= m; i++) {
		cin >> x >> y;	
	}
	memset(vis, 0, sizeof vis);
	df(1, 1);
	while (q--) {
		int t, u;
		cin >> t >> u >> k;
		e[u] += k;
		mb = d[u];
		memset(vis, 0, sizeof vis);
		if (t == 1) {
			dfs(u);
		} else {
			dfss(u);
		}
	}
	for (int i = 1; i <= n; i++) cout << e[i] << " ";
	fclose(stdin);
	fclose(stdout);
	return 0;
}

WA 20分方法:

通过数据范围:

另有20%的数据下: 1 ≤ n ≤ 1 0 5 , m = 0 1≤n≤10^5,m=0 1n105m=0且只有操作2 。

可知,我们可以只考虑操作2。dfs时不需要考虑深度,因为从需要加值的节点开始向下遍历,其深度一定大。

从降低复杂度考虑,可以用一个桶数组存放操作需要增加的值,对于相同的节点的操作2可以把增加值累计到一起。在dfs时用类似前缀和的方式累加,积累到原来的权值上。

WA 20分代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5;
int mod = 1e9 + 7;
long long int head[N + 5], v[N + 5], nex[N + 5], cnt, n;
long long int vis[N + 5], a[N + 5], dw[N + 5];
void add(long long int x, long long int y) {
	v[++cnt] = y;	
	nex[cnt]=head[x];	
	head[x]=cnt;	
}
void dfss(long long int x){   //遍历累加
	vis[x]=1;
	if (dw[x]) a[x] = (dw[x] + a[x]) % mod;  //如果需要累加,加上权值
	for(int i=head[x];~i;i=nex[i]){
		int y=v[i];	
		if(vis[y])	continue;
		dw[y] = (dw[x] + dw[y]) % mod;   //前缀和,把需要加的值压到下面
		dfss(y);	
	}
}
int main() {
	memset(head, -1, sizeof head);
	memset(vis, 0, sizeof vis);
	cnt = 0;
	long long int x, y, n, m, q;
	scanf("%lld%lld%lld", &n, &m, &q);
	for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
	for(int i = 1; i < n; i++) {
		scanf("%lld%lld", &x, &y);
		add(x, y);
		add(y, x);	
	}
	while (q--) {
		long long int t, u, k;
		scanf("%lld%lld%lld", &t, &u, &k);
		if (t == 1) {
		} else {
			dw[u] = (dw[u] + k) % mod;  //只考虑第二种操作
		}
	}
	memset(vis, 0, sizeof vis);
	dfss(1);
	for (int i = 1; i <= n; i++) printf("%lld ", a[i] % mod);
	return 0;
}

题解:

若是对同一个点进行多次操作1,可以将这多次操作1合成1次操作1,操作1的权值为多次操作1的权值和,而由其他结点传递过来的操作1也可以跟当前结点的操作1进行合并。操作2同理,用两个桶标记数组即可。

  • 对于操作1,可以发现,深度最大的结点不会受到对其他结点进行操作一的影响,那么深度最大的结点操作完后,深度次大的结点就不会再次受到影响。依此类推,我们可以根据结点深度从大到小进行操作1,这样只需进行1次遍历即可完成所有的操作1。
  • 对于操作2,类似于操作1,可以根据结点深度从小到大进行操作。

因此,我们只需要先bfs处理出各个结点的深度,然后将操作进行记录,
在up和down数组中记录操作1和操作2累积的权值,再根据操作按深度从小到大或从大到小进行传递,最后将up、down数组与原本的权值数组进行求和输出即可。

具体流程如下:

  1. 打表每次操作的up增加值、down增加值,并不更新其他相邻的节点。
  2. 对于操作一,从深度最大的节点开始往上更新up值。
  3. 对于操作二,从深度最小的节点开始往下更新down值。

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1e6,M=1e9+7;
int head[N],ve[N],nex[N],vis[N],cnt,tot;
int n,m,q,u,v,k,t,a[N],deep[N];
long long up[N],dw[N];
struct node {
	int dp,x;
} g[N];
int cmp(node a,node b) {
	return a.dp<b.dp;	//对深度从小到大排序
}
void add(int x,int y) {
	ve[++cnt]=y;
	nex[cnt]=head[x];
	head[x]=cnt;
}
void dfs(int x) {
	vis[x]=1;
	tot++;
	g[tot].dp=deep[x];
	g[tot].x=x;
	for(int i=head[x]; ~i; i=nex[i]) {
		int y=ve[i];
		if(vis[y])	continue ;
		deep[y]=deep[x]+1;
		dfs(y);
	}
}
int main() {
	memset(head,-1,sizeof head);
	cin>>n>>m>>q;
	for(int i=1; i<=n; i++)	cin>>a[i];
	for(int i=1; i<n; i++) {
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs(1);		//虚假边对深度没影响,提前求出深度
	while(m--) {
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	while(q--) {
		cin>>t>>u>>k;
		if(t==1)	up[u]=(up[u]+k)%M;	//上面,深度小的
		else dw[u]=(dw[u]+k)%M;	 	//下面,深度大的
	}
	sort(g+1,g+1+n,cmp);	//深度从小到大排序
	for(int i=1; i<=n; i++) {	//遍历所有点
		int x=g[i].x;
		for(int j=head[x]; ~j; j=nex[j]) {
			int y=ve[j];
			if(deep[y]>deep[x])	dw[y]=(dw[y]+dw[x])%M;  //找x所有深度比他小的点y,累加权值
		}
	}
	reverse(g+1,g+1+n);
	for(int i=1; i<=n; i++) {	//遍历所有点
		int x=g[i].x;
		for(int j=head[x]; ~j; j=nex[j]) {
			int y=ve[j];
			if(deep[y]<deep[x])	up[y]=(up[y]+up[x])%M;  //找x所有深度比他大的点y,累加权值
		}
	}
	for(int i=1; i<=n; i++)	cout<<(a[i]+up[i]+dw[i])%M<<" ";
	return 0;
}

补充题:图的遍历(相关:第四题)

题目描述:

n n n个点(编号1~ n n n), m m m条边,邻接表存储无向图。输出每个结点所连接的点。

输入格式:

两个正整数, n n n m m m,表示点数和变数。

下面的 m m m行,每行两个正整数 x x x y y y,表示 x x x y y y两点之间存在一条边。

输出格式:

n n n行,每行输出第 i i i个点的邻接点。输出格式为:

i : 邻接点(从小到大间隔一个空格输出) i:邻接点(从小到大间隔一个空格输出) i:邻接点(从小到大间隔一个空格输出)

没有邻接点的,输出zero

题解:

我们知道,头插法存储图,后添加的点一定会被先遍历到,所以我们可以对输入以 y y y为关键字从大到小排序,再依次存入。然而,在存无向图时,要把 x   y x\ y x y y   x y\ x y x都保存,然后再排序,这样可以保证在存储时对于每一个点,存入邻接点的顺序总是从大到小的。

AC代码:

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 2e5;
int head[N + 5], v[N + 5], nex[N + 5], cnt;
void add(int x, int y) {   //头插法
	v[++cnt] = y;	
	nex[cnt]=head[x];	
	head[x]=cnt;	
}
struct node{   //存有向边x→y
	int x;
	int y;
}a[N];
bool cmp(node a, node b) {   //对于终点y从大到小排序
	return a.y > b.y;
}
int main() {
	memset(head, -1, sizeof head);
	int n, m, x, y;
	cin >> n >> m;
	int no = 0;
	while (m--) {
		cin >> x >> y;
		a[++no].x = x;   //保存x→y的有向边
		a[no].y = y;
		a[++no].x = y;   //保存y→x的有向边(无向图双向连通)(同时保证排序后每个店邻接点的递增性)
		a[no].y = x;
	}
	sort (a + 1, a + no + 1, cmp);   //排序
	for (int i = 1; i <= no; i++) {
		add(a[i].x, a[i].y);    //把排序后的变依次存入邻接表
	}
	for (int j = 1; j <= n; j++) {
		cout << j << " :";
		bool flag = false;
		for(int i = head[j]; ~i; i = nex[i]) {   //遍历邻接点输出
			cout << v[i] << " ";
			flag = 1;
		}
		if (!flag) cout << "zero";  //没有邻接点,输出zero
		cout << endl;
	}
	return 0;
}

总结

这一次的做题策略有一定的提升,但是要注意时间的比重。

尽快解决前两道题,多一点时间思考后面两道题的思考方向和获得更多分数的方法。

前面两道题尽量少调试,在后面的题目的调试上多下功夫。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值