(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
题意:
-
复读定义:给定一个长度为 n n n的仅包含小写字母和数字的字符串,字母表示需要复读的消息,数字表示要复读的次数。
例如: kdy3 \texttt{kdy3} kdy3表示将 kdy \texttt{kdy} kdy复读3遍,输出为: kdykdykdy \texttt{kdykdykdy} kdykdykdy; -
更复杂的复读模拟:这个字符串中可能包含多个数字,当多次出现数字时,例如 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| ∣w−v∣的值最小,请问 ∣ w − v ∣ |w-v| ∣w−v∣最小为多少。
赛时思路:
前缀和。
两个前缀和数组,第一个存矛的战斗力,第二个存盾的战斗力。遍历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: 让结点 u u u的权值增加 k k k,并对与结点 u u u相邻的结点中,深度比结点 u u u小的结点重复操作1。
-
操作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。
赛时代码:
#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 1≤n≤105,m=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数组与原本的权值数组进行求和输出即可。
具体流程如下:
- 打表每次操作的up增加值、down增加值,并不更新其他相邻的节点。
- 对于操作一,从深度最大的节点开始往上更新up值。
- 对于操作二,从深度最小的节点开始往下更新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;
}
总结
这一次的做题策略有一定的提升,但是要注意时间的比重。
尽快解决前两道题,多一点时间思考后面两道题的思考方向和获得更多分数的方法。
前面两道题尽量少调试,在后面的题目的调试上多下功夫。