第四章 数学知识

数论

质数

定义为在大于1的整数中,如果只包含1和本身两个约数,就被称为质数,或者叫素数。

质数的判定 - 试除法

暴力解就是逐个枚举从2 ~ (n-1)的所有数字看是否是n的因子,优化解考虑只需要找到两个因子中较小的因子就行,那样只需要枚举到sqrt(n),但是如果在循环条件判断中用sqrt函数会造成额外时间开销(每轮循环都调用该函数),但是如果i^2作为条件判断又可能出现数值溢出风险,故最终采用如下代码。

cpp 复制代码
bool is_prime(int n) 
{
    if (n < 2) return false;
    
    for (int i = 2; i <= n / i; ++i) 
    {
        if (n % i == 0) return false;
    }
    
    return true;
}
 

分解质因数 - 试除法

从2开始枚举到n,如果判定为质因数,那么就一直整除消掉该因子并记录该质因子,该方法借鉴了质数判定法的优化思路,最终实现O(sqrt(n))的时间复杂度。

cpp 复制代码
void divide(int x)
{
    for (int i = 2; i <= n / i; i ++)
        if(n % i == 0)
        {
            int s = 0;
            while(n % i == 0)
            {
                n /= i;
                s ++;
            }
            cout << i << ' ' << s << endl;
        }
}

筛质数

最暴力的方法是2 ~ (n-1)的每个数都乘上它的倍数进行筛,时间复杂度为O(nlnn),代码实现如下:

cpp 复制代码
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i])             // 如果 i 没有被标记,说明它是质数
            primes[cnt++] = i;   // 把质数 i 存起来(注意这里存的是 i,不是 n)
            
            // 用花括号把内层循环包起来,让它受 if 的控制
        for (int j = i + i; j <= n; j += i) 
            st[j] = true;    // 把 i 的倍数全部标记为合数
    }
}

针对朴素筛法的改进是埃氏筛,他只对质数的倍数进行筛,时间复杂度优化为O(nloglogn),代码实现如下:

cpp 复制代码
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i]) {            // 如果 i 没有被标记,说明它是质数
            primes[cnt++] = i;   // 把质数 i 存起来(注意这里存的是 i,不是 n)
            
            // 用花括号把内层循环包起来,让它受 if 的控制
            for (int j = i + i; j <= n; j += i) {
                st[j] = true;    // 把 i 的倍数全部标记为合数
            }
        }
    }
}

更进一步优化是线性筛,它的优化原则是每一个合数只被自己的最小质因子筛选一次,因此,时间复杂度是O(n),代码实现如下:

cpp 复制代码
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        // 如果当前数字 i 没有被标记,说明它是质数,存入质数表
        if (!st[i]) {
            primes[cnt++] = i; 
        }
        
        // 内层循环:用当前已知的质数 primes[j] 去筛掉合数 i * primes[j]
        // 条件 primes[j] <= n / i 是为了防止 i * primes[j] 越界(等价于 i * primes[j] <= n)
        for (int j = 0; primes[j] <= n / i; j++) {
            st[primes[j] * i] = true; // 标记合数
            
            // ⭐ 灵魂刹车(线性筛的核心优化):
            // 如果 i 能被 primes[j] 整除,说明 primes[j] 已经是 i 的最小质因子。
            // 此时必须跳出循环,保证每个合数只被它的最小质因子筛掉一次。
            if (i % primes[j] == 0) break; 
        }
    }
}

约数

试除法求一个数的所有约数

逐个枚举数验证,这里优化可以只列举较小的约数,较大的通过计算直接得出,但是这个过程中还需要注意边界问题。代码实现如下:

cpp 复制代码
vector<int> get_divisors(int n) {
    vector<int> res;
    // 只需要遍历到 sqrt(n),即 i <= n / i
    for (int i = 1; i <= n / i; i++) {
        if (n % i == 0) {
            res.push_back(i); // 存入较小的约数 i
            // 如果 i 不等于 n/i,说明不是完全平方数的平方根,存入对应的较大数 n/i
            if (i != n / i) {
                res.push_back(n / i);
            }
        }
    }
    // 由于是成对存入的,此时 vector 内的元素是无序的,需要进行排序
    sort(res.begin(), res.end());
    return res;
}

约数个数

