1.小红的子序列计数
题目描述
小红认为一个仅包含数字的字符串是好的,当且仅当将其转化为 10 进制数并去除前导零后的结果是 6 的倍数,如 06、48、0 都是好的。给定一个长为 n 的仅包含数字的字符串 s。请你计算有多少 s 的非空子序列是好的。由于答案可能很大,请将答案对 998244353 取模后输出。
【名词解释】子序列:从原序列中删除任意个(可以为零,也可以为全部)元素,且保持剩余元素相对顺序不变得到的新序列。
输入描述
第一行输入一个整数 n(1≤n≤2×10^5)。第二行输入一个长为 n 的字符串 s。
输出描述
输出一个整数,代表答案对 998244353 取模后的值。
示例 1
输入
5
01592
输出
5
说明
合法的子序列有 0,12,012,192,0192。
示例 2
输入
13
1145141919810
输出
1720
AC代码
c
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n; // 字符串长度
string s; // 数字字符串
ll ans = 0; // 最终答案:合法子序列数量
ll dp[3]; // dp[i]:遍历到当前位置之前,所有子序列中,数字和模3等于i的子序列总数
ll mod = 998244353; // 题目要求的模数
signed main() {
cin >> n;
cin >> s;
// 遍历字符串中的每一个字符(数字)
for (auto p : s) {
int a = p - '0'; // 把字符转换成真正的数字 0~9
int b = a % 3; // 计算当前数字模3的结果
ll add = 0; // 本轮新增的合法子序列数量
// --------------------------
// 第一步:统计合法答案(核心)
// 条件:当前数字必须是偶数(作为子序列最后一位,保证能被2整除)
// --------------------------
if (a % 2 == 0) {
// 数学推导:前面子序列和 + 当前数 ≡ 0 (mod3)
// 所以前面需要的模值 = (3 - 当前数模) % 3
int need = (3 - b) % 3;
// 新增的合法子序列 = 前面满足条件的子序列数量
add = dp[need];
// 如果当前数字本身模3=0(0、6),单独选它自己也是合法子序列
if (b == 0) {
add = (add + 1) % mod;
}
// 累加到总答案
ans = (ans + add) % mod;
}
// --------------------------
// 第二步:更新dp数组(关键:用临时数组,不覆盖旧值)
// --------------------------
ll temp[3] = {0}; // 临时数组,保存新的dp状态
temp[0] = dp[0]; // 继承:不选当前数字的情况
temp[1] = dp[1];
temp[2] = dp[2];
// 情况1:把当前数字拼接到【之前所有子序列】的后面
for (int i = 0; i < 3; i++) {
int m = (i + b) % 3; // 新的和模3
temp[m] = (temp[m] + dp[i]) % mod;
}
// 情况2:只选当前数字自己,作为一个新子序列
temp[b] = (temp[b] + 1) % mod;
// 把临时数组赋值回dp,完成状态更新
dp[0] = temp[0];
dp[1] = temp[1];
dp[2] = temp[2];
cout << ans;
return 0;
}
2.小红的博弈
题目描述
现在有 n 堆石子,每堆石子中有若干个石子,用一个数组 a={a1,a2,...,an} 表示。小红和小芳要进行一场游戏,由小红先手轮流进行如下操作:
- 选一个正整数 x,要求 x 不能小于上一步操作所选的数值(特别地,游戏的第一步操作可以任选正整数 x),之后选择任意一个至少含有 x 个石子的堆,从中拿走 x 个石子。
如果轮到某人时无法操作,那么她就输了。我们认为两人都足够聪明,请你判断谁会赢得游戏。
输入描述
每个测试文件均包含多组测试数据。第一行输入一个整数 T(1≤T≤10^5) 代表数据组数,每组测试数据描述如下:第一行输入一个整数 n(1≤n≤2×10^5)。第二行输入 n 个整数,第 i 个整数代表第 i 堆石子中有 ai(1≤ai≤10^9) 个石子。
除此之外,保证单个测试文件的 n 之和不超过 2×10^5。
输出描述
对于每组测试数据,新起一行。如果小红会赢,请输出 red;如果小芳会赢,请输出 fang。
样例输入
2
6
1 1 4 5 1 4
4
7 6 7 6
样例输出
red
fang
AC代码
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
ll t;
cin >> t;
while (t--) {
ll n;
cin >> n;
unordered_map<ll, ll> mp; // 哈希表:key=石子数,value=出现次数
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x; // 输入每堆石子数量
mp[x]++; // 统计该数字出现次数 +1
}
ll cnt = 0; // 标记:是否存在 出现次数为奇数 的数字
for (auto i : mp) { // 遍历哈希表
if (i.second % 2) { // 如果当前石子数出现次数是奇数
cnt = 1; // 标记存在
break; // 找到一个就可以直接退出,不用继续遍历
}
}
// 核心规则:只要有出现奇数次的数字 → 小红胜,否则小芳胜
if (cnt % 2)
cout << "red\n";
else
cout << "fang\n";
}
return 0;
}
总结
1. 大类:ACM / 算法竞赛 → 博弈论(组合游戏)
细分:配对博弈
题目本质结论
把相同石子数当成一组:
- 所有数值出现次数全为偶数:后手必胜 (fang)
- 存在任意一个数值出现次数为奇数:先手必胜 (red)
原理:相同大小的石子可以两两配对,对方怎么操作一组里一个,你镜像操作同数值另一个,全偶数后手稳赢;有奇数项先手破局。
2. 代码实现层面:哈希计数题
- 输入海量数据(T≤10^5,∑n≤2×10^5)
- 用
unordered_map/ 排序离散化统计每个数字出现频次 - 遍历找有没有次数奇数的值即可,找到就直接 break 优化。
3.Jump!无法回头
题目描述
青蛙王国的公主被坏人拐走了,勇敢的呱呱骑士前去营救。
坏人的城外有一条护城河,河面上有一些石头,呱呱可以跳上石头去到对面。每个石头上都有一个数字,数字表示最大跳跃距离(可能为 0)。比如第 3 个石头上是 2,表示下一次可以跳到第 4 块或第 5 块石头。
呱呱想知道,自己是否能救到公主,最快需要几跳能到对岸。
输入格式:
第一行输入n表示石头的数量。
接下来一个长度为n的数组a,空格隔开,其中aᵢ表示第i块石头允许的最大跳跃距离。
对于20%的数据(n<20,1 ≤ aᵢ ≤ 20)
对于50%的数据(n<500,1 ≤ aᵢ ≤ 500)
对于100%的数据(n<2000,0 ≤ aᵢ ≤ 2000) 注意aᵢ可能是0
输出格式:
如果可以营救,输出最少跳跃次数(起始在第1个位置,跳到第n+1个位置,详见样例解释)。如果无法营救输出-1。
输入样例:
5
1 2 3 4 5
输出样例:
3
提示: 有5块石头
1 2 3 4 5
起始在第一个位置
& 2 3 4 5
最大跳跃为1,跳1步
1 & 3 4 5
最大跳跃为2,跳1步
1 2 & 4 5
最大跳跃为3,跳3步
1 2 3 4 5 &
即最少需要3步。
AC代码
c
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
signed main() {
int n;
cin >> n;
int a[2000 + 10];
for(int i = 1; i <= n; i++) cin >> a[i];
// steps:记录跳跃的步数
int steps = 0;
// cur:当前这一步能到达的最远位置
int cur = 1;
// next:下一步能到达的最远位置
int next = 1;
for(int i = 1; i <= n; i++) {
// 更新下一步能到达的最远位置
next = max(next, i + a[i]);
// 如果已经能跳到终点(超过/等于 n+1),直接输出答案并结束
if(next >= n + 1){
cout << steps + 1;
return 0;
}
// 当遍历到当前步的最远边界时
if(i == cur) {
// 如果无法再往前跳,说明被困住,返回 -1
if(cur == next) {
cout << -1;
return 0;
}
// 跳一步
steps++;
// 更新当前能到达的最远位置为下一步能到达的最远位置
cur = next;
}
}
}
4.双向奔赴
题目描述
在一个无穷大的二维网格上,住着几个仙灵。他们能以特殊的方式进行移动。
比如某个仙灵在 (i, j) 位置:
-
可以移动到 (i, j+1) 和 (i, j×2)
-
可以移动到 (i+1, j) 和 (i×2, j)
小明来到了这个无穷网格的原点 (0, 0),想要去拜访几位仙灵朋友,请分别计算小明拜访朋友们最少需要走几步。(每次都从原点开始行动)
输入
第一行输入n表示仙灵朋友数量
接下来n行每行两个整数x,y表示仙灵的位置。
对于20%的数据:n=1,1<=x,y<=50
对于40%的数据:n=1,1<=x,y<=10000
对于60%的数据:n=10,1<=x,y<=1e8
对于100%的数据:1<=n<=100000,1<=x,y<=1e18
输出
输出n行,每行表示从原点出发最少需要的移动次数。
输入样例:
2
1 1
7 8
输出样例:
2
9
**提示:**对于第一个询问路线是:
(0,0)->(0,1)->(1,1)
对于第二个询问路线是:
(0,0)->(0,1)->(1,1)->(1,2)->(2,2)->(2,4)->(3,4)->(6,4)->(6,8)->(7,8)
AC代码
c
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 计算从 0 走到 n 最少需要的步数(只在一个维度上移动)
ll f(ll n) {
if(n == 0) return 0; // 起点 0,不用走
// __builtin_clzll(n):GCC 内置函数
// 作用:计算 64 位无符号整数 n 的**二进制前导 0 的个数**(count leading zeros)
// 例:n=1(二进制 000...0001) → 前导 0 有 63 个
// 例:n=2(二进制 000...0010) → 前导 0 有 62 个
// 63 - 前导0个数 = 数字 n 的二进制最高位的位数(从0开始)
// 也就是:2^a ≤ n < 2^(a+1),a 是最高位指数
int a = 63 - __builtin_clzll(n);
// __builtin_popcountll(n):GCC 内置函数
// 作用:计算 64 位整数 n 的二进制中 1 的个数
// 例:n=3(11) → 返回 2;n=5(101) → 返回2
int b = __builtin_popcountll(n);
// 一个维度上的最少步数 = 最高位位数 + 二进制中1的个数
return a + b;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll n;
cin >> n;
for (ll i = 1; i <= n; i++) {
ll x, y;
cin >> x >> y;
// 总步数 = x 方向最少步数 + y 方向最少步数
// 因为两个方向移动互不干扰,可以独立计算再相加
cout << f(x) + f(y) << '\n';
}
}
两个核心函数通俗解释
__builtin_clzll(n)
-
全称:count leading zeros for long long
-
功能:数出
n的64 位二进制前面有多少个 0 -
公式:
63 - __builtin_clzll(n)= n 的二进制最高位是第几位- 数字 1 → 二进制
1→ 最高位第 0 位 - 数字 2 → 二进制
10→ 最高位第 1 位 - 数字 4 → 二进制
100→ 最高位第 2 位 - 数字 8 → 二进制
1000→ 最高位第 3 位
- 数字 1 → 二进制
__builtin_popcountll(n)
- 全称:population count for long long
- 功能:数出
n的二进制里有多少个 1- 3 →
11→ 2 个 1 - 5 →
101→ 2 个 1 - 7 →
111→ 3 个 1
- 3 →
这道题的数学原理
题目移动规则(单方向):只能做 +1 或 ×2求从 0 → n 最少步数。
最优策略:倒推法
从 n 往回走:
- 偶数:除以 2(对应正向 ×2,1 步)
- 奇数:先减 1(对应正向 +1,1 步),再除以 2
步数公式:
总步数 = 二进制最高位位数 + 二进制中 1 的个数
- 最高位位数:需要乘 2 的次数
- 1 的个数:需要加 1 的次数
两个方向独立计算,再相加就是答案。
样例验证:
样例 1:(1,1)
- f (1):二进制
1,a=0,b=1 → 0+1=1 - f(1) = 1
- 总步数:1+1=2 ✔️
样例 2:(7,8)
- 7 → 二进制
111,a=2,b=3 → 2+3=5 - 8 → 二进制
1000,a=3,b=1 →3+1=4 - 总步数:5+4=9 ✔️
总结
__builtin_clzll(n)算前导 0,用来求二进制最高位__builtin_popcountll(n)算1 的个数- 单方向最少步数 = 最高位位数 + 1 的个数
- 答案 = x 方向步数 + y 方向步数
- 代码能轻松处理 1018 和 105 组数据