(KDY)CSP-J模拟赛五补题报告
日期:2023年10月4日
文章目录
一、AC情况
第一题 | 第二题 | 第三题 | 第四题 |
---|---|---|---|
AC 100分 | AC 100分 | AC 100分 | WA 0分(赛后AC) |
总计300分。
二、赛中概况
第一题很快写完;
第二题相邻一下找到了规律,也很简单,写完;
第三题思考用了大部分时间,算了很长时间,调试了很久,写完;
最后一天没有时间考虑了,于是骗分。
三、解题报告
问题一:重复判断(repeat)
情况:
AC
题意:
判断字符串a是否是字符串b重复若干次得到的。
赛时思路&题解:
两个指针同时遍历,指针 i i i遍历字符串a,同时指针 l l l遍历字符串b,如果字符串b遍历了一遍,就调到开头继续比较,知道把a遍历一遍为止。如果 a i ! = b l a_i!=b_l ai!=bl,则不是重复字符串;如果遍历结束 l l l没有,即a中出了b仍有多余字符,也不是重复字符串。
AC代码&AC代码:
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("repeat.in", "r", stdin);
freopen("repeat.out", "w", stdout);
int T;
cin >> T;
while (T--) {
string s, t;
cin >> s >> t;
int flag = 1, l = 0;
for (int i = 0; i < s.size(); i++) { //指针i,遍历字符串a
if (s[i] != t[l]) { //比较出不同,不符合题意
flag = 0;
break;
}
l++; //同时指针l遍历字符串b
l %= t.size(); //l保证在[l,size_b)中重复遍历
}
if (flag && l == 0) { //没有多余元素
printf("YES\n");
} else {
printf("NO\n");
}
}
fclose(stdin);
fclose(stdout);
return 0;
}
问题二:歪果仁学乘法(multiplication)
情况:
AC
题意:
现有一种不需要九九乘法表也可以计算乘法的方式,对于a × b:
- 将a,b的每一位上的数码画成线,不同位之间分隔开。
- a 和 b 的方向垂直画出。
- 数出每个方向上交点的个数,即是 c 对应位置上的数码。
样例图给出了计算12 ×13的方法:
- 红色线分别画出 1 条和 2 条;
- 蓝色线分别画出 1 条和 3 条;
- 数出红、蓝色线的交点个数,依次为 1,5,6 个;
- 得到答案:12 × 13 = 156。
给出两个数字 a, b,求它们的乘积时交点的总个数是多少。
赛时思路&题解:
观察一下,我们发现,设 a = x y ‾ a=\overline{xy} a=xy, b = p q ‾ b=\overline{pq} b=pq,那么它一共有四个交点(如果有个位数情况,将十位看做 0 0 0来考虑),每个交点各有节点个数: x p xp xp, x q xq xq, y p yp yp, y q yq yq。所以节点总个数为:
a n s = x p + x q + y p + y q ans=xp+xq+yp+yq ans=xp+xq+yp+yq
我们可以对a,b进行数位分离,每位上的数字两两相乘求和。
赛时代码&AC代码:
#include <bits/stdc++.h>
using namespace std;
int A[100], B[100];
typedef long long ll;
int main() {
freopen("multiplication.in", "r", stdin);
freopen("multiplication.out", "w", stdout);
int a, b;
scanf("%d%d", &a, &b);
int la = 0, lb = 0;
while (a) { //对a数位分离
A[++la] = a % 10;
a /= 10;
}
while (b) { //对b数位分离
B[++lb] = b % 10;
b /= 10;
}
ll ans = 0;
for (int i = 1; i <= la; i++) {
for (int j = 1; j <= lb; j++) { //两两相乘求和
ans += A[i] * B[j];
}
}
printf("%lld", ans);
fclose(stdin);
fclose(stdout);
return 0;
}
问题三:去重求和(summation)
情况:
AC
题意:
定义 s u m ( l , r ) sum(l,r) sum(l,r)为 a l a_l al~ a r a_r ar之间的式子去重后的和。
求 ∑ l = 1 n ∑ r = l n s u m ( l , r ) \sum\limits^n_{l=1}\sum\limits^n_{r=l}sum(l,r) l=1∑nr=l∑nsum(l,r)。(结果对 1 0 9 + 7 10^9+7 109+7取余)
赛时思路&题解:
举例a[]={1, 1, 3, 1, 5}
。
作为l,r时的sum(l,r) | a[1] | a[2] | a[3] | a[4] | a[5] |
---|---|---|---|---|---|
a[1] | 1 | 1 | 4 | 4 | 9 |
a[2] | 1 | 4 | 4 | 9 | |
a[3] | 3 | 4 | 9 | ||
a[4] | 1 | 6 | |||
a[5] | 5 |
拆分之后:
作为l,r时的sum(l,r) | a[1] | a[2] | a[3] | a[4] | a[5] |
---|---|---|---|---|---|
a[1] | 1 | 1 | 1+3 | 1+3 | 1+3+5 |
a[2] | 1 | 1+3 | 1+3 | 1+3+5 | |
a[3] | 3 | 1+3 | 1+3+5 | ||
a[4] | 1 | 1+5 | |||
a[5] | 5 |
整理一下每次出现的次数:
作为l时所有sum出现的次数 | a[1] | a[2] | a[3] | a[4] | a[5] | 总和 |
---|---|---|---|---|---|---|
a[1] | 5 | 0 | 3 | 0 | 1 | 1×5+1×0+3×3+1×0+5×1 |
a[2] | 4 | 3 | 0 | 1 | 1×4+3×3+1×0+5×1 | |
a[3] | 3 | 2 | 1 | 3×3+1×2+5×1 | ||
a[4] | 2 | 1 | 1×2+5×1 | |||
a[5] | 1 | 5×1 |
现在我们可以发现规律:
- 如果第 i i i个元素前面(以左端点 l l l为起点)没有重复的,则它在当前着一个 l l l为左端点的所有 s u m sum sum中出现次数为 n − i + 1 n-i+1 n−i+1;
- 如果第 i i i个元素前面(以左端点 l l l为起点)有重复的,则它在当前着一个 l l l为左端点的所有 s u m sum sum中出现次数为 0 0 0;
- 第 i i i个元素在所有的 s u m sum sum中最多出现了 i i i次;
- 如果第 i i i个元素前面相同的元素的编号为 x x x(无相同, x = 0 x=0 x=0),那么第 i i i个元素出现次数不为 0 0 0的个数为 i − x i-x i−x(因为在 ( x , i ] (x,i] (x,i]的 l l l作为左端点的 s u m sum sum中都不受前面相同的元素影响且一定包含第 i i i个元素)。
整理规律:第 i i i个元素的贡献值为:
a n s + = ( n − i + 1 ) × a [ i ] × ( i − x ) ans+=(n-i+1)×a[i]×(i-x) ans+=(n−i+1)×a[i]×(i−x)
注意取余 m o d mod mod。
赛时代码&AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
const int mod = 1e9 + 7;
typedef long long ll;
ll a[N], n;
ll ans = 0;
map<ll, ll> p;
int main() {
freopen("summation.in", "r", stdin);
freopen("summation.out", "w", stdout);
scanf("%lld", &n);
for (ll i = 1; i <= n; i++) scanf("%lld", &a[i]);
for (ll i = 1; i <= n; i++) {
ll x = p[a[i]];
ans = (ans + (n - i + 1) % mod * a[i] % mod * (i - x) % mod) % mod; //公式
p[a[i]] = i;
}
printf("%lld", ans % mod);
fclose(stdin);
fclose(stdout);
return 0;
}
问题四:点集操作(point)
情况:
WA,赛后AC
题意:
一个有向无环图,对这个图做任意次 m o d i f y ( i , j ) modify(i,j) modify(i,j)操作之后的图中剩余的最小点数,其中 1 ≤ i , j ≤ n 1≤i,j≤n 1≤i,j≤n,其中 m o d i f y ( i , j ) modify(i,j) modify(i,j)为一次操作:
- 任选不同的两个点;
- 称 A i A_i Ai为 i i i能到达的所有点组成的点集, A j A_j Aj为 j j j能到达的所有点组成的点集 (每个点可以到达的点集包含这个点本身);
- 设 B B B为一个最大的点集,满足 B 既是 A i A_i Ai的子集,又是 A j A_j Aj的子集 。
- 将 B B B在图中变成一个新点, B B B内的所有边全部删除。点集 B B B以外的点与点集 B B B以内的点的连边关系转移到新点上。
赛时思路:
骗分。
题解:
一般的点有如下几种可能:
- 入度为0:这种点不可能会被合并;
- 出度为0:这种点可能被合并;
- 出度、入度都为0:这种点一定会被合并。
用数组存储每个点的入度、出度。
a n s = n ans = n ans=n。(初始认为所有的点都不需要合并)
把每个点都遍历一遍。当发现第3种点(入度、出度都大于0),那么把它的能到达的点都要合并到这个点中(因为它的儿子节点一定是第2或第3种点);如果这个点被标记过,那么它的儿子节点一定也要被合并。将没有标记过的点标记并 a n s − − ans-- ans−−。
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6;
typedef long long ll;
ll head[N + 5], v[N + 5], nex[N + 5], cnt;
ll du[N + 5][5]; //用于记录入度[1]和出度[2]
bool vis[N + 5]; //用于标记是否合并
void add(int x, int y) { //头插法
v[++cnt] = y;
nex[cnt]=head[x];
head[x]=cnt;
}
int main() {
memset(head, -1, sizeof head);
memset(vis, 0, sizeof vis);
ll n, m, x, y;
cin >> n >> m;
for(ll i = 1; i <= m; i++) {
cin >> x >> y;
add(x, y); //存边
du[y][1]++; //入度++
du[x][2]++; //出度++
}
ll ans = n;
for (ll j = 1; j <= n; j++) { //遍历每一个点
if (vis[j] || (du[j][1] > 0 && du[j][2] > 0)) { //如果这个点被标记或者是第三种情况的点,那么它的儿子节点都需要标记合并
for(ll i = head[j]; ~i; i = nex[i]) { //遍历儿子节点
if (!vis[v[i]]) { //没有标记过
ans--; //ans--,表示这个点合并到父亲节点那里,减少了一个点
}
vis[v[i]] = true; //标记为合并
}
}
}
cout << ans;
return 0;
}
总结
还是要注意时间比重的调整。把更多的时间用在第三、第四道题上,尽可能地为后面的题目那数据范围里的10%~50%的点的方法留时间。
注意多举例子。比如第三题,多举几个较长的,有概括性的,能够代表绝大多数情况的样例分析,可以节省很多的调试时间。