任何一个大于 1 的整数,都可以唯一地分解成若干个质数的乘积。也就是任何正整数 N 都可以写成这种形式:N = p₁^a₁ × p₂^a₂ × ... × pₖ^aₖ。这里的计算需要用到计数原理知识,p1可以从0取到a1,也就是(a1+1),其他以此类推。代码实现如下:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int x;
    cin >> x;
    
    LL res = 1;
    for (int i = 2; i <= x / i; i++) {
        if (x % i == 0) {
            int count = 0;
            while (x % i == 0) {
                x /= i;
                count++;
            }
            res *= (count + 1); // 约数个数公式:(a1+1)*(a2+1)*...
        }
    }
    // ⚠️ 处理剩下的最大质因数
    if (x > 1) res *= 2; 
    
    cout << res << endl;
    return 0;
}

约数之和

cpp 复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

typedef long long LL;
const int mod = 1e9 + 7;

int main() {
    int n;
    cin >> n; // 输入数字的个数
    
    // 使用 unordered_map 作为"全局账本",记录每个质因数及其总指数
    // key 是质因数 p,value 是该质因数的总指数 a
    unordered_map<int, int> primes;

    while (n--) {
        int x;
        cin >> x;
        // 质因数分解,并将结果汇总到 map 中
        for (int i = 2; i <= x / i; i++) {
            while (x % i == 0) {
                x /= i;
                primes[i]++; // 核心:直接累加质因数 i 的指数
            }
        }
        // 别忘了处理剩下的大于 sqrt(x) 的质因数
        if (x > 1) primes[x]++;
    }

    // 统一遍历 map,计算最终的约数和
    LL res = 1;
    for (auto& [p, a] : primes) { // C++17 结构化绑定,写法更简洁
        LL s = 1;
        // 计算当前质因数 p 的部分和:(1 + p + p^2 + ... + p^a)
        while (a--) {
            s = (s * p + 1) % mod;
        }
        res = res * s % mod;
    }<websource>source_group_web_2</websource>

    cout << res << endl;
    return 0;
}

欧几里得算法(辗转相除法)

该算法核心是(a,b)的最大公约数和(a mod b, b)是一样的,代码实现如下:

cpp 复制代码
int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;    
}

欧拉函数

该函数接收数字n作为输入,返回1到n范围内与n互质的数的个数。其实现基于容斥原理推导的数学公式。这里运用了一个数学性质:大于sqrt(n)的质因子最多存在一个(可通过反证法证明)。基于此优化思路,具体实现代码如下:

cpp 复制代码
#include <algorithm>
#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
    while(n --)
    {
        int a;
        cin >> a;
        int res = a;
        for(int i = 2; i <= a / i; i ++)
        {
            if(a % i == 0)
            {
                res = res / i * (i-1);
                while(a % i == 0) a /= i;
            }
        if (a > 1) res = res / a * (a-1);
        cout << res << endl;
        }
    }
    return 0;
}

还有另一种是求1~n中所有数的欧拉函数对应的和,该题可以使用线性筛求解,该代码也就是在线性筛的基础上,分类讨论,在对应三种情况下分别利用欧拉函数的数学公式进行计算,代码实现如下:

cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

// 线性筛求 1 到 n 的欧拉函数
vector<int> get_eulers(int n) {
    vector<int> phi(n + 1); // phi[i] 存储 i 的欧拉函数值
    vector<int> primes;     // 存储筛出来的素数
    vector<bool> st(n + 1, false); // 标记数组,st[i]为true表示i是合数

    phi[1] = 1; // 1的欧拉函数值为1

    for (int i = 2; i <= n; ++i) {
        // 如果 i 没有被筛掉,说明 i 是素数
        if (!st[i]) {
            primes.push_back(i);
            phi[i] = i - 1; // 素数的欧拉函数值等于它本身减一
        }

        // 线性筛的核心:用当前数 i 乘以已有的素数去筛合数
        for (int j = 0; j < primes.size(); ++j) {
            int p = primes[j];
            if (1LL * i * p > n) break; // 防止乘积越界,超出范围则退出
            
            st[i * p] = true; // 标记 i * p 为合数

            if (i % p == 0) {
                // 核心递推公式1:p 是 i 的最小质因子
                phi[i * p] = phi[i] * p;
                break; // 保证每个合数只被它的最小质因子筛掉,这是线性复杂度的关键
            } else {
                // 核心递推公式2:i 和 p 互质
                phi[i * p] = phi[i] * (p - 1);
            }
        }
    }
    return phi;
}

