2022年校区新生程序设计竞赛初赛
以下内容为个人理解,如有错误,敬请谅解!
此次比赛共5题,ACM赛制,题目难度有梯度。
个人认为总体难度排序为:A < B < D < C < E
A题:homtrue
考察知识:1.大小写转化; 2.二进制理解;
解析:一个字符串 “homtruehomtruehomtrue” 固定,一个变量k∈[0,127]k \in [0, 127]k∈[0,127](即二进制为7位的数),k的二进制位数上为1则字符串相应位数上的字母为大写,否则为小写。注意输入还可能为114514,此时要输出yc。
总结:非常友好的题目,显示出了出题人的热情。
代码附上:
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
string str = "homtrue";
int n; cin >> n;
if (n == 114514) {printf("yc\n"); return 0;}
for (int i = 6; i >= 0; i--, n >>= 1)
if (n % 2) str[i] = 'A' + str[i] - 'a';
cout << str << endl;
return 0;
}
B题:烦人的图书 O(T∗n)O(T*n)O(T∗n)
考察知识:
1.逆向思维
2.相对的思想
题目解析:
1.多组数据测试T∈[1,100]T \in [1, 100]T∈[1,100]。
2.一个系列的书有 nnn 种,这 nnn 种书都被借阅过,但是次数不一样 Ai∈[0,231−1]A_i \in[0, 2^{31} - 1]Ai∈[0,231−1]。
3.达利园同学(应该患有某种强迫症)想要让这 nnn 种书的借阅次数是一样的,所以他要刷借阅次数。
4.但是他好像不太聪明的亚子,每次只拿 n−1n - 1n−1 种书,一种也不多一种也不少。
5.所以我们要计算他最少要来回借多少趟书。(注意取mod)
解答:
1.一开始我们比较容易正向思考——选要拿的书,去凑,使得所有种类的书的借阅次数一样。然而我们会发现这种想法并不够好,因为我们拿的数的数量是固定的 n−1n - 1n−1 本,凑的时候会使得我们想要的目标借阅数量发生变换。
2.所以我们要学会 正难则反正难则反正难则反 的思维。既然是借 n−1n - 1n−1 本书,我们为什么不能讨论不借的那 111 本书是哪一本呢?再运用相对的思想:你们都在疯狂卷,而我在正常学习,那就相对于我在摆烂。
3,我们通过一个样例来理解一下。对于:1,2,3,51, 2, 3, 51,2,3,5 这个样例,我们先列出它们之间相对的借阅次数:0,1,2,40, 1, 2, 40,1,2,4,再进行借书操作:
1. 4号书不借,1,2,3号数借,则它们的相对借阅次数变为:0,1,2,30, 1, 2, 30,1,2,3;
2. 4号书不借,1,2,3号数借,则他们的相对借阅次数变为:0,1,2,20, 1, 2, 20,1,2,2;
3. 4号书不借,1,2,3号数借,则他们的相对借阅次数变为:0,1,2,10, 1, 2, 10,1,2,1;
4. 4号书不借,1,2,3号数借,则他们的相对借阅次数变为:0,1,2,00, 1, 2, 00,1,2,0;
5. 3号书不借,1,2,4号数借,则他们的相对借阅次数变为:0,1,1,00, 1, 1, 00,1,1,0;
6. 3号书不借,1,2,4号数借,则他们的相对借阅次数变为:0,1,0,00, 1, 0, 00,1,0,0;
7. 2号书不借,1,3,4号数借,则他们的相对借阅次数变为:0,0,0,00, 0, 0, 00,0,0,0;
当相对次数都为0时,不就表示他们的借阅次数一样了嘛!
总的借阅次数为 0+1+2+4=70 + 1 + 2 + 4 = 70+1+2+4=7, 即我们只要输出所有书的相对数量之和就行了。
#include <iostream>
#include <cstdio>
const int N = 1e7 + 10;
const int mod = 1e9 + 7;
using namespace std;
long long a[N];
int main() {
int t, n;
cin >> t;
while (t--) {
cin >> n;
long long minn = 9999999999;
for (int i = 0; i < n; i++) {
scanf("%lld", &a[i]);
minn = min(a[i], minn);
}
long long ans = 0;
for (int i = 0; i < n; i++) {
ans += a[i] - minn;
ans %= mod;
}
cout << ans << endl;
}
return 0;
}
C题:Sakura教天依玩厄斐琉斯 O(n!)O(n!)O(n!)
考察知识:
1.搜索
2.数据结构的运用
题目解析:
1.目标字符串 “GPBRWGPBRWGPBRW”
2.初始字符串 str,由 GGG、PPP、BBB、RRR、WWW组成
3.变换规则:
(1)将第一个字符和第二个字符交换
(2)将第一个字符放到最后
4.(1)变换的次数不计,求最少的(2)变换的次数使得初始字符串变成目标字符串
解答:
1.首先会有贪心的想法,每次执行(2)的时候确保字符的顺序是正确的,即如:BBB 要比 PPP 后放,如BPGRWBPGRWBPGRW的时候,让BBB和PPP先交换一下,变成PBGRWPBGRWPBGRW,PPP 在 BBB 前面,再执行(2)操作。然而这种贪心思想并不能由局部推到整体,题目中的样例二就是一个反例,所以只能放弃这种想法。
2.暴力搜索:由于只有5个字母,明显地,就算时全排列的暴力搜索在时间上也是绰绰有余的。难点就在于存储和判重的问题。
3.我们可以用queuequeuequeue队列来存储字符串的状态,用 unordered_map来进行判重。
4.考虑到有同学没接触过,我们先简单地解释一下:
q.push(x)q.push(x)q.push(x) 表示将 xxx 放入队列队尾;
q.front()q.front()q.front() 表示队列队首元素;
q.pop()q.pop()q.pop() 表示删除队列队首元素;
d.count(t)d.count(t)d.count(t)查找ttt元素是否存在,用来判重
d[t]d[t]d[t] 表示由字符串指向整数,即该字符串的已被执行的次数。
这道题能解决之后可以尝试着做一做八数码八数码八数码问题,一样的数据结构就能解决。
代码附上:
#include <iostream>
#include <cstdio>
#include <unordered_map>
#include <queue>
using namespace std;
int bfs(string start) {
string end = "GPBRW"; // 目标字符串
queue<string> q; // 状态队列
unordered_map<string, int> d; // 判重及存储该字符串的执行次数
q.push(start);
d[start] = 0;
while (q.size()) {
string t = q.front();
q.pop();
int dist = d[t];
if (t == end) return dist;
swap(t[0], t[1]);
if (!d.count(t)) {
d[t] = dist;
q.push(t);
}
swap(t[0], t[1]);
char tmp = t[0];
for (int i = 0; i < 4; i++) t[i] = t[i+1];
t[4] = tmp;
if (!d.count(t)) {
d[t] = dist + 1;
q.push(t);
}
}
return -1;
}
int main() {
string start;
cin >> start;
cout << bfs(start) << endl;
return 0;
}
D题:痛苦减法O(T)O(T)O(T)
考察知识:
1.代码实现能力
2.思维能力
题目解析
1.注意多组数据测试 T∈[1,5∗105]T\in[1, 5 *10^5]T∈[1,5∗105]
2.给定4个数 n,a,b,kn, a, b, kn,a,b,k, 0≤b≤n≤10160 \leq b \leq n \leq 10^{16}0≤b≤n≤1016,1≤a≤91\leq a \leq 91≤a≤9 ,0≤k≤100\leq k \leq 100≤k≤10
3.对 nnn 进行减法运算,每次减 aaa,每次运算个位要借位的运算消耗 kkk 单位时间,个位不用借位的运算消耗 111 单位时间,直到 n≤bn\leq bn≤b
4.求总的运算时间。
解答
我们可以发现 nnn 这个数很大,很明显不能暴力地一次一次减。
所以我们要利用好循环节来进行计算。因为每一次减 aaa ,nnn 的个位数都会发生变化,我们只要找到循环节的大小和消耗的时间就能用除法进行快速求解啦。
代码附上:
#include <iostream>
#include <cstdio>
using namespace std;
bool st[15]; // 用来判断循环
int main() {
long long t, n, a, b, k;
cin >> t;
while (t--) {
scanf("%lld %lld %lld %lld", &n, &a, &b, &k);
long long sum = 0; // 一次循环的时间消耗
long long all = 0; // 一次循环的大小
long long ans = 0; // 总的时间消耗
for (int i = 0; i < 10; i++) st[i] = 0; // 每次要初始化
long long tmp = n % 10; // n刚才始的个位数是多少
while (!st[tmp]) { // 没出现的个位数
st[tmp] = 1;// 出现了就进行标记
if (tmp >= a) {tmp -= a; sum++;} // 不用借位的
else {tmp += 10 - a; sum += k;} // 需要借位的
all += a;
}
ans += (n - b) / all * sum; // 求出有多少个循环节,再乘上每个循环节的时间消耗
n = b + (n - b) % all; // 可能会有比循环节短一点的部分,分开计算
while (n > b) {
tmp = n % 10;
if (tmp >= a) ans++;
else ans += k;
n -= a;
}
printf("%lld\n", ans);
}
return 0;
}
E题:天依的飞行棋 O(m∗n)O(m*n)O(m∗n)
考察知识:
1.数学中逆元的概念及求解
2.简单的动态规划
题目解析:
非常经典的概率问题,程序设计中的概率问题与我们生活中的不同,一般都会有取模和求逆元的运算。
1.投 nnn 次骰子(touzi),求总点数为 mmm 的概率是多少?
解答:
1.对于 111 次投掷,点数和概率都很明显,1,2,3,4,5,6点等概率都是 16\cfrac{1}{6}61,两次以上的投掷的话要讨论。
2.这里我们尝试讨论一下:
(1)我要求投 333 次, 总点数为 333 的情况, 很明显, 三次都必须为1,所以概率就为16∗16∗16=1216\cfrac{1}{6}*\cfrac{1}{6}*\cfrac{1}{6} = \cfrac{1}{216}61∗61∗61=2161
(2)我要求投 333 次, 总点数为 444 的情况,这里我们尝试递推一下。
若要达成3投4分,则第一次投掷必须是1或2才行,概率分别为16\cfrac{1}{6}61和16\cfrac{1}{6}61;
第二次,要看第一次的情况,如果第一次为1,则第二次可以为1或2,如果第一次为2,则第二次只能为1,概率分别为16∗16\cfrac{1}{6}*\cfrac{1}{6}61∗61或16∗16\cfrac{1}{6}*\cfrac{1}{6}61∗61,和16∗16\cfrac{1}{6}*\cfrac{1}{6}61∗61;
第三次,只需看前一次的情况,我们无需再看第一次从情况,因为我们只需要知道之前出现总数为2和总数为3的概率即可,所以我们能从第二次推到第三次的概率,为:16∗16∗16+16∗16∗16+16∗16∗16\cfrac{1}{6}*\cfrac{1}{6}*\cfrac{1}{6} + \cfrac{1}{6}*\cfrac{1}{6}*\cfrac{1}{6} + \cfrac{1}{6}*\cfrac{1}{6}*\cfrac{1}{6}61∗61∗61+61∗61∗61+61∗61∗61
3.至此我们已经知道怎么递推了,但是分数怎么进行加减计算呢,怎么存储呢?
经过上面的推理,其实我们能够发现,分母是有规律的,为 6n6^n6n,分母能求,我们只需将分子相加就行了。
4.经过递推我们能得到最终的概率的分子和分母,方便表达这里记为 X, Y;
我们只需求出 XYmodP\cfrac{X}{Y} mod PYXmodP就行了。这里我们就要引出逆元的知识了,
逆元知识的讲解
逆元: a∗bmodp=amodp∗bmodpa * b mod p = a mod p * b mod pa∗bmodp=amodp∗bmodp ,但是ab\cfrac{a}{b}ba modmodmod ppp ≠\neq= amodpbmodp\cfrac{amodp}{bmodp}bmodpamodp ,
对于大整数aaa, bbb, 如a=21000a = 2^{1000}a=21000,由于计算机存储的数值大小的限制,当出现除法的时候就无法处理,所以我们希望找到一个数x,使得 ab\cfrac{a}{b}ba modmodmod ppp === a∗xa * xa∗x modmodmod ppp。其中 xxx 就被称作 bbb 的逆元。
看到这其实你就已经可以解决问题了,因为出题人很贴心地附上了求逆元的代码,以及 666 的逆元,我们完全可以直接使用。因为我们可以将Y拆成6的一个个逆元相乘。
如果有兴趣的话,这里还有求逆元最常用的方法有两种:1.快速幂求逆元;2.扩展欧几里得算法求逆元。首先是快速幂求逆元。
推导:
ab\cfrac{a}{b}ba ≡\equiv≡ a∗xa * xa∗x (mod(mod(mod p)p)p)
1b\cfrac{1}{b}b1 ≡\equiv≡ xxx (mod(mod(mod p)p)p)
b∗xb * xb∗x ≡\equiv≡ 111 (mod(mod(mod p)p)p)
由于 ppp 为质数, 所以有 bp−1≡1b^{p - 1} \equiv 1bp−1≡1 (mod(mod(mod p)p)p),(这里是费马定理费马定理费马定理)
所以, x=bp−2x = b^{p-2}x=bp−2
费马定理是欧拉定理欧拉定理欧拉定理的特殊情况,如果有兴趣的可以接着看补充,暂时接受不了的可以跳过。
欧拉定理
若 aaa 与 nnn 互质,则有:aϕ(n)≡1a^{\phi(n)} \equiv 1aϕ(n)≡1 (mod(mod(mod n)n)n)
证明:
假设: 1−n1 - n1−n 中, 与 nnn 互质的数有: a1,a2,a3,...,aϕ(n)a_1, a_2, a_3,..., a_{\phi(n)}a1,a2,a3,...,aϕ(n)。①
由于: gcd(a,n)==1gcd(a, n) == 1gcd(a,n)==1
所以: a∗a1,a∗a2,a∗a3,...,a∗aϕ(n)a * a_1, a * a_2, a * a_3,..., a * a_{\phi(n)}a∗a1,a∗a2,a∗a3,...,a∗aϕ(n) 也与 nnn 互质。②
由于 1−n1 - n1−n 中, 与 nnn 互质的数有只有 ϕ(n)\phi(n)ϕ(n) 个,所以 ①,②两组数在 modmodmod nnn 的情况下是同一组数。
所以有:a1∗a2∗a3∗...∗aϕ(n)≡aϕ(n)∗(a1∗a2∗a3∗...∗aϕ(n))a_1 * a_2 * a_3 *...* a_{\phi(n)} \equiv a^{\phi(n)} * (a_1 * a_2 * a_3 *...* a_{\phi(n)})a1∗a2∗a3∗...∗aϕ(n)≡aϕ(n)∗(a1∗a2∗a3∗...∗aϕ(n)) (mod(mod(mod n)n)n)
两边消去 a1∗a2∗a3∗...∗aϕ(n)a_1 * a_2 * a_3 *...* a_{\phi(n)}a1∗a2∗a3∗...∗aϕ(n),
得:aϕ(n)≡1a_{\phi(n)} \equiv 1aϕ(n)≡1 (mod(mod(mod n)n)n)
证毕。
因为当 nnn 为质数时 ϕ(n)=n−1\phi(n) = n - 1ϕ(n)=n−1 ,所以得:
费马定理:an−1≡1a^{n - 1} \equiv 1an−1≡1 (mod(mod(mod n)n)n)
快速幂求逆元代码
long long quick_mi(long long a, long long p) {
long long ans = 1, tmp = a, res = p - 2;
while (res) {
if (res & 1) ans = ans * tmp % p;
tmp = tmp * tmp % p;
res >>= 1;
}
return ans;
}
欧几里得算法求逆元代码:x即为逆元,但可能为负数
void exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1, y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= a / b * x;
}
欧几里得算法求逆元涉及递归和等式变换,有一定的难度,这里暂不做推理,有兴趣的同学可以自行学习
附上代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e3 + 10;
const int mod = 998244353;
long long dp[N][N*6]; // 投掷i次,总数为j的概率,记住这里我们只记录分子
long long qmi(int a, int p) {
long long ans = 1, tmp = a, res = p - 2;
while (res) {
if (res & 1) ans = ans * tmp % p;
tmp = tmp * tmp % p;
res >>= 1;
}
return ans;
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= 6; i++)
dp[1][i] = 1; // 初始化:第一次投掷,为 i 点的概率。我们只记分子
for (int i = 2; i <= n; i++) //第i次投掷
for (int j = i; j <= m; j++) // 总点数为j
for (int k = 1; k <= 6; k++) { // 第i次投掷的点数
if (j - k <= 0) break; // 点数不会为0哦,也可以换成j - k <= i - 1,因为前i-1次投掷最少为i-1点
dp[i][j] = (dp[i][j] + dp[i-1][j-k]) % mod;//从i-1次的情况转移而来
}
for (int i = 0; i < n; i++) //处理分母,投n次要乘n次
dp[n][m] = dp[n][m] * qmi(6, mod) % mod; // 这里的qmi(6, mod)的结果就是样例一中的166374059
cout << dp[n][m] << endl;
return 0;
}
如有错误,欢迎提出!
本文解析了2022年广东工业大学揭阳校区新生程序设计竞赛初赛的五道题目,介绍了每道题目的核心知识点和解题思路,包括大小写转换、逆向思维、搜索算法、代码实现能力和动态规划等。
4457





