【学习记录】

1.小红的子序列计数

D-小红的子序列计数_牛客周赛 Round 146

题目描述

小红认为一个仅包含数字的字符串是好的,当且仅当将其转化为 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.小红的博弈

E-小红的博弈_牛客周赛 Round 146

题目描述

现在有 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';
    }
}

两个核心函数通俗解释

  1. __builtin_clzll(n)
  • 全称:count leading zeros for long long

  • 功能:数出 n64 位二进制前面有多少个 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

这道题的数学原理

题目移动规则(单方向):只能做 +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 ✔️

总结

  1. __builtin_clzll(n)前导 0,用来求二进制最高位
  2. __builtin_popcountll(n)1 的个数
  3. 单方向最少步数 = 最高位位数 + 1 的个数
  4. 答案 = x 方向步数 + y 方向步数
  5. 代码能轻松处理 1018 和 105 组数据
相关推荐
社交怪人1 小时前
【奇偶ASCII值】信息学奥赛一本通C语言解法(题号1042)
算法
Wils0nEdwards1 小时前
技术栈的学习
学习
清辞8532 小时前
入门大模型工程师第四课----通过RAG增强大模型原本无法回答的问题
大数据·人工智能·学习·语言模型
牢七2 小时前
吾爱破解安卓逆向入门教程学习
学习
噜噜噜阿鲁~2 小时前
python学习笔记 | 12.0、错误、调试和测试
笔记·python·学习
小欣加油2 小时前
leetcode3635 最早完成陆地和水上游乐设施的时间II
数据结构·c++·算法·leetcode
三品吉他手会点灯2 小时前
C语言学习笔记 - 46.运算符和表达式 - 运算符4 - 对初学运算符的一些建议
c语言·开发语言·笔记·学习
wangqiaowq2 小时前
Rerank模型学习
学习
呉師傅2 小时前
EPSON爱普生 L3118打印头【喷头】清洗方法
运维·服务器·网络·学习·电脑