int main() {
    int n;
    cout << "请输入一个正整数 n: ";
    cin >> n;

    vector<int> phi = get_eulers(n);

    // 输出 1 到 n 每个数的欧拉函数值
    for (int i = 1; i <= n; ++i) {
        cout << "phi(" << i << ") = " << phi[i] << endl;
    }
    
    // 如果你只想求某个特定数字 k 的欧拉函数,直接输出 phi[k] 即可
    // cout << n << " 的欧拉函数值为: " << phi[n] << endl;

    return 0;
}

欧拉定理

数论中的欧拉定理被称为费马 - 欧拉定理或欧拉函数定理。如果正整数a与正整数n互质(即它们的最大公约数为1),则满足如下同余式:a^φ(n) ≡ 1 (mod n),其中φ(n)被称为欧拉函数,它表示小于等于n的正整数中,与n互质的数的个数。其中,费马小定理是关于该定理的特殊化,如果p是一个素数,且整数a与p互质,那么a^(p-1)≡ 1(mod p),简单来说,就是 a 的 (p−1) 次方除以素数 p 的余数恒等于 1。

快速幂

题目通常就是求a的k次方对p取幂的结果。若采用暴力解法逐次相乘,时间复杂度高达 O(k)O(k) ,而快速幂算法的核心优化思路在于将指数 kk 转化为二进制表示,从而将其拆解为若干个 2 的幂次之和,代码实现如下:

cpp 复制代码
int qmi(int a, int k, int p)
{
    int res = 1;
    while(k)
    {
        if (k & 1) res = res * a % p;
        k >>= 1;
        a = a * a % p;
    }
    return res;
}

求逆元

求解 a 在模 m 下的逆元 x ,本质上就是求解同余方程 ax≡1(mod m) 。将其还原为普通的线性等式,等价于存在整数 y 使得 ax+my=1 ,这完美契合了贝祖等式 ax+by=gcd⁡(a,b) 的形式。这意味着只要 a 和 m 的最大公约数 gcd⁡(a,m)=1(即两者互质),扩展欧几里得算法解出的 x 就是 a 的模 m 逆元。而在模数 p 为质数且 a 与 p 互质的特定条件下,还可以使用费马小定理来求解。由a^(p-1) ≡ 1(mod p)可以推出a*a^(p-2)≡ 1(mod p)。这意味着,在模素数 p 的情况下, a 的乘法逆元(即 a 的倒数)就是 a^(p-2) 。这在处理大数除法取模时极其重要。代码实现如下:

cpp 复制代码
#include <iostream>

using namespace std;

// 快速幂函数:计算 (base^exp) % mod
long long fastPow(long long base, long long exp, long long mod) {
    long long result = 1;
    base %= mod; // 先取模,防止 base 过大
    while (exp > 0) {
        // 如果当前指数是奇数(二进制末位为1),将当前的 base 乘入结果
        if (exp & 1) {
            result = (result * base) % mod;
        }
        // base 自乘并取模
        base = (base * base) % mod;
        // 指数右移一位(相当于除以2)
        exp >>= 1;
    }
    return result;
}

// 核心应用1:利用费马小定理求 a 在模 p 下的乘法逆元
// 注意:此方法的前提条件是 p 必须是质数
long long modInverse(long long a, long long p) {
    // 根据费马小定理,逆元为 a^(p-2) % p
    return fastPow(a, p - 2, p);
}

int main() {
    long long a, p;
    cout << "请输入整数 a 和质数 p (求 a 模 p 的逆元): ";
    if (!(cin >> a >> p)) return 0;

    // 确保 a 和 p 互质(因为 p 是质数,只要 a 不是 p 的倍数即可)
    if (a % p == 0) {
        cout << a << " 是 " << p << " 的倍数,不存在模逆元。" << endl;
    } else {
        long long inv = modInverse(a, p);
        // 验证:(a * 逆元) % p 应该等于 1
        cout << a << " 模 " << p << " 的乘法逆元是: " << inv << endl;
        cout << "验证: (" << a << " * " << inv << ") % " << p << " = " << (a * inv) % p << endl;
    }

    return 0;
}

拓展欧几里得算法

它不仅能够计算两个整数 a 和 b 的最大公约数 gcd⁡(a,b),还能同时找到一组整数解 (x,y) ,使它们满足著名的贝祖等式。代码实现如下:

cpp 复制代码
#include <iostream>
using namespace std;

