2025HAUE新生周赛二C++题解
前言
本场题目相较第一场难度略有提升,为了让大部分人有良好的体验,我对题面进行了精心的加工(在一个月里进行了多次修改)。
不知道大家有没有感觉理解题意很轻松?小Q还是太善良了!
本场涉及算法:排序,前缀和,动态规划。
除F I两题较为简单,其他题解均使用了DeepSeek添加注释,方便大家理解。
希望大家都能够在本场比赛中有所收获~
A-E题设定来源:Steins;Gate;
G,H题设定来源:Rick and Morty;
都是相当有趣的作品,感兴趣的同学可以看一看~
难度预期
简单 F I A D
中等 E B H G
难 C J
题解顺序依照难度预期。
F 小Q的算法秘典
逐行复制粘贴输出即可~
#include <iostream>
using namespace std;
int main() {
cout<<"+===========+"<<endl;
cout<<"|###########|"<<endl;
cout<<"|####+==+##|"<<endl;
cout<<"|####|算| #|"<<endl;
cout<<"|####|法| #|"<<endl;
cout<<"|####|秘| #|"<<endl;
cout<<"|####|典| #|"<<endl;
cout<<"|####+==+##|"<<endl;
cout<<"|###########|"<<endl;
cout<<"|###########|"<<endl;
cout<<"+===========+"<<endl;
}
这里再教大家一种特别的输出方法。
C++11引入的----原始字符串字面量(raw string literals)。这是一种增强型的字符串表示方式,用于简化处理复杂的字符串。
它与普通字符串字面量的主要区别在于,原始字符串字面量可以包含反斜杠(\)和引号(“)等特殊字符,而无需对他们进行转义。
格式为 R"(任意字符串)"。
例如:
#include<bits/stdc++.h>
using namespace std;
int main()
{
string s=R"(printf("hello world!"))";
cout<<s<<endl;
cout<<R"(printf("hello world!"))";
}
输出
printf("hello world!")
printf("hello world!")
本题使用这个技巧,可以让你快人一步~
#include<bits/stdc++.h>
using namespace std;
int main()
{
cout<<R"(+===========+
|###########|
|####+==+##|
|####|算| #|
|####|法| #|
|####|秘| #|
|####|典| #|
|####+==+##|
|###########|
|###########|
+===========+)";
}
I 小Q的乒乓球对决 I
注意表中为小Q胜率,而需要输出的是Z老师的胜率。
善良的小Q特意在题面中对此进行了加粗处理,仍然因此WA的同学以后要注意好好读题哦~
#include<bits/stdc++.h>
using namespace std;
int main()
{
int a,b;
cin>>a>>b;
if(a==1)
{
if(b==1)
cout<<0.7;
else
cout<<0.5;
}
else
{
if(b)
cout<<0.9;
else
cout<<0.8;
}
}
A 小Q的心理相对论
版本一
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n; // 读取活动数量
double ans = 0; // 记录实际感受总时长
double sum = 0; // 记录已记录活动的真实时长总和
for(int i = 1; i <= n; i++) {
double t, k;
cin >> t >> k; // 读取活动的真实时长t和心情系数k
// 计算该活动的实际感受时长并累加
ans += t * k;
// 累加已记录活动的真实时长
sum += t;
}
// 加上未记录时段的感受时长(心情系数为1,所以直接加时长)
ans += (24 - sum);
// 输出结果,保留2位小数
cout << fixed << setprecision(2) << ans;
return 0;
}
版本二
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n; // 读取活动数量
// 初始化总时长为24小时(假设所有时段心情系数都为1)
double ans = 24;
for(int i = 1; i <= n; i++) {
double t, k;
cin >> t >> k; // 读取活动的真实时长t和心情系数k
// 从总时长中减去该活动的原始时长(心情系数为1的情况)
ans -= t;
// 加上该活动的实际感受时长(考虑心情系数)
ans += t * k;
}
// 输出结果,保留2位小数
cout << fixed << setprecision(2) << ans;
return 0;
}
D 小Q的时间跳跃机器
int类型的最大值为231-1,即2147483647。int类型的运算结果大于该值将导致数值溢出,结果错误。
所以要使用数据范围更大的long long进行运算。
long long类型的最大值263-1,即922337203685477。
善良的小Q已经在样例输出中给出了结果的最大值,仍然因此WA的同学要牢记此知识点~
记住:在竞赛中,当看到数据范围或结果可能较大时,优先使用 long long 可以避免很多不必要的错误!
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n; // 读取时间跳跃次数n
long long ans = 0; // 使用long long防止整数溢出
// 循环计算总疲劳值:1² + 2² + 3² + ... + n²
for(int i = 1; i <= n; i++) {
ans += i * i; // 第i次跳跃产生i²的疲劳值
}
cout << ans; // 输出总疲劳值
return 0;
}
数学公式
更高效,不过在数据范围允许暴力求解时不需要浪费时间推导数学公式。
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
long long ans = (long long)n * (n + 1) * (2 * n + 1) / 6;
cout << ans;
return 0;
}
E 小Q的时间机器
简单模拟。
#include<bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m; // 读取初始燃料量n和旅行次数m
int x = 2025; // 初始年份为2025年
int cnt = 0; // 记录总共需要消耗的燃料量
// 遍历每次旅行
for(int i = 1; i <= m; i++) {
int y;
cin >> y; // 读取第i次旅行的目的地年份
// 计算跨越年份所需的燃料:|目的地年份 - 出发年份|
cnt += abs(y - x);
// 更新当前位置为本次旅行的目的地
x = y;
}
// 加上每次启动机器消耗的燃料:m次旅行 × 1单位/次
cnt += m;
// 判断燃料是否足够
if(cnt > n)
cout << "Need more!"; // 燃料不足
else
cout << "Yes sir!"; // 燃料足够
return 0;
}
B 小Q的世界线变动率
整数位数分离。
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n; // 读取数据条数
int ans = 0; // 记录干扰数据的数量
for(int i = 1; i <= n; i++) {
int a, b; // a: 整数部分, b: 小数部分
char op; // 读取小数点 '.'
// 格式化输入:整数部分 + 小数点 + 小数部分
cin >> a >> op >> b;
int d[6]; // 数组存储小数部分的6位数字
// 将小数部分b的6位数字分解到数组d中
// 从个位开始取,所以需要倒序存储
for(int j = 5; j >= 0; j--) {
d[j] = b % 10; // 取最后一位数字
b /= 10; // 去掉最后一位
}
int o = 1; // 标记是否为有效数据,1表示有效,0表示干扰
// 检查小数部分的数字序列是否非递减
for(int j = 0; j < 5; j++) {
if(d[j] > d[j + 1]) { // 如果前一位大于后一位
o = 0; // 标记为干扰数据
break; // 提前结束检查
}
}
if(o == 0) // 如果是干扰数据
ans++; // 计数器加1
}
// 输出结果
if(ans == 0)
cout << "El Psy Congroo!"; // 所有数据都有效
else
cout << ans; // 输出干扰数据数量
return 0;
}
使用字符串会简单许多~
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n; // 读取数据条数
int ans = 0; // 记录干扰数据的数量
while(n--) { // 循环处理每条数据
string s;
cin >> s; // 以字符串形式读取整个浮点数
int o = 1; // 标记是否为有效数据,1表示有效,0表示干扰
// 检查小数部分的数字序列是否非递减
// 字符串格式:X.abcdef,小数部分从索引3开始
for(int i = 3; i < s.size(); i++) {
if(s[i] < s[i - 1]) { // 如果当前数字小于前一个数字
o = 0; // 标记为干扰数据
break; // 提前结束检查
}
}
if(o == 0) // 如果是干扰数据
ans++; // 计数器加1
}
// 输出结果
if(ans == 0)
cout << "El Psy Congroo!"; // 所有数据都有效
else
cout << ans; // 输出干扰数据数量
return 0;
}
H 小Q的超级煤采集
升序排序后取后k个即可。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
int a[100010]; // 定义数组存储超级煤储量
// 读取每个宇宙的超级煤储量
for(int i = 0; i < n; i++)
cin >> a[i];
// 将数组从小到大排序
sort(a, a + n);
// 计算最多可以采集的宇宙数量k = ⌈n/2⌉
int k = ceil(n * 1.0 / 2);
int ans = 0;
// 选择最大的k个值(排序后数组末尾的k个元素)
for(int i = n - k; i < n; i++)
ans += a[i];
cout << ans;
return 0;
}
也可降序排序后取前k个。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n; // 读取平行宇宙的数量
int a[100010]; // 数组存储每个宇宙的超级煤储量
// 读取每个宇宙的超级煤储量
for(int i = 0; i < n; i++)
cin >> a[i];
// 将超级煤储量按从大到小排序
// greater<int>() 表示降序排列
sort(a, a + n, greater<int>());
// 计算最多可以停留采集的宇宙数量
// k = ⌈n/2⌉ (n/2的上取整)
int k = ceil(n * 1.0 / 2);
int ans = 0; // 记录最大采集量
// 选择前k个最大的超级煤储量相加
// 因为数组已按从大到小排序,前k个就是最大的k个值
for(int i = 0; i < k; i++)
ans += a[i];
// 输出最大可能的采集量
cout << ans;
return 0;
}
冒泡排序
sort函数的时间复杂度为O(n log n),冒泡排序的时间复杂度为O(n2)。在本题中如果n为10 000以上,冒泡排序将有时间超限的风险。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
int a[100010];
for(int i = 0; i < n; i++)
cin >> a[i];
// 使用冒泡排序实现从大到小排序
for(int i = 0; i < n - 1; i++)
for(int j = 0; j < n - 1 - i; j++)
if(a[j] < a[j + 1]) // 如果前一个小于后一个,交换位置
swap(a[j], a[j + 1]);
// 计算最多可以采集的宇宙数量k = ⌈n/2⌉
int k = ceil(n * 1.0 / 2);
int ans = 0;
// 选择最大的k个值(排序后数组前k个元素)
for(int i = 0; i < k; i++)
ans += a[i];
cout << ans;
return 0;
}
G 小Q的中央有限曲线
前缀和模板题。
#include<bits/stdc++.h>
using namespace std;
// 定义数组:a存储原始数据,s存储前缀和
int a[100010], s[100010];
int main()
{
int n, m;
cin >> n >> m; // 读取宇宙数量n和查询次数m
// 读取每个宇宙的稳定性数值,从下标1开始存储
for(int i = 1; i <= n; i++)
cin >> a[i];
// 计算前缀和数组
// s[i]表示前i个宇宙的稳定性数值之和
// 即:s[i] = a[1] + a[2] + ... + a[i]
for(int i = 1; i <= n; i++)
s[i] = s[i-1] + a[i]; // 当前前缀和 = 前一个前缀和 + 当前数值
// 处理m次查询
while(m--)
{
int l, r;
cin >> l >> r; // 读取查询区间[l, r]
// 利用前缀和计算区间和:s[r] - s[l-1]
// s[r] = a[1] + a[2] + ... + a[r]
// s[l-1] = a[1] + a[2] + ... + a[l-1]
// 相减得到:a[l] + a[l+1] + ... + a[r]
cout << s[r] - s[l-1] << '\n'; // 使用'\n'提高输出效率
}
return 0;
}
C 小Q的D-Mail
较难的字符串处理。
#include<bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s; // 读取原始字符串
// 技巧:在字符串末尾添加一个不会出现的字符(大写'A')
// 这样可以确保最后一个字符序列也能被正确处理
s += "A";
int o = 1; // 计数器,记录当前字符连续出现的次数,初始为1
string s1 = ""; // 存储压缩后的结果字符串
// 遍历字符串(从第2个字符开始到最后一个添加的字符)
for(int i = 1; i < s.size(); i++)
{
if(s[i] == s[i-1]) // 当前字符与前一个字符相同
o++; // 增加计数器
else
{
// 遇到不同字符,处理前一个字符序列
if(o != 1) // 如果连续出现次数大于1
{
s1 += s[i-1]; // 添加字符本身
s1 += to_string(o); // 添加出现次数
}
else // 只出现1次
s1 += s[i-1]; // 只添加字符本身
o = 1; // 重置计数器为1(当前新字符)
}
}
// 检查压缩后长度是否超过36字节
if(s1.size() > 36)
cout << "D-Mail too long!";
else
cout << s1;
return 0;
}
J 小Q的乒乓球对决 II
作为本场的防AK题,应该看两眼题面就被劝退了吧?
被劝退是对的!如果优先死磕难题,不仅会浪费大量时间,还容易心态爆炸。
动态规划题目看题解不会很难理解,不过上手写的话会相当有难度。
感兴趣的同学可以参悟下面两份不同版本题解后尝试自己写一下~
来自hyc学长的动态规划代码。
#include <bits/stdc++.h>
using namespace std;
#define int long long
int x, y;
double pa, pb, ans;
double dp[30][30]; // dp[i][j]表示比分i:j时的概率
/**
* 处理平局阶段(10:10以后)
* @param a 小Q当前得分
* @param b Z老师当前得分
* @param c 起始发球方:1-小Q发球,0-Z老师发球
*/
void OT(int a, int b, int c) {
int sum = a + b; // 当前总得分
// 遍历所有可能的比分
for (int i = a; i < 20; i++) {
for (int j = max(b, i - 1); j <= i + 1; j++) {
// 计算当前发球方:平局阶段每球换发
int k = (i + j - sum + c) % 2; // k=1:小Q发球, k=0:Z老师发球
if (k) { // 小Q发球
dp[i + 1][j] += dp[i][j] * pa; // 小Q得分
dp[i][j + 1] += dp[i][j] * (1 - pa); // Z老师得分
} else { // Z老师发球
dp[i + 1][j] += dp[i][j] * (1 - pb); // 小Q得分
dp[i][j + 1] += dp[i][j] * pb; // Z老师得分
}
}
}
// 累加小Q获胜的概率
for (int i = 12; i <= 20; i++) {
ans += dp[i][i - 2]; // 领先2分获胜(12:10, 13:11, ..., 20:18)
}
ans += dp[20][19]; // 特殊规则:19:19时先到20分获胜
}
/**
* 处理常规阶段(10:10之前)
* @param a 小Q起始得分
* @param b Z老师起始得分
*/
void game(int a, int b) {
int sum = a + b; // 当前总得分
// 计算发球模式:常规阶段每人连续发2球
// q和p表示小Q发球的轮次(在4球循环中的位置)
int q = 0, p = 1; // 正常情况下小Q发第1、2球
if (sum % 2) p = 3; // 如果总得分奇数,调整发球模式
// 遍历常规阶段所有可能比分
for (int i = a; i < 11; i++) {
for (int j = b; j < 11; j++) {
int k = (i + j - sum) % 4; // 计算当前在4球循环中的位置
if (k == q || k == p) { // 小Q发球轮次
dp[i + 1][j] += dp[i][j] * pa; // 小Q得分
dp[i][j + 1] += dp[i][j] * (1 - pa); // Z老师得分
} else { // Z老师发球轮次
dp[i + 1][j] += dp[i][j] * (1 - pb); // 小Q得分
dp[i][j + 1] += dp[i][j] * pb; // Z老师得分
}
}
}
// 累加小Q在常规阶段获胜的概率(11:0 到 11:9)
for (int i = 0; i <= 9; i++)
ans += dp[11][i];
// 清除10:11和11:10的概率(这些比分不会在常规阶段出现)
dp[11][10] = 0;
dp[10][11] = 0;
// 计算进入平局阶段时的起始发球方
int k = (10 + 10 - sum) % 4; // 10:10时的发球轮次
int c = 0; // 平局阶段起始发球方
if (k == q || k == p) c = 1; // 如果10:10时是小Q发球,则平局阶段从小Q开始
// 处理平局阶段
OT(10, 10, c);
}
void solve() {
cin >> x >> y >> pa >> pb;
dp[x][y] = 1; // 初始化:当前比分的概率为1
// 根据当前比分选择处理方式
if (x < 10 && y < 10)
game(x, y); // 常规阶段
else
OT(x, y, 1); // 平局阶段,假设从小Q发球开始
cout << fixed << setprecision(6) << ans << endl;
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
solve();
return 0;
}
记忆化搜索。(在赛时将时间限制设为2s,即使不做记忆化,常规DFS也不会时间超限)
#include<bits/stdc++.h>
using namespace std;
// 记忆化数组:m[i][j][r][t] 表示当前比分i:j,发球方r,当前发球轮次t时小Q获胜的概率
double m[25][25][2][2];
double Pa, Pb; // 小Q发球得分概率,Z老师发球得分概率
/**
* 递归计算小Q获胜概率
* @param i 小Q当前得分
* @param j Z老师当前得分
* @param r 当前发球方:0-小Q发球,1-Z老师发球
* @param t 当前发球轮次:0-第1个发球,1-第2个发球(仅在常规阶段有效)
* @return 小Q获胜的概率
*/
double dfs(int i, int j, int r, int t) {
// 边界条件:根据特殊规则,先到20分者获胜
if (i >= 20) return 1.0; // 小Q达到20分获胜
if (j >= 20) return 0.0; // Z老师达到20分获胜
// 常规胜利条件:先到11分且领先至少2分
if (i >= 11 && i - j >= 2) return 1.0; // 小Q常规胜利
if (j >= 11 && j - i >= 2) return 0.0; // Z老师常规胜利
// 记忆化:如果已经计算过,直接返回结果
if (m[i][j][r][t] >= 0) return m[i][j][r][t];
double res = 0;
// 平局阶段(双方都达到10分以上)
if (i >= 10 && j >= 10) {
if (r == 0) { // 小Q发球
// 平局阶段每球换发:赢球得分且换发,输球对方得分且换发
res = Pa * dfs(i + 1, j, 1, 0) + (1 - Pa) * dfs(i, j + 1, 1, 0);
} else { // Z老师发球
res = (1 - Pb) * dfs(i + 1, j, 0, 0) + Pb * dfs(i, j + 1, 0, 0);
}
}
// 常规阶段
else {
if (t == 0) { // 当前是第1个发球
if (r == 0) { // 小Q发球
// 赢球或输球后,仍由小Q发球(第2个发球)
res = Pa * dfs(i + 1, j, 0, 1) + (1 - Pa) * dfs(i, j + 1, 0, 1);
} else { // Z老师发球
res = (1 - Pb) * dfs(i + 1, j, 1, 1) + Pb * dfs(i, j + 1, 1, 1);
}
} else { // 当前是第2个发球
if (r == 0) { // 小Q发球
// 第2个发球结束后,换Z老师发球
res = Pa * dfs(i + 1, j, 1, 0) + (1 - Pa) * dfs(i, j + 1, 1, 0);
} else { // Z老师发球
// 第2个发球结束后,换小Q发球
res = (1 - Pb) * dfs(i + 1, j, 0, 0) + Pb * dfs(i, j + 1, 0, 0);
}
}
}
// 保存结果到记忆化数组
m[i][j][r][t] = res;
return res;
}
int main() {
int x, y;
cin >> x >> y; // 读取当前比分
cin >> Pa >> Pb; // 读取得分概率
// 初始化记忆化数组为-1,表示未计算
for (int i = 0; i < 25; i++) {
for (int j = 0; j < 25; j++) {
for (int k = 0; k < 2; k++) {
for (int l = 0; l < 2; l++) {
m[i][j][k][l] = -1;
}
}
}
}
double ans;
// 根据当前比分确定初始状态
if (x >= 10 && y >= 10) {
// 平局阶段:初始发球方为小Q(0),轮次参数设为0(在平局阶段轮次参数无意义)
ans = dfs(x, y, 0, 0);
} else {
// 常规阶段:根据总得分判断是小Q的第几个发球
if ((x + y) & 1) { // 总得分为奇数,是小Q的第2个发球
ans = dfs(x, y, 0, 1);
} else { // 总得分为偶数,是小Q的第1个发球
ans = dfs(x, y, 0, 0);
}
}
// 输出结果,保留6位小数
cout << fixed << setprecision(6) << ans << endl;
return 0;
}
赛时随笔
观察到有人用long (int)通过了D题。
在C语言中,long 类型的字节长度并不是固定的,它会根据不同的操作系统和平台有所变化。一般来说,在32位系统中,long 通常占用 4个字节,而在64位系统中,long 可能占用 8个字节。
机房电脑是32位系统,long占4个字节,与int大小相同,所以在机房电脑的DevC++中样例3000的结果无法正确输出。
而OJ服务器是64位系统,long占8个字节,与long long大小相同,可以通过题目。
C题本来是要当作防AK,念起今年同学们的代码能力较强,即增添了J题作防AK。结果不出所料,C题能过3个人(其余两个是打星参赛)。
在难度预期里前缀和模板题G题是比C题等级低的,但没想到大家都还没学前缀和…
439

被折叠的 条评论
为什么被折叠?