// 扩展欧几里得算法
// 返回 a 和 b 的最大公约数,并通过引用返回 x 和 y
int exgcd(int a, int b, int &x, int &y) {
    // 递归终止条件
    if (b == 0) {
        x = 1;
        y = 0;
        return a; // 此时 gcd(a, 0) = a
    }
    
    // 递归调用,注意传入的参数顺序是 (b, a % b)
    // 这里巧妙地将 x1, y1 直接传给了下一层的 x, y
    int gcd = exgcd(b, a % b, x, y); 
    
    // 回溯时更新 x 和 y
    // 根据推导:新的 x 等于上一层的 y (即当前的 x)
    // 新的 y 等于上一层的 x1 (即当前的 x 的旧值) 减去 (a/b) * 上一层的 y
    int temp = x;      // 暂存上一层的 x1
    x = y;             // 当前 x = 上一层 y1
    y = temp - (a / b) * y; // 当前 y = 上一层 x1 - (a/b) * 上一层 y1
    
    return gcd;
}

int main() {
    int a = 48, b = 18;
    int x, y;
    int gcd = exgcd(a, b, x, y);
    
    cout << "gcd(" << a << ", " << b << ") = " << gcd << endl;
    cout << "满足等式 " << a << "x + " << b << "y = " << gcd << " 的解为:" << endl;
    cout << "x = " << x << ", y = " << y << endl;
    // 验证:48 * (-1) + 18 * 3 = -48 + 54 = 6
    
    return 0;
}

高斯消元

高斯消元法用于求解线性方程组,其步骤如下:

  1. 枚举每一列c,找到该列中绝对值最大的元素所在行
  2. 将该行与当前最上方行交换位置
  3. 将该行的首元素化为1(行变换)
  4. 用该行将下方所有行的第c列元素消为0

该方法代码实现如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <cmath> // 引入cmath库以使用fabs求绝对值
using namespace std;

const int N = 110;
const double eps = 1e-6; // 定义极小值eps,用于判断浮点数是否为0

int n;
double a[N][N]; // 存储增广矩阵,a[i][n]为第i个方程的常数项b

// 返回值:0-有唯一解,1-有无穷多组解,2-无解
int gauss()
{
    int c, r; // c表示当前枚举的列,r表示当前枚举的行
    for (c = 0, r = 0; c < n; c++)
    {
        // 1. 找主元:在当前列c中,从第r行开始往下找绝对值最大的一行
        int t = r;
        for (int i = r; i < n; i++)
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        // 2. 判断当前列的最大值是否接近0(即该列全为0)
        if (fabs(a[t][c]) < eps) continue; 

        // 3. 将绝对值最大的行(第t行)交换到当前处理的顶部(第r行)
        for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]);

        // 4. 归一化:将第r行的首元素(主元)变为1
        for (int i = n; i >= c; i--) a[r][i] /= a[r][c];

        // 5. 消元:用第r行将下方所有行的第c列元素消为0
        for (int i = r + 1; i < n; i++)
            if (fabs(a[i][c]) > eps) // 如果下方行的当前列元素不为0
                for (int j = n; j >= c; j--)
                    a[i][j] -= a[r][j] * a[i][c];
        
        r++; // 处理完一行,行数指针下移
    }

    // 6. 判断解的情况
    if (r < n) // 阶梯形矩阵的非零行数小于未知数个数
    {
        for (int i = r; i < n; i++)
            if (fabs(a[i][n]) > eps) // 出现 0 = 非0 的矛盾方程,无解
                return 2;
        return 1; // 否则有无穷多组解
    }

    // 7. 回代求解:从最后一行往上,将上三角矩阵化为单位矩阵
    for (int i = n - 1; i >= 0; i--)
        for (int j = i + 1; j < n; j++)
            a[i][n] -= a[j][n] * a[i][j]; // 注意:a[i][j]此时已经是消元后的系数

    return 0; // 有唯一解
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n + 1; j++) // 注意这里要读入 n+1 列(包含等号右边的常数项)
            cin >> a[i][j];

    int t = gauss();
    if (t == 0)
    {
        for (int i = 0; i < n; i++) 
            printf("%.2f\n", a[i][n]); // 补全printf格式,并输出换行
    }
    else if (t == 1) puts("Infinite group solutions");
    else puts("No solution"); // 修正拼写错误
    
    return 0;
}

组合计数

组合数求解第一种方法可以用递归公式(杨辉三角)求解,代码实现主要是预处理一个数组记载所有数值,具体实现如下:

cpp 复制代码
void init()
{
    for (int i = 0; i < N; i++)
        for (int j = 0; j < i; j ++) {
            if(!j) c[i][j] = 1;
            else c[i][j] = c[i-1][j-1] + c[i-1][j];
        }
}

组合数求解第二种方法直接用数学公式进行求解,但是公式中的除法通过逆元转换成乘法进行预处理。假设模数MOD为质数(通常取10^9+7),根据费马小定理,x的逆元x^(-1) ≡ x^(MOD-2)(mod MOD)。代码实现如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

typedef long long LL;
const int N = 10010, mod = 1e9 + 7;
int fact[N], infact[N];

int qmi(int a, int k, int p)
{
    int res = 1;
    while(k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
        return res;
    }
}

int main()
{
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i ++)
    {
        fact[i] = (LL)fact[i-1] * i % mod;
        infact[i] = (LL)infact[i-1] * qmi(i, mod-2, mod) % mod;
    }
    int a, b;
    cin >> a >> b;
    cout << fact[a] * infact[b] % mod * infact[a-b] % mod;
    return 0;
}

组合数求解第三种方法利用卢卡斯定理,利用这个公式递归使用,直至一方为0停止迭代。该方法另一种理解是将组合数的上标和下标分别写成p进制组合数取模的结果,就等于它们对应 p 进制位上的组合数之积再取模。代码实现如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

int p;

int qmi(int a, int k)
{
    int res = 1;
    while(k)
    {
        if (k & 1) res = res * a % p;
        a = a * a % p;
        k >>= 1;    
    }
    return res;
}

int C(int a, int b)
{
    // 增加一个边界判断,如果 b > a,组合数为 0
    if (b > a) return 0; 
    
    int res = 1;
    for (int i = 1, j = a; i <= b; i ++, j --)
    {
        res = res * j % p;
        res = res * qmi(i, p-2) % p;
    }
    return res; // ✅ 补上缺失的返回值
}

int lucas(int a, int b)
{
    if (a < p && b < p) return C(a, b);
    return (long long)C(a % p, b % p) * lucas(a / p, b / p) % p; // 建议加上 (long long) 防止乘法溢出
}

int main()
{
    int a, b;
    cin >> a >> b >> p;
    cout << lucas(a, b);
    return 0;
}

组合数求解第四种方法是用算数基本定理将组合数看成由几个最小质因数累乘组成,先找出所有质因数,然后质因数次数的确定可以利用勒让德公式计算,具体代码实现如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 5010;
int primes[N], cnt;
bool st[N], sum[N];

// 欧拉筛出所有质因子
void get_primes(int n)
{
    for (int i = 2; i <= n; i ++)
    {
        if (!st[i]) primes[cnt ++] = i;
        for (int j = 0; primes[j] <= n / i; j ++)
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

// 勒让德公式计算质因子指数
int get(int n, int p)
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}

// 高精度乘法
vector<int> mul(vector<int> a, int b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++)
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    
    while(t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return res;
}

int main()
{
    int a, b; // 组合数的上标和下标
    cin >> a >> b;
    get_primes(a);
    for (int i = 0; i < cnt; i ++)
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(b, p) - get(a - b, p); // sum记录每个质因子指数
    }
    vector<int> res;
    res.push_back(1);
    
    for (int i = 0; i < cnt; i ++)
        for (int j = 0; j < sum[i]; j ++)
            res = mul(res, primes[i]);
            
    for (int i = res.size() - 1; i >= 0; i --) cout << res[i];
    
    return 0;
}

容斥原理

求多个集合的并集大小,遵循"奇加偶减"的原则。即:加上所有单个集合的大小,减去所有两两交集的大小,加上所有三个交集的大小,以此类推。该方法通常搭配另一个技巧,即二进制枚举,我们可以用一个 m 位的二进制数来代表这 m 个质数的选取状态(1 表示选,0 表示不选)。遍历 1 到 2^(m-1) 的所有状态,就能完美枚举出所有的非空子集组合。例题890题代码如下:

cpp 复制代码
#include <iostream>
#include <algorithm>

using namespace std;

// 定义长整型别名:防止多个质数相乘时超出 int 范围导致溢出
typedef long long LL;
const int N = 20; // 题目中 m <= 16,开 20 足够
int p[N];         // 用来存储输入的 m 个质数

int main() {
    int n, m;
    cin >> n >> m;
    // 读入 m 个质数
    for (int i = 0; i < m; i++) cin >> p[i];

    int res = 0; // 最终结果:1~n 中能被至少一个质数整除的数的个数

    // 核心:二进制枚举所有非空子集(容斥原理)
    // 1 << m 表示 2 的 m 次方,i 从 1 开始枚举到 2^m - 1,代表所有非空选取状态
    for (int i = 1; i < (1 << m); i++) {
        int t = 1; // 存储当前子集里所有被选中质数的乘积(即最小公倍数)
        int s = 0; // 统计当前子集选中了几个质数(用于判断奇加偶减)

        // 遍历当前状态 i 的每一位,判断是否选中了第 j 个质数
        for (int j = 0; j < m; j++) {
            // i >> j & 1:判断 i 的二进制表示中第 j 位是否为 1
            if (i >> j & 1) {
                // 防溢出与剪枝:如果当前乘积 * 新的质数已经大于 n
                // 那么 1~n 中就不存在能被这个乘积整除的数,直接标记并跳出
                if ((LL)t * p[j] > n) {
                    t = -1; // 标记当前组合无效
                    break;
                }
                t *= p[j]; // 累乘当前质数
                s++;       // 选中的集合数量 + 1
            }
        }

        // 如果乘积未溢出(即该组合有效)
        if (t != -1) {
            // 容斥原理核心:奇加偶减
            // 选中奇数个集合,加上 n/t;选中偶数个集合,减去 n/t
            if (s % 2) res += n / t;
            else       res -= n / t;
        }
    }

    cout << res << endl;
    return 0;
}

简单博弈论

Nim游戏

Nim 游戏本质上是一场二人轮流拿石子的对战。桌上摆放着若干堆石子,两人轮流充当"搬运工",规则很简单:每次只能从其中一堆里拿走任意数量的石子,拿走最后一颗石子的人获胜。

求解这道题的精髓,在于用二进制的视角去解构石子堆 。我们可以将每堆石子的数量拆解成"4、2、1"等二进制面额的筹码。此时,Nim 游戏就变成了一场"筹码配对游戏":我们将所有堆的石子状态进行异或运算,如果结果为 0,说明在每一个二进制面额(如"1"位、"2"位)上,所有堆的筹码总数都是偶数,这便构成了完美的**"必败态(平衡态)"**。在这种状态下,无论你如何拿取,都会打破这种偶数平衡,将局面送给对手。

反之,如果异或结果不为 0,则当前为**"必胜态(失衡态)"** 。这里就涉及到了那个关键的特定操作:因为异或结果不为 0,说明在二进制的最高非零位上存在"多余的筹码"。我们只需要找到包含这个"多余筹码"的那一堆石子,通过拿走一定数量的石子,强行将该堆的数量更新为"原数量与当前异或总和的异或值"。这一步操作能精准地抵消掉那个"多余的筹码",将原本失衡的局面重新拨回异或和为 0 的"必败态"丢给对手。随着石子总数不断减少,这种"打破平衡-恢复平衡"的博弈循环,最终会将"全0"的必败死局留给对手,从而锁定胜局。代码实现如下:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    
    int res = 0; // 用来存所有石子数的异或和
    while (n--) {
        int x;
        cin >> x;
        res ^= x; // 核心操作:把每堆石子数量异或起来
    }
    
    // 如果异或和不为0,先手必胜;否则必败
    if (res != 0) puts("Yes");
    else puts("No");
    
    return 0;
}
相关推荐
吃好睡好便好1 小时前
矩阵旋转的计算
学习·线性代数·算法·矩阵
埃菲尔铁塔_CV算法2 小时前
基于扩张卷积与双分支参数调控的低光照图像增强算法完整研究与工程解析
人工智能·神经网络·算法·机器学习·计算机视觉
迈巴赫车主2 小时前
优先队列(PriorityQueue)
数据结构·算法
hai3152475432 小时前
有规则的AI编制操作系统演进过程展示
人工智能·程序人生·算法·逻辑回归·创业创新
数据仓库搬砖人2 小时前
SHAP 详解:从博弈论原理到 XGBoost 实战
算法
老鱼说AI2 小时前
统计学习方法第七章:支持向量机精讲(超硬核长文深入预警!)
人工智能·深度学习·神经网络·算法·机器学习·支持向量机·学习方法
容器魔方2 小时前
KubeEdge SIG AI: 基于KubeEdge-Ianvs的大模型联邦微调算法
大数据·人工智能·算法·云原生·容器·云计算
列星随旋2 小时前
矩阵快速幂
java·算法·矩阵
z200509302 小时前
今日算法(回溯全排列)
c++·算法·leetcode