数学部分知识点

BLM-dolphin / 2024-02-27 / 原文

数学部分知识点

涉及的知识点

  • 数论

  • 线性代数

  • 组合数学

  • 博弈论

数论

  • 质数
  • 约数
  • 欧拉函数
  • 快速幂
  • 扩展欧几里得算法
  • 中国剩余定理

质数

质数的定义

  • 约数只有1和他自身的整数,被称为质数或素数。

    • 例如,2,3,5,7,11…
  • 也就是,质数与合数的概念是针对大于1的数来定义的。所有小于等于1的整数既不是质数也不是合数。

质数的判定

  • 暴力做法

    • 直接利用定义,枚举从 2 到 $x-1$ 的每个数看是否能整除,时间复杂度是$O(n)$的。

    • 代码

      •   bool is_prim(int x) { //判定质数
          	if (x < 2) return false;
          	for (int i = 2; i < x; i++)
          		if (x % i == 0) return false;
          	return true;
          }
        
  • 优化

    • 性质

      • 如果 $a$ 是 $n$ 的约数,那么 $\frac{n}{a}$ 也是 $n$ 的约数。
    • 对于每一对 $(a,\frac{n}{a})$,可以枚举每一对当中较小的那一个。时间复杂度可以降到$O(\sqrt{n})$

    • 代码

      •   bool is_prim(int x) { //判定质数
          	if (x < 2) return false;
          	for (int i = 2; i <= x / i; i++)
          		if (x % i == 0) return false;
          	return true;
          }
        
      • 细节

        • 不要写i<=sqrt(n)sqrt运算较慢
        • 不要写i*i<=ni*i容易溢出
    • 练习 洛谷 P5736

分解质因数

  • 唯一分解定理

    • 每个正整数都能够唯一的表示成它的质因数的乘积。
      $$
      n=p_1^{\alpha_1} p_2{ }^{\alpha_2} \cdots p_s^{\alpha_s},\quad p_1<p_2<\cdots<p_s
      $$
  • 朴素算法

    • 思路:从小到大枚举 $n$ 的所有因子

    • 代码

      •   void decompose(int x) { //分解质因数
          	for (int i = 2; i <= x; i++)
          		while (x % i == 0) a[i]++, x /= i;
          }
        
      • 细节

        • 这里循环枚举的是大于2的整数,而不是枚举质因数。思考一下为什么可行。
    • 时间复杂度 $O(n)$

  • 优化

    • 性质

      • $n$ 中最多只包含一个大于$\sqrt{n}$的质因数
    • 代码

      •   void decompose(int x) { //分解质因数
          	for (int i = 2; i <= x / i; i++)
          		while (x % i == 0) a[i]++, x /= i;
          	if (x > 1) a[x]++;
          }
        
    • 举例

      •   x=280
          i=2,   a[2]=3,x=35
          i=3,4,
          i=5,   a[5]=1,x=7
          7>1    a[7]=1
        
    • 时间复杂度

      • $O(\sqrt{n})$
      • 最好$O(\log n)$,最坏 $O(\sqrt{n})$ 的
        • 当$n=2^k$的时候,时间复杂度就是$O(\log n)$
        • 当n是质数,枚举 $\sqrt{n}$ 次。
    • 练习 洛谷 P2043

筛素数

  • 朴素做法

    • 思想

      • 依次删掉2的倍数,3的倍数,4的倍数,5的倍数……筛完之后剩下的就是质数
    • 代码

      •   void get_prim(int n) { //朴素做法
          	for (LL i = 2; i <= n; i++) {
          		if (!vis[i]) {
          			prim[++cnt] = i;
          		}
          		for (LL j = i + i; j <= n; j += i) vis[j] = true;
          	}
          }
        
    • 时间复杂度

      • 对于每个$i$,筛的次数是 $\frac{n}{i}$
      • 从2开始筛,筛的次数是$\frac{n}{2}+\frac{n}{3}+\cdots+\frac{n}{n}=n(\frac{1}{2}+\frac{1}{3}+\cdots+\frac{1}{n})$
        • 当$n\rightarrow+\infty$时,$1+\frac{1}{2}+\frac{1}{3}+\cdots+\frac{1}{n}=\ln n+c$,其中$c$是欧拉常数
      • 又因为 $n\ln n < n \log_{2} n$的,所以时间复杂度可以认为是 $O(n \log n)$
  • 埃氏筛法

    • 思想

      • 不用把每个数的倍数删掉,只需要把质数的倍数删掉就可以了
    • 代码

      •   void get_prim(int n) { //埃氏筛法
          	for (LL i = 2; i <= n; i++) {
          		if (!vis[i]) {
          			prim[++cnt] = i;
          			for (LL j = i * i; j <= n; j += i) vis[j] = 1;
          		}
          	}
          }
        
      • 细节

        • 这里j枚举的时候是从i*i开始枚举的,因为i*2~i*(i-1)中的所有合数已经被筛掉了
    • 举例

      •   n=20
          i=2: 质数{2}; 筛掉{4,6,8,10,12,14,16,18,20}
          i=3: 质数{3}; 筛掉{9,12,15,18}
          i=4:
          i=5: 质数{5}
          i=6:
          i=7: 质数{7}
          i=8,9,10
          i=11: 质数{11}
          i=12:
          i=13: 质数{13}
          i=14,15,16
          i=17: 质数{17}
          i=18:
          i=19: 质数{19}
          i=20:
        
    • 时间复杂度 $O(n \log (\log n))$

      • 当$n=2^{32}$时, $\log (\log n)=5$
  • 线性筛法(欧拉筛法)

    • 思想

      • 每一个合数,只会被他的最小质因子筛掉
    • 流程

      • 从小到大枚举每个数i
        • 如果当前数没划掉,必定是质数,记录该质数
        • 枚举已记录的质数 prim[j],与i乘构成合数i * prim[j]
          • 筛:
            • 合数未越界,则划掉合数;
          • 停:
            • 如果合数已越界则中断
            • 若ⅰ是质数,则最多枚举到自身中断
            • 若ⅰ是合数,则最多枚举到自身的最小质因数中断
    • 代码

      •   void get_prim(int n) { //线性筛法
          	for (int i = 2; i <= n; i++) {
          		if (!vis[i]) prim[++cnt] = i;
          		for (int j = 1; prim[j] <= n / i; j++) {
          			vis[i * prim[j]] = 1;
          			if (i % prim[j] == 0) break;   //条件成立时,说明prim[j]是i的最小质因子
          		}
          	}
          }
        
    • 举例

      •   n=30
          i=2: 质数{2}; 筛掉{4}; (2%2==0) break;
          i=3: 质数{3}; 筛掉{6,9}; (3%3==0) break;
          i=4:         筛掉{8}; (4%2==0) break;
          i=5: 质数{5}; 筛掉{10,15,25}; (5%5==0) break;
          i=6:         筛掉{12}; (6%2==0) break;
          i=7: 质数{7}; 筛掉{14,21};(35>30);
          i=8:         筛掉{16}; (8%2==0) break;
          i=9:         筛掉{18,27}; (9%3==0) break;
          i=10:         筛掉{20}; (10%2==0) break;
          i=11: 质数{11}; 筛掉{22};(33>30);
          i=12:        筛掉{24};(12%2==0) break;
          ……
          i=16:                (32>30)
        
    • 算法的正确性

      • 筛掉的每一个数,一定是通过它的最小质因子筛掉的。
        • 筛数的过程vis[i * prim[j]] = 1;,分两种情况讨论
          • i % prim[j] ==0时,prim[j]一定是 i的最小质因子,也一定是i*prim[j]的最小质因子。
          • i % prim[j] !=0时,那么prim[j]一定小于i的所有质因子,所以prim[j]i*prim[j]的最小质因子。
        • 综上,i*prim[j]一定是被自己的最小质因子筛掉的。
      • 任何一个合数,一定会被筛掉
        • 任何一个合数,都会存在一个最小质因子,假设是pj。那么当i枚举到x/pj的时候,那么我们就会在这个时候通过vis[i * prim[j]] = 1;把它筛掉。
    • 时间复杂度

      • $O(n)$
      • 每个质数只被记录一次,每个合数只被筛掉一次
      • 线性筛法在10^7的时候,会比埃氏筛法快一倍
    • 练习 洛谷 P3383

约数

试除法求约数

  • 核心思想

    • 从小到大判断,类似质数判定。
    • 如果$d|n$,那么$\frac{n}{d}|n$
  • 代码

    •   vector<int> get_divisors(int x) {
        	vector<int> res;
        	for (int i = 1; i <= x / i; i ++ )
        		if (x % i == 0) {
        			res.push_back(i);
        			if (i != x / i) res.push_back(x / i);
        		}
        	sort(res.begin(), res.end());
        	return res;
        }
      
    • 细节

      • 如果 i == x / i,说明两个约数相同,只要记录一次。
  • 时间复杂度 $O(\sqrt{n})$

    • 一个数的约数个数的期望是$O(\log {n})$的,这里排序的时间复杂度是$O(\log {n}\log {\log {n}})$,小于 $O(\sqrt{n})$
      • 1是$n$个数的约数,2是$\frac{n}{2}$个数的约数,3是$\frac{n}{3}$个数的约数,……加起来求平均不超过 $O(\log {n})$

约数个数

  • 定理

    • 若 $n=\prod_{i=1}^s p_i{ }^{\alpha_i}$, 则 $d(n)=\prod_{i=1}^s\left(\alpha_i+1\right)$
    • 证明
      • $p_i{ }^{\alpha_i}$ 的约数有 $p_i^0, p_i^1, \cdots, p_i^{\alpha_i}$ 共 $\left(\alpha_i+1\right)$ 个, 根据乘法原理, $d(n)=\prod_{i=1}^s\left(\alpha_i+1\right)$ 。
  • 题目

    • 给定 $n$ 个正整数 $a_i$ ,请你输出这些数的乘积的约数个数,答案对 $10^9+7$ 取模。

    • 代码

      •   #include <iostream>
          #include <algorithm>
          #include <unordered_map>
          #include <vector>
          
          using namespace std;
          
          typedef long long LL;
          
          const int N = 110, mod = 1e9 + 7;
          
          int main() {
          	int n;
          	cin >> n;
          
          	unordered_map<int, int> primes;
          
          	while (n -- ) {
          		int x;
          		cin >> x;
          
          		for (int i = 2; i <= x / i; i ++ )
          			while (x % i == 0) {
          				x /= i;
          				primes[i] ++ ;
          			}
          
          		if (x > 1) primes[x] ++ ;
          	}
          
          	LL res = 1;
          	for (auto p : primes) res = res * (p.second + 1) % mod;
          
          	cout << res << endl;
          
          	return 0;
          }
        
  • 题目

    • 给一个数 $n\left(n \leq 1 \times 10^6\right)$, 输出 $1 \sim n$ 每个数的约数个数。

    • 思路

      • 两个数组
        • a[i]:整数 i 的最小质因子的次数
        • d[i]:整数 i 的约数的个数
      • 若 $i$ 是质数
        • a[i]=1d[i]=2
      • 在线性筛中, 每个合数 $m$ 都是被最小的质因子筛掉的。
        • 设 $p_j$ 是 $m$ 的最小质因子, 则 $m$ 通过 $m=p_j \times i$ 筛掉。
      • 寻找a[m]a[i]d[m]d[i]之间的关系
        • i%p[j]==0, 则 $p_j$ 一定是 $i$ 的最小质因子。
          • a[m]=a[i]+1
          • d[i]=(a[i]+1)*…d[m]=(a[m]+1)*…,所以d[m]=d[i]/a[m]*(a[m]+1)
        • i%p[j]!=0,则 $i$ 不包含质因子 $p_j$ 。
          • a[m] = 1
          • d[m]=d[i]*(1+1)
    • 代码

      •   void get_d(int n) { //筛法求约数个数
          	d[1] = 1;
          	for (int i = 2; i <= n; i++) {
          		if (!vis[i]) {
          			p[++cnt] = i;
          			a[i] = 1;
          			d[i] = 2;
          		}
          		for (int j = 1; i * p[j] <= n; j++) {
          			int m = i * p[j];
          			vis[m] = 1;
          			if (i % p[j] == 0) {
          				a[m] = a[i] + 1;
          				d[m] = d[i] / a[m] * (a[m] + 1);
          				break;
          			} else {
          				a[m] = 1;
          				d[m] = d[i] * 2;
          			}
          		}
          	}
          }
        
      • 时间复杂度 $O(n)$

    • 举例

约数之和

  • 定理

    • 若 $n=\prod_{i=1}^s p_i^{\alpha_i}$, 则 $f(n)=\prod_{i=1}^s \sum_{j=0}^{\alpha_i} p_i^j$

    • 证明

      • $p_i{ }^{\alpha_i}$ 的约数有 $p_i^0, p_i^1, \cdots, p_i^{\alpha_i}$ 共 $\left(\alpha_i+1\right)$ 个,其约数和为 $\sum_{j=0}^{\alpha_i} p_i^j$,
      • 根据乘法原理, $f(n)=\prod_{i=1}^S \sum_{j=0}^{\alpha_i} p_i^j$
    • 举例

    • $$
      \begin{aligned}
      & 12=2^2 \times 3^1 \
      & f(12)={1+2+4} \times{1+3}=7 \times 4=28
      \end{aligned}
      $$

  • 题目

    • 给定 $n$ 个正整数 $a_i$ ,请你输出这些数的乘积的约数之和,答案对 $10^9+7$ 取模。

    • 代码

      •   #include <iostream>
          #include <algorithm>
          #include <unordered_map>
          #include <vector>
          
          using namespace std;
          
          typedef long long LL;
          
          const int N = 110, mod = 1e9 + 7;
          
          int main() {
          	int n;
          	cin >> n;
          
          	unordered_map<int, int> primes;
          
          	while (n -- ) {
          		int x;
          		cin >> x;
          
          		for (int i = 2; i <= x / i; i ++ )
          			while (x % i == 0) {
          				x /= i;
          				primes[i] ++ ;
          			}
          
          		if (x > 1) primes[x] ++ ;
          	}
          
          	LL res = 1;
          	for (auto p : primes) {
          		LL a = p.first, b = p.second;
          		LL t = 1;
          		while (b -- ) t = (t * a + 1) % mod;
          		res = res * t % mod;
          	}
          
          	cout << res << endl;
          
          	return 0;
          }
        
  • 题目

    • 给一个数 $n\left(n \leq 1 \times 10^6\right)$, 输出 $1 \sim n$ 每个数的约数和。

    • 思路

      • g[i]:整数 i 的最小质因子的约数和,$g[i]=p_j0+p_j1+\cdots+p_j^{\alpha_j}$
      • f[i]:整数 i 的约数和
      • 若 $i$ 是质数
        • g[i]=f[i]=i+1
      • 在线性筛中, 每个合数 $m$ 都是被最小的质因子筛掉的。
        • 设 $p_j$ 是 $m$ 的最小质因子, 则 $m$ 通过 $m=p_j \times i$ 筛掉。
      • 寻找g[m]g[i]f[m]f[i]之间的关系
        • i%p[j]==0, 则 $p_j$ 一定是 $i$ 的最小质因子。
          • $g[i]=p_j0+p_j1+\cdots+p_j{\alpha_j}$,$g[m]=p_j0+p_j1+\cdots+p_j$,所以有g[m]=g[i]*p[j]+1
          • $f[i]=g[i] \times \cdots$,$f[m]=g[m] \times \cdots$,所以有f[m]=f[i]/g[i]*g[m]
        • i%p[j]!=0,则 $i$ 不包含质因子 $p_j$
          • $g[m]=1+p_j$
          • $f[m]=g[m] \times f[i]$
    • 代码

      •   #include <iostream>
          using namespace std;
          
          const int N = 1000010;
          int p[N], vis[N], cnt;
          //g[i]表示i的最小质因子的1+p^1+...+p^k
          int g[N], f[N];//f[i]表示i的约数和
          
          void get_f(int n){ //筛法求约数和
            g[1] = f[1] = 1;
            for(int i=2; i<=n; i++){
              if(!vis[i]){
                p[++cnt] = i;
                g[i] = f[i] = i+1;
              }
              for(int j=1; i*p[j]<=n; j++){
                int m = i*p[j]; 
                vis[m] = 1;
                if(i%p[j] == 0){
                  g[m] = g[i]*p[j]+1;
                  f[m] = f[i]/g[i]*g[m];
                  break;
                } 
                else{
                  g[m] = p[j]+1;
                  f[m] = f[i]*g[m];
                }
              }
            }
          }
          int main(){
            int n;
            cin >> n;
            get_f(n);
            for(int i=1; i<=n; i++)
              printf("%d\n",f[i]);
            return 0;
          }
        
    • 举例

逆元

欧拉函数

  • 定义

    • $1 \sim n$ 中与 $n$ 互质的数的个数称为欧拉函数, 记为 $\varphi(n)$
  • 举例

    • $\varphi(1)=1, \varphi(2)=1, \varphi(3)=2, \varphi(4)=2, \varphi(5)=4$
  • 欧拉函数的性质

    • 若 $p$ 是质数, 则 $\varphi(p)=p-1$
      • 因为任何一个质数都是和它前面的整数互质
    • 若 $p$ 是质数, 则 $\varphi\left(p^k\right)=(p-1) p^{k-1}$
      • $1……p……2p……3p……p^k$
      • 其中,$p,2p,3p,\cdots,pk$都和$pk$不互质共$p^{k-1}$个
      • 所以和$p^k$互质的一共有$(p-1) p^{k-1}$
    • 积性函数:若 $g c d(m, n)=1$, 则 $\varphi(m n)=\varphi(m) \varphi(n)$
  • 欧拉函数的计算公式

    • $$
      \varphi(n) = n \times \frac{p_1-1}{p_1} \times \frac{p_2-1}{p_2} \times \cdots \times \frac{p_s-1}{p_s}
      $$

    • 证法1

      • 由唯一分解定理 $n=\prod_{i=1}^S p_i{ }^{\alpha_i}=p_1{ }^{\alpha_1} p_2{ }^{\alpha_2} \cdots p_s{ }^{\alpha_s}$,

        • $
          \begin{aligned}
          \varphi(n) & =\prod_{i=1}^s \varphi\left(p_i^{\alpha_i}\right) \
          & =\prod_{i=1}^s p_i^{\alpha_i-1}\left(p_i-1\right) \
          & =\prod_{i=1}^s p_i^{\alpha_i}\left(1-\frac{1}{p_i}\right) \
          & =\prod_{i=1}^s p_i^{\alpha_i} \times \prod_{i=1}^s\left(1-\frac{1}{p_i}\right) \
          & =n \times \prod_{i=1}^s \frac{p_i-1}{p_i} \
          & =n \times \frac{p_1-1}{p_1} \times \frac{p_2-1}{p_2} \times \cdots \times \frac{p_s-1}{p_s}
          \end{aligned}
          $

        • 第一个等号用到了性质3,第二个等号用到了性质2,第三个等号是把括号里面的$p_{i}$提出来,第四个等号是改变连乘的顺序,第五个等号是唯一分解定理和通分。

    • 证法2

      • 从1~n中去掉$p_1,p_2,\cdots,p_k$所有的倍数,加上$p_ip_j$的倍数,减去$p_ip_j*p_k$的倍数,……

      • 所以个数就是
        $
        n-\frac{n}{p_1}-\frac{n}{p_2}-\cdots-\frac{n}{p_3} \
        +\frac{n}{p_1p_2}+\frac{n}{p_1p_3}+\cdots \
        -\frac{n}{p_1p_2p_3} - \cdots
        $

      • 把欧拉公式的计算式展开,可以看到和这个式子是相等的

    • 所以,欧拉函数仅由 $n$ 和质因子决定,与次数无关。

    • 举例

      • $\varphi(12)=12 \times \frac{2-1}{2} \times \frac{3-1}{3}=4$
  • 试除法求欧拉函数

    • 代码

      •   #include <iostream>
          using namespace std;
          
          const int N = 1e9 + 10;
          
          int phi(int n) { //试除法求欧拉函数
          	int res = n;
          	for (int i = 2; i * i <= n; i++) {
          		if (n % i == 0) {
          			res = res / i * (i - 1);
          			while (n % i == 0) n /= i;
          		}
          	}
          	if (n > 1) res = res / n * (n - 1);
          	return res;
          }
          int main() {
          	int n;
          	cin >> n;
          	cout << phi(n) << endl;
          	return 0;
          }
        
    • 时间复杂度 $O(\sqrt{n})$

    • 举例

  • 筛法求欧拉函数

    • 求出1~n当中每个数的欧拉函数

    • 思路

      • 若 $i$ 是质数
        • $\operatorname{phi}[i]=i-1$
      • 在线性筛中, 每个合数 $m$ 都是被最小的质因子筛掉的。
        • 设 $p_j$ 是 $m$ 的最小质因子, 则 $m$ 通过 $m=p_j \times i$ 筛掉。
      • i%p[j]==0, 则 $i$ 包含了 $m$ 的所有质因子
        • $\varphi(m)=m \times \prod_{k=1}^S \frac{p_k-1}{p_k}=p_j \times i \times \prod_{k=1}^S \frac{p_k-1}{p_k}=p_j \times \varphi(i)$
        • 举例
          • $\varphi(12)=\varphi(2 \times 6)=2 \times \varphi(6)$
      • i%p[j]!=0, 则$i$和$p_j$互质
        • $\varphi(m)=\varphi\left(p_j \times i\right)=\varphi\left(p_j\right) \times \varphi(i)=\left(p_j-1\right) \times \varphi(i)$
        • 举例
          • $\varphi(75)=\varphi(3 \times 25)=(3-1) \times \varphi(25)$
    • 代码

      •   #include <iostream>
          using namespace std;
          
          const int N = 1000010;
          int p[N], vis[N], cnt;
          int phi[N];
          
          void get_phi(int n) { //筛法求欧拉函数
          	phi[1] = 1;
          	for (int i = 2; i <= n; i++) {
          		if (!vis[i]) {
          			p[cnt++] = i;
          			phi[i] = i - 1;
          		}
          		for (int j = 0; i * p[j] <= n; j++) {
          			int m = i * p[j];
          			vis[m] = 1;
          			if (i % p[j] == 0) {
          				phi[m] = p[j] * phi[i];
          				break;
          			} else
          				phi[m] = (p[j] - 1) * phi[i];
          		}
          	}
          }
          int main() {
          	int n;
          	cin >> n;
          	get_phi(n);
          	for (int i = 1; i <= n; i++)
          		printf("%d\n", phi[i]);
          	return 0;
          }
        
    • 举例

快速幂

  • 给你三个整数 $a, b, p$, 求 $a^b \bmod p$

  • 思路

    • $a^n=a \times a \times \cdots \times a$, 暴力的计算需要 $\mathrm{O}(\mathrm{n})$ 的时间。
    • 快速幂使用二进制拆分和倍增思想, 仅需要 $O(\log n)$ 的时间。
      • 对 $n$ 做二进制拆分
        • 例如, $3{13}=3=3^8 \cdot 3^4 \cdot 3^1$
      • 对 $a$ 做平方倍增,例如, $3^1, 3^2, 3^4, 3^8 \cdots \cdots$ 。
      • $\mathrm{n}$ 有 $\log \mathrm{n}+1$ 个二进制位, 我知道了 $a^1, a^2, a^4, a^8, \ldots, a{2{\operatorname{logn}}}$ 后, 只需要计算 $\log n+1$ 次乘法就可以了。
  • 快速幂可以应用在任何具有结合律的运算中, 例如,取模运算、矩阵乘法等。
    例如,
    $$
    \left(3^{13}\right) % p=\left(\left(3^8\right) % p \cdot\left(3^4\right) % p \cdot\left(3^1\right) % p\right) % p
    $$

  • 举例

  • 代码

    • #include <iostream>
      using namespace std;
      
      typedef long long LL;
      int a, b, p;
      
      int quickpow(LL a, int n, int p) {
      	int res = 1;
      	while (n) {
      		if (n & 1) res = res * a % p;
      		a = a * a % p;
      		n >>= 1;
      	}
      	return res;
      }
      int main() {
      	cin >> a >> b >> p;
      	int s = quickpow(a, b, p);
      	printf("%d^%d mod %d=%d\n", a, b, p, s);
      	return 0;
      }
      
  • 练习 洛谷 P1226

乘法逆元

  • 定义
    • 若 $a, b$ 互质,且满足同余方程 $a x \equiv 1(\bmod b)$, 则称 $\boldsymbol{x}$ 为 $\boldsymbol{a}$ 模 $\boldsymbol{b}$ 的乘法逆元,记作 $\boldsymbol{a}^{-1}$
    • 举例
      • $8 x \equiv 1(\bmod 5)$, 解得 $x=2,7,12 \ldots$

费马小定理

  • 定理

    • 若 $p$ 为质数,且 $a, p$ 互质,则 $a^{p-1} \equiv 1(\mod p)$
  • 举例

    • $4^{3-1} \equiv 1(\bmod 3)$
  • 证明

    • 构造一个与 $p$ 互质的序列 $A={1,2,3, \cdots, p-1}$,
    • 则 $a \times A_i(\bmod p)$ 的余数必是独一无二的
      • 原因
        • 假设 $a A_i(\bmod p)$ 与 $a A_j(\bmod p)$ 的余数都是 $r$ ,
        • 则 $a A_i=x p+r, a A_j=y p+r$,
        • 即 $a\left(A_i-A_j\right)=(x-y) p$ ,
        • 左边不是 $p$ 的倍数,而右边是 $p$ 的倍数,矛盾
    • 因为 $a \times A_i(\bmod p)p$,所以 $a \times A_i(\bmod p)$ 必与 $A_i(\bmod p)$ 的余数集合相同
    • 所以 $a^{p-1} \times \prod_{i=1}^{p-1} A_i \equiv \prod_{i=1}^{p-1} A_i(\bmod p)$
  • 举例:

    • $p=5, A={1,2,3,4}, a=2$ 。
    • 则 $a A={2,4,6,8}, a A % p={2,4,1,3}, A % p={1,2,3,4}$
  • 求乘法逆元

    • 给两个数 $a, p, p$ 是质数, 求 $a$ 模 $p$ 的乘法逆元

    • 由费马小定理
      $a^{p-1} \equiv 1(\bmod p)$, 得 $a \times a^{p-2} \equiv 1(\bmod p)$,

    • 所以 $a^{p-2}$ 就是 $a$ 模 $p$ 的乘法逆元。用快速幂求即可。

    • 代码

    #include<iostream>
    	using namespace std;
    
    typedef long long LL;
    	int a, p;
    
      int quickpow(LL a, int b, int p) {
    	int res = 1;
    	while (b) {
    		if (b & 1) res = res * a % p;
    		a = a * a % p;
    		b >>= 1;
    	}
    	return res;
    }
    int main() {
    	cin >> a >> p;
    	if (a % p)
    		printf("%d\n", quickpow(a, p - 2, p));
    	return 0;
    	}
    	```
    
    
    - 时间复杂度 $O(\log p)$
    
    

欧拉定理

  • 剩余类(同余类)

    • 给定一个正整数 $n$ ,把所有整数根据模 $n$ 的余数 $r \in [0, n-1]$ 分为 $n$ 类, 每一类表示为 $C_r=n x+r$ 的形
      式,这类数所构成的一个集合称为模 $n$ 的剩余类。
    • 例 $n=5, r=3$, 则 $C_3=5 x+3$ 为模 5 的一个剩余类。
  • 完全剩余系 (完系)

    • 给定一个正整数 $n$, 有 $n$ 个不同的模 $n$ 的剩余类,从这 $n$ 个不同的剩余类中各取出一个元素,总共 $n$ 个数,将这些数构成一个新的集合,则称这个集合为模 $n$ 的完全剩余系。
    • 例 $n=5$, 则 ${0,1,2,3,4}$ 是一个模 5 的完全剩余系, ${5,1,-3,8,9}$ 也是一个模 5 的完全剩余系。
  • 简化剩余系 (缩系)

    • 给定一个正整数 $n$, 有 $\varphi(n)$ 个不同的模 $n$ 的余数 $r$ 与 $n$ 互质的剩余类, 从这 $\varphi(n)$ 个剩余类中各取出一个元素, 总共 $\varphi(n)$ 个数,将这些数构成一个新的集合,则称这个集合为模 $n$ 的简化剩余系。
    • 例 $n=5$, 则 ${1,2,3,4}$ 是一个模 5 的简化剩余系, $n=10$, 则 ${1,3,7,9}$ 是一个模 10 的简化剩余系, 显然模 $n$ 的简化剩余系中所有的数都与 $n$ 互质。
  • 欧拉定理

    • 若 $\operatorname{gcd}(a, m)=1$, 则 $a^{\varphi(m)} \equiv 1(\bmod m)$

      • 当 $m$ 为质数时, 由于 $\varphi(m)=m-1$, 代入欧拉定理 可得到费马小定理 $a^{m-1} \equiv 1(\bmod m)$ 。
    • 举例

      • $
        \begin{aligned}
        & a=3, m=4 \text {, 则 } 3^{\varphi(4)} \equiv 3^2 \equiv 1(\bmod 4) \
        & a=3, m=5 \text {, 则 } 3^{\varphi(5)} \equiv 3^4 \equiv 1(\bmod 5)
        \end{aligned}
        $
  • 证明

    • 构造一个与 $m$ 互质的序列。
      • 设 $\left{r_1, r_2, \cdots, r_{\varphi(m)}\right}$ 是一个模 $m$ 的简化剩余系,则 $\left{a r_1, a r_2, \cdots, a r_{\varphi(m)}\right}$ 也是一个模 $m$ 的简化剩余系。
      • 所以 $\prod_{i=1}^{\varphi(m)} r_i \equiv \prod_{i=1}^{\varphi(m)} a r_i \equiv a^{\varphi(m)} \prod_{i=1}^{\varphi(m)} r_i(\bmod m)$,可约去 $\prod_{i=1}^{\varphi(m)} r_i$, 即得 $a^{\varphi(m)} \equiv 1(\bmod m)$ 。
  • 补充:

    • 费马小定理和欧拉定理

扩展欧拉定理

  • 扩展欧拉定理没有要求 $\operatorname{gcd}(a, m)=1$

  • $
    a^b \equiv\left{\begin{array}{lll}
    a^b, & b<\varphi(m), & (\bmod m) \
    a^{b \bmod \varphi(m)+\varphi(m)}, & b \geq \varphi(m), & (\bmod m)
    \end{array}\right.
    $

  • 举例

    • 如果用欧拉定理,则结果不对: $a=2, m=4, b=1, \quad 2^1 \neq 2^{1 \bmod 2+2}(\bmod 4)$
    • 如果用扩展欧拉定理,则结果正确: $a=2, m=4, b=6,2^6 \equiv 2^{6 \bmod 2+2}(\bmod 4)$
  • 题目 洛谷 P5091

    • 给你三个正整数 $a, m, b$, 你需要求: $a^b \bmod m$

    • 数据范围
      $$
      1 \leq \mathrm{a} \leq 10^9, \quad 1 \leq \mathrm{m} \leq 10^8, \quad 1 \leq \mathrm{b} \leq 10^{20000000}
      $$

  • 代码

    •   #include <iostream>
        using namespace std;
        
        typedef long long LL;
        int a, b, m, phi, flag;
        char s[20000005];
        
        int get_phi(int n) { //求欧拉函数
        	int res = n;
        	for (int i = 2; i * i <= n; i++) {
        		if (n % i == 0) {
        			res = res / i * (i - 1);
        			while (n % i == 0) n /= i;
        		}
        	}
        	if (n > 1) res = res / n * (n - 1);
        	return res;
        }
        int depow(int phi) { //降幂
        	int b = 0;
        	for (int i = 0; s[i]; i++) {
        		b = b * 10 + (s[i] - '0');
        		if (b >= phi) flag = 1, b %= phi;
        	}
        	if (flag) b += phi;
        	return b;
        }
        int qpow(LL a, int b) { //快速幂
        	int res = 1;
        	while (b) {
        		if (b & 1) res = res * a % m;
        		a = a * a % m;
        		b >>= 1;
        	}
        	return res;
        }
        int main() {
        	scanf("%d%d%s", &a, &m, s);
        	phi = get_phi(m);
        	b = depow(phi);
        	printf("%d", qpow(a, b));
        	return 0;
        }
      

公约数

求最大公约数

  • 性质

    • 如果 $d|a$,$d|b$,那么$d|a+b$,$d|ax+by$
  • 欧几里得算法(辗转相除法)

    • $gcd(a,b)=gcd(b,a \mod b)$
    • 证明
      • 设 $a>b, a % b=a-k b$ ,其中 $k=\left\lfloor\frac{a}{b}\right\rfloor$
      • (1) 若 $d$ 是 $(a, b)$ 的公约数,
        • 则 $d \mid a$ 且 $d \mid b$ , 则 $d \mid(a-k b)$,
        • 则 $d \mid(a % b)$ 故 $d$ 也是 $(b, a % b)$ 的公约数。
      • (2) 若 $d$ 是 $(b, a % b)$ 的公约数,
        • 则 $d \mid b$ 且 $d \mid(a-k b)$,
        • 则 $d|(a-k b+k b)=d| a$, 故 $d$ 也是 $(a, b)$ 的公约数。
      • 所以 $(a, b)$ 和 $(b, a % b)$ 的公约数是相同的,故最大公约数也相同。
    • $gcd(a,0)=a$
  • 代码

    •   LL gcd(LL a, LL b) {
        	return b == 0 ? a : gcd(b, a % b);
        }
      
  • 举例

    •   gcd(28,16)
        gcd(16,12)
        gcd(12,4)
        gcd(4,0)
        return 4
      
  • 欧几里得算法求 $\operatorname{gcd}(a, b)$ 如果代入负数, 结果会返回负数

    • 例: $\operatorname{gcd}(8,-4)=-4$
  • 时间复杂度是 $O(\log n)$

    • 当我们求 $\operatorname{gcd}(a, b)$ 的时候,会遇到两种情况:
      • $a<b$ ,这时候 $\operatorname{gcd}(a, b)=\operatorname{gcd}(b, a)$ ;
      • $a \geq b$ ,这时候 $\operatorname{gcd}(a, b)=\operatorname{gcd}(b, a \bmod b)$ ,而对 $a$ 取模会让 $a$ 至少折半。这意味着这一过程最多发生 $O(\log n)$ 次。
      • 第一种情况发生后一定会发生第二种情况,因此第一种情况的发生次数一定不多于第二种情况的发生次数。 从而我们最多递归 $O(\log n)$ 次就可以得出结果。
  • 练习 洛谷 P1029

    • 本题应满足三个约束条件:

      • $\operatorname{gcd}(p, q)=x$
      • $\operatorname{lcm}(p, q)=y$
      • $p q=\operatorname{gcd}(p, q) \operatorname{lcm}(p, q)=x y$
    • 证明:

      • 设 $p=a d, q=b d$, 则 $l c m(p, q)=a b d$
      • 因为 $p, q$ 乘积是定值, 所以枚举 $p$ 即可。
      • 因为 $p, q$ 是成对的, 所以枚举到 $\sqrt{x y}$ 即可。
      • 三个条件不是独立的,满足两条即可。
    • 代码

      •   #include <iostream>
          #include <cstring>
          #include <algorithm>
          using namespace std;
          
          typedef long long LL;
          LL x, y, ans;
          
          LL gcd(LL a, LL b) {
          	return b == 0 ? a : gcd(b, a % b);
          }
          int main() {
          	cin >> x >> y;
          	LL t = x * y;
          	for (LL i = 1; i * i <= t; i++)
          		if (t % i == 0 && gcd(i, t / i) == x)
          			ans += 2;
          	if (x == y) ans--;
          	cout << ans;
          	return 0;
          }
        
                     >
        

裴蜀定理

  • 裴蜀定理

    • 一定存在整数 $x, y$, 满足 $a x+b y=g c d(a, b)$
    • 举例
      • 例 $4 x+6 y=2$, 有整数解 $x=-1, y=1$ 。
      • 而 $4 x+6 y=3$, 即 $x=\frac{3-6 y}{4}$ 无整数解。
  • 证明

    • 设取整数 $x_0, y_0$ 时, $a x+b y$ 的最小正整数为 $s$ 即 $a x_0+b y_0=s$

    • 证明 $\operatorname{gcd}(a, b) \mid s$

      • 因 $\operatorname{gcd}(a, b)\left|a x_0, \operatorname{gcd}(a, b)\right| b y_0$,故 $\operatorname{gcd}(a, b) \mid s$
    • 证明 $s \mid \operatorname{gcd}(a, b)$

      • 设 $a=q s+r(0 \leq r<s)$


      • $$
        \begin{aligned}
        r & =a-q s \
        & =a-q\left(a x_0+b y_0\right) \
        & =a\left(1-q x_0\right)+b\left(-q y_0\right) \
        & =a x+b y
        \end{aligned}
        $$

      • 因 $\mathrm{s}$ 是最小正整数, 故 $r=0$

      • 所以 $s \mid a$,同理 $s \mid b$,

      • 故 $s \mid \operatorname{gcd}(a, b)$

    • 得 $s=\operatorname{gcd}(a, b)$

  • 推广1

    • 一定存在整数 $x, y$, 满足 $a x+b y=\operatorname{gcd}(a, b) \times n$
      • 例 $4 x+6 y=8$, 有整数解 $x=-4, y=4$ 。
  • 推广2

    • 一定存在整数 $X_1 \cdots X_i$, 满足 $\sum_{i=1}^n A_i X_i=\operatorname{gcd}\left(A_1, A_2, \cdots, A_n\right)$
      • 例 $4 x_1+6 x_2+2 x_3=4$, 有整数解 $x_1=1, x_2=0, x_3=0$ 。
  • 注意

    • 如果系数 $A_i$ 为负数, 求 $g c d$ 时结果可能为负。可代入其绝对值, 确保所求最大公约数为正数, 这样并不会影响解的存在。
  • 题目 洛谷 P4549

    • 给定一个包含 $\mathrm{n}$ 个元素的整数序列 $A$ ,记作 $A_1, A_2, \ldots, A_n$ 。 求另一个包含 $\mathrm{n}$ 个元素的待定整数序列 $X$,
      记 $S=\sum_{i=1}^n A_i \times X_i$, 使得 $S>0$ 且 $S$ 尽可能的小。

    • 代码

      •   #include<iostream>
          #include<cmath>
          using namespace std;
          
          int n, a, s;
          
          int gcd(int a, int b) {
          	return b == 0 ? a : gcd(b, a % b);
          }
          int main() {
          	cin >> n;
          	for (int i = 1; i <= n; i++) {
          		cin >> a;
          		s = gcd(s, abs(a));
          	}
          	cout << s;
          	return 0;
          }
        

扩展欧几里得算法

  • 解决裴蜀定理中 $x,y$ 的取值问题

    • 求 $a x+b y=g c d(a, b)$ 的一组整数解
  • 扩展欧几里得算法

    • 当 $b=0$ 时
      • $a x+b y=a$ 故而 $x=1, y=0$
    • 当 $b \neq 0$ 时
      • 由欧几里得算法, $\operatorname{gcd}(a, b)=\operatorname{gcd}(b, a % b)$
      • 由裴蜀定理,

    $$
    \begin{aligned}
    \operatorname{gcd}(a, b) & =a x+b y \
    \operatorname{gcd}(b, a % b) & =b x_1+(a % b) y_1 \
    & =b x_1+\left(a-\left[\frac{a}{b}\right] \times b\right) y_1 \
    & =a y_1+b\left(x_1-\frac{a}{b} y_1\right)
    \end{aligned}
    $$

    • 所以 $x=y_1, y=x_1-\frac{a}{b} y_1$
    • 可以用递归算法,先求出下一层的 $x_1, y_1$,
      再回代到上一层, 层层回代, 可求特解 $\left(x_0, y_0\right)$
    • 构造通解

    $$
    \left{\begin{array}{l}
    x=x_0+\frac{b}{\operatorname{gcd}(a, b)} * k \
    y=y_0-\frac{a}{\operatorname{gcd}(a, b)} * k
    \end{array} \right.
    $$

    • 举例

      • $8 x+6 y=2$, 解: $(1,-1),(4,-5),(7,-9) \cdots$
    • 代码

      •   int exgcd(int a, int b, int &x, int &y) {
          	if (b == 0) {
          		x = 1, y = 0;
          		return a;
          	}
          	int x1, y1, d;
          	d = exgcd(b, a % b, x1, y1);
          	x = y1, y = x1 - a / b * y1;
          	return d;
          }
        
  • 举例

  • 补充

    • 扩展欧几里得算法(喻邻溥)
  • 应用:求不定方程 $\boldsymbol{a x}+\boldsymbol{b} y=\boldsymbol{c}$ 的一组整数解

    • 若 $\operatorname{gcd}(a, b) \mid c$,则有整数解

      • 先用扩欧算法求 $a x+b y=\operatorname{gcd}(a, b)$ 的解 再乘以 $c / \operatorname{gcd}(a, b)$ , 即得原方程的特解 $\left(x_0, y_0\right)$
        • 例: $8 x+6 y=6$, 解得 $x=3, y=-3$
      • 构造通解
        • $\left{\begin{array}{l}x=x_0+\frac{b}{\operatorname{gcd}(a, b)} * k \ y=y_0-\frac{a}{\operatorname{gcd}(a, b)} * k\end{array} \quad\right.$
        • 考虑 $a x+b y=0$ 构造
    • 若 $\operatorname{gcd}(a, b) \nmid c$ ,则无整数解。

      • 例: $8 x+6 y=3, x=\frac{3-6 y}{8}$
    • 代码

      •   #include <iostream>
          #include <cstring>
          #include <algorithm>
          using namespace std;
          
          int exgcd(int a, int b, int &x, int &y) {
          	if (b == 0) {
          		x = 1, y = 0;
          		return a;
          	}
          	int x1, y1, d;
          	d = exgcd(b, a % b, x1, y1);
          	x = y1, y = x1 - a / b * y1;
          	return d;
          }
          int main() {
          	int a, b, c, x, y;
          	cin >> a >> b >> c;
          	int d = exgcd(a, b, x, y);
          	if (c % d == 0)
          		printf("%d %d", c / d * x, c / d * y);
          	else puts("none");
          	return 0;
          }
        
  • 应用:扩展欧几里得算法求同余方程

    • 问题

      • 如果 $x$ 存在整数解, 则输出任意一个; 如果不存在, 则输出 none
      • 例: $8 x \equiv 4(\bmod 6)$, 整数解 $x=2$
    • 思路

      • 把同余方程转化为不定方程
        • 由 $a x \equiv b(\bmod m)$
        • 得 $a x=m(-y)+b$
        • 即 $a x+m y=b$
      • 由裴蜀定理,当 $\operatorname{gcd}(a, m) \mid b$ 时 有解
      • 用扩欧算法 求 $a x+m y=\operatorname{gcd}(a, m)$ 的解 把 $x$ 乘以 $b / \operatorname{gcd}(a, m)$ 即得原方程的特解
    • 代码

      •   #include <iostream>
          #include <cstring>
          #include <algorithm>
          using namespace std;
          
          int exgcd(int a, int b, int &x, int &y) {
          	if (b == 0) {
          		x = 1, y = 0;
          		return a;
          	}
          	int x1, y1, d;
          	d = exgcd(b, a % b, x1, y1);
          	x = y1, y = x1 - a / b * y1;
          	return d;
          }
          int main() {
          	int a, b, m, x, y;
          	scanf("%d%d%d", &a, &b, &m);
          	int d = exgcd(a, m, x, y);
          	if (b % d == 0)
          		printf("%d", 1ll * x * b / d % m);
          	else puts("none");
          	return 0;
          }
        
  • 应用:扩展欧几里得算法求乘法逆元

    • 问题

      • $a$ 与 $m$ 互质时, 对于方程 $a x \equiv \mathbf{1}(\bmod m)$, 求 $a$ 的乘法逆元 $x \quad(0<\mathrm{x}<\mathrm{m})$
      • 例: $3 x \equiv 1(\bmod 4)$, 整数解 $x=3$
    • 思路

      • 乘法逆元转化为不定方程 等价变形 $a x+m y=1$
      • 扩欧算法 求 $a x+m y=\operatorname{gcd}(a, m)$ 的解 $x$
      • $(x % m+m) % m$ 即答案
        • 这一操作可以保证得到最小正整数
          • 例: $x=-7, m=5$, 则 $(-7 % 5+5) % 5=3$
          • 例: $x=7, m=5$ ,则 $(7 % 5+5) % 5=2$
    • 代码

      • #include <iostream>
        #include <cstring>
        #include <algorithm>
        using namespace std;
            
        int exgcd(int a,int b,int &x,int &y){
          if(b == 0){
            x = 1, y = 0; return a;
          }
          int x1, y1, d;
          d = exgcd(b, a%b, x1, y1);
          x = y1, y = x1-a/b*y1;
          return d;
        }
        int main(){
          int a, m, x, y;
          scanf("%d%d", &a, &m);
          exgcd(a, m, x, y);
          printf("%d", (x%m+m)%m);
          return 0;
        }
        
    • 时间复杂度 $O(\log\max(a,b))$

中国剩余定理

  • 问题

    • $
      \left{\begin{array}{c}
      x \equiv r_1\left(\bmod m_1\right) \
      x \equiv r_2\left(\bmod m_2\right) \
      \vdots \
      x \equiv r_n\left(\bmod m_n\right)
      \end{array}\right.
      $
      其中模数 $m_1, m_2, \cdots, m_n$ 为两两互质的整数, 求 $x$ 的最小非负整数解。
  • 来源:

    • 《孙子算经》
      • 有物不知其数,三三数之剩二,五五数之剩三, 七七数之剩二。问物几何?
  • 中国剩余定理 (Chinese Remainder Theorem, CRT)

    1. 计算所有模数的积 $M$
    2. 计算第 $i$ 个方程的 $c_i=\frac{M}{m_i}$
    3. 计算 $c_i$ 在模 $m_i$ 意义下的逆元 $c_i^{-1}$
    4. $x=\sum_{i=1}^n r_i c_i c_i^{-1}(\bmod M)$
  • 证明

    • 先证明 $x=\sum_{i=1}^n r_i c_i c_i^{-1}$ 满足每个 $x \equiv r_i\left(\bmod m_i\right)$ 。

      • 当 $i \neq j$ 时, $c_j \equiv 0\left(\bmod m_i\right)$, 则 $c_j c_j^{-1} \equiv 0\left(\bmod m_i\right)$

      • 当 $i=j$ 时, $c_i \not \equiv 0\left(\bmod m_i\right)$, 则 $c_i c_i^{-1} \equiv 1\left(\bmod m_i\right)$

        • $
          \begin{aligned}
          & x \equiv \sum_{j=1}^n r_j c_j c_j^{-1}\left(\bmod m_i\right) \
          & \equiv r_i c_i c_i^{-1} \quad\left(\bmod m_i\right) \
          & \equiv r_i \quad\left(\bmod m_i\right) \
          &
          \end{aligned}
          $
    • 再看对 $x=\sum_{i=1}^n r_i c_i c_i^{-1}$ 模 $M$ 后,是不是对每个式子成立

      • $\sum_{i=1}^n r_i c_i c_i^{-1}(\bmod M)$ 对 $m_i$ 来说,只是减去了 $m_i$ 的若干倍, 不影响余数 $r_i$ 。证毕
  • 题目 洛谷 P1495

    • 代码

      •   #include <iostream>
          #include <cstring>
          #include <algorithm>
          using namespace std;
          
          typedef long long LL;
          LL n, a[11], b[11];
          
          LL exgcd(LL a, LL b, LL &x, LL &y) {
          	if (b == 0) {
          		x = 1, y = 0;
          		return a;
          	}
          	LL d, x1, y1;
          	d = exgcd(b, a % b, x1, y1);
          	x = y1, y = x1 - a / b * y1;
          	return d;
          }
          LL CRT(LL m[], LL r[]) {
          	LL M = 1, ans = 0;
          	for (int i = 1; i <= n; i++) M *= m[i];
          	for (int i = 1; i <= n; i++) {
          		LL c = M / m[i], x, y;
          		exgcd(c, m[i], x, y);
          		ans = (ans + r[i] * c * x % M) % M;
          	}
          	return (ans % M + M) % M;
          }
          int main() {
          	scanf("%lld", &n);
          	for (int i = 1; i <= n; ++i)
          		scanf("%lld%lld", a + i, b + i);
          	printf("%lld\n", CRT(a, b));
          	return 0;
          }
        
    • 时间复杂度

      • $O(n \log c)$

扩展中国剩余定理

  • 问题

    • 求解线性同余方程组
      $
      \left{\begin{array}{c}
      x \equiv r_1\left(\bmod m_1\right) \
      x \equiv r_2\left(\bmod m_2\right) \
      \cdots \
      x \equiv r_n\left(\bmod m_n\right)
      \end{array}\right.
      $
      其中 $m_1, m_2, \cdots, m_n$ 为不一定两两互质的整数,求 $x$ 的最小非负整数解。
  • 中国剩余定理 (CRT) 已不可行

    • 其构造解 $x=\sum_{i=1}^n r_i c_i c_i^{-1}(\bmod M)$
    • 其中 $c_i x \equiv 1\left(\bmod m_i\right)$ , 即 $c_i x+m_i y=1=\operatorname{gcd}\left(c_i, m_i\right)$
    • 根据裴蜀定理, $c_i, m_i$ 应该互质, $c_i=\frac{m_1 \times \cdots \times m_n}{m_i}$
    • 如果 $c_i, m_i$ 不互质, 则 $c_i^{-1}$ 不存在, 算法失效
  • 扩展中国剩余定理

    • 前两个方程可以转化为不定方程
      • $x \equiv r_1\left(\bmod m_1\right), x \equiv r_2\left(\bmod m_2\right)$
      • 转化为不定方程
        • $x=m_1 p+r_1=m_2 q+r_2$
      • 移项,得不定方程
        • $m_1 p-m_2 q=r_2-r_1$
      • 由裴蜀定理,可知
      • 当 $\operatorname{gcd}\left(m_1, m_2\right) \nmid\left(r_2-r_1\right)$ 时,无解
      • 当 $\operatorname{gcd}\left(m_1, m_2\right) \mid\left(r_2-r_1\right)$ 时,有解
      • 在有解的情况下,由扩欧算法先求出前两个方程的解
      • 特解 $p=p * \frac{r_2-r_1}{g c d}, q=q * \frac{r_2-r_1}{g c d}$
      • 通解 $P=p+\frac{m_2}{g c d} * k, Q=q-\frac{m_1}{g c d} * k$
      • 由 $P,Q$ 可以构造 $x$
        • $x=m_1 P+r_1=\frac{m_1 m_2}{g c d} * k+m_1 p+r_1$
      • 那么,前两个方程等价合并为一个方程 $x \equiv r(\bmod m)$
      • 其中 $r=m_1 p+r_1, \quad m=\frac{m_1 m_2}{gcd}=\operatorname{lcm}\left(m_1, m_2\right)$
    • 所以 $n$ 个同余方程只要合并 $n-1$ 次,即可求解
  • 代码

    •   #include <iostream>
        #include <cstring>
        #include <algorithm>
        using namespace std;
        
        typedef __int128 LL;
        const int N = 100005;
        LL n, m[N], r[N];
        
        LL exgcd(LL a, LL b, LL &x, LL &y) {
        	if (b == 0) {
        		x = 1, y = 0;
        		return a;
        	}
        	LL d, x1, y1;
        	d = exgcd(b, a % b, x1, y1);
        	x = y1, y = x1 - a / b * y1;
        	return d;
        }
        LL EXCRT(LL m[], LL r[]) {
        	LL m1, m2, r1, r2, p, q;
        	m1 = m[1], r1 = r[1];
        	for (int i = 2; i <= n; i++) {
        		m2 = m[i], r2 = r[i];
        		LL d = exgcd(m1, m2, p, q);
        		if ((r2 - r1) % d) {
        			return -1;
        		}
        		p = p * (r2 - r1) / d; //特解
        		p = (p % (m2 / d) + m2 / d) % (m2 / d);
        		r1 = m1 * p + r1;
        		m1 = m1 * m2 / d;
        	}
        	return (r1 % m1 + m1) % m1;
        }
        int main() {
        	scanf("%lld", &n);
        	for (int i = 1; i <= n; ++i)
        		scanf("%lld%lld", m + i, r + i);
        	printf("%lld\n", EXCRT(m, r));
        	return 0;
        }
      

线性代数

线性方程组

  • 一般形式

    • $
      \begin{aligned}
      & \mathrm{a}{11} \mathrm{x}1+\mathrm{a} \mathrm{x}2+\mathrm{a} \mathrm{x}3+\cdots+\mathrm{a}{1 \mathrm{n}} \mathrm{x}{\mathrm{n}}=\mathrm{b}1 \
      & \mathrm{a}
      \mathrm{x}1+\mathrm{a} \mathrm{x}2+\mathrm{a} \mathrm{x}3+\cdots+\mathrm{a}{2 \mathrm{n}} \mathrm{x}{\mathrm{n}}=\mathrm{b}2 \
      & \cdots \cdots \
      & \mathrm{a}
      1} \mathrm{x}1+\mathrm{a} 2} \mathrm{x}2+\mathrm{a} 3} \mathrm{x}3+\cdots+\mathrm{a}{\mathrm{nn}} \mathrm{x}
      {\mathrm{n}}=\mathrm{b}_{\mathrm{n}}
      \end{aligned}
      $
  • 矩阵形式

    • $
      \left[\begin{array}{ccccc}
      a_{11} & a_{12} & a_{13} & \ldots & a_{1 n} \
      a_{21} & a_{22} & a_{23} & \ldots & a_{2 n} \
      \vdots & \vdots & \vdots & & \vdots \
      a_{n 1} & a_{n 2} & a_{n 3} & \ldots & a_{n n}
      \end{array}\right] \times\left[\begin{array}{c}
      x_1 \
      x_2 \
      \vdots \
      x_n
      \end{array}\right]=\left[\begin{array}{c}
      b_1 \
      b_2 \
      \vdots \
      b_n
      \end{array}\right]
      $

    • 即 $A X=B$。

      • $A$ 是 $n \times n$ 矩阵, $X$ 是 $n \times 1$ 矩阵, $B$ 是 $n \times 1$ 矩阵
  • 增广矩阵

    • $
      \left[\begin{array}{lccccr}
      a_{11} & a_{12} & a_{13} & \ldots & a_{1 n} & b_1 \
      a_{21} & a_{22} & a_{23} & \ldots & a_{2 n} & b_2 \
      \vdots & \vdots & \vdots & & \vdots & \vdots \
      a_{n 1} & a_{n 2} & a_{n 3} & \ldots & a_{n n} & b_n
      \end{array}\right]
      $
  • 矩阵的初等行变换

    • 交换两行
    • 把某一行乘一个非 0 的数
    • 把某行的若干倍加到另一行上去

高斯消元法

  • 思想

    • 先把系数矩阵消成上三角矩阵, 再从下到上回代求解
  • 流程

    • 枚举主元, 找到主元下面系数不是0的一行
      • 有时候为了保证精度,会选择主元绝对值最大的行
    • 把这一行与主元行交换
    • 把主元系数变成1
    • 把主元下面的系数变成 0
  • 举例

  • 解的三种情况

    • 唯一解

      • $i$ 可以枚举完 $n$ 行

      • $
        \left[\begin{array}{llll}
        1 & 1 & 1 & 6 \
        0 & 1 & 2 & 8 \
        0 & 0 & 1 & 3
        \end{array}\right]
        $

    • 无解

      • $a[i][i]=0, b \neq 0$

      • $
        \left[\begin{array}{llll}
        1 & 1 & 1 & 6 \
        0 & 1 & 2 & 3 \
        0 & 0 & 0 & 3
        \end{array}\right] \begin{aligned}
        & x_2+2 x_3=3 \
        & x_2+2 x_3=6
        \end{aligned}
        $

    • 无穷多解

      • $a[i][i]=0, b=0$

      • $
        \left.\left[\begin{array}{llll}
        1 & 1 & 1 & 6 \
        0 & 1 & 2 & 3 \
        0 & 0 & 0 & 0
        \end{array}\right] \begin{array}{r}
        x_2+2 x_3=3 \
        2 x_2+4 x_3=6
        \end{array}\right]
        $

  • 代码

    •   #include <iostream>
        #include <algorithm>
        #include <cmath>
        using namespace std;
        
        const int N = 110;
        const double eps = 1e-6;
        int n;
        double a[N][N]; //增广矩阵
        
        int gauss() {
        	for (int i = 1; i <= n; ++i) { //第i主元
        		for (int k = i; k <= n; ++k) //换非0行
        			if (fabs(a[k][i]) > eps) {
        				swap(a[k], a[i]);
        				break;
        			}
        		if (fabs(a[i][i]) < eps) return 0;
        
        		for (int j = n + 1; j >= i; j--) //变1
        			a[i][j] /= a[i][i];
        		for (int k = i + 1; k <= n; k++) //变0
        			for (int j = n + 1; j >= i; j--)
        				a[k][j] -= a[k][i] * a[i][j];
        	}
        	for (int i = n - 1; i >= 1; i--) //回代
        		for (int j = i + 1; j <= n; j++)
        			a[i][n + 1] -= a[i][j] * a[j][n + 1];
        	return 1;             //存在唯一解
        }
        int main() {
        	scanf("%d", &n);
        	for (int i = 1; i <= n; i++)
        		for (int j = 1; j <= n + 1; j++)
        			scanf("%lf", &a[i][j]);
        	if (gauss())
        		for (int i = 1; i < n + 1; i++)
        			printf("%.2lf\n", a[i][n + 1]);
        	else puts("No Solution");
        	return 0;
        }
      
  •  // 高斯消元法补充
      #include<bits/stdc++.h>
      #define maxn 1005
      #define INF 2147483647
      #define eps (1e-15)
      
      using namespace std;
      
      int n, ans;
      double a[maxn][maxn];
      
      int gauss(){
      	int r=0, c=0;
      	for(c=0, r=0;c<n;c++){
      		int t=r;
      		for(int i=r;i<n;i++){
      			if(fabs(a[i][c])>fabs(a[t][c])){
      				t=i;
      			}
      		}
      		if(fabs(a[t][c])<eps){ continue; }
      		for(int i=c;i<=n;i++) swap(a[t][i], a[r][i]);
      		for(int i=n;i>=c;i--) a[r][i]/=a[r][c];
      		for(int i=r+1;i<n;i++){
      			if(fabs(a[i][c])>eps){
      				for(int j=n;j>=c;j--){
      					a[i][j]-=a[r][j]*a[i][c];
      				}
      			}
      		}
      		r++;
      	}
      	
      	if(r<n){
      		for(int i=r;i<n;i++){
      			if(fabs(a[i][n])>eps){
      				return -1;
      			}
      		}
      		return 0;
      	}
      	
      	for(int i=n-1;i>=0;i--){
      		for(int j=i+1;j<n;j++){
      			a[i][n]-=a[i][j]*a[j][n];
      		}
      	}
      	return 2147483647;
      }
      
      int main()
      {
      	cin>>n;
      	for(int i=0;i<n;i++){
      		for(int j=0;j<=n;j++){
      			cin>>a[i][j];
      		}
      	}
      	
      	ans=gauss();
      	if(ans==2147483647){
      		for(int i=0;i<n;i++){
      			if(a[i][n]==0) printf("x%d=0\n", i+1);
      			else printf("x%d=%.2lf\n", i+1, a[i][n]);
      		}	
      		return 0;
      	}
      	
      	cout<<ans;
      	return 0;
      }
    
  • 练习 洛谷 P3389

高斯约旦消元法

  • 思路

    • 先把系数矩阵消成主对角矩阵, 再除以主元
  • 流程

    • 每轮循环, 主元所在行不变, 主元所在列消成 0
  • 举例

  • 代码

    •   #include <iostream>
        #include <algorithm>
        #include <cmath>
        using namespace std;
        
        const int N = 110;
        const double eps = 1e-6;
        int n;
        double a[N][N]; //增广矩阵
        
        bool Gauss_Jordan() {
        	for (int i = 1; i <= n; ++i) { //第i主元
        		for (int k = i; k <= n; ++k) //换非0行
        			if (fabs(a[k][i]) > eps) {
        				swap(a[k], a[i]);
        				break;
        			}
        		if (fabs(a[i][i]) < eps) return 0;
        
        		for (int k = 1; k <= n; ++k) { //对角化
        			if (k == i) continue;
        			for (int j = n + 1; j >= i; --j)
        				a[k][j] -= a[k][i] / a[i][i] * a[i][j];
        		}
        	}
        	for (int i = 1; i <= n; ++i)
        		a[i][n + 1] /= a[i][i]; //除以主元
        	return 1;                 //存在唯一解
        }
        int main() {
        	scanf("%d", &n);
        	for (int i = 1; i <= n; i++)
        		for (int j = 1; j <= n + 1; j++)
        			scanf("%lf", &a[i][j]);
        	if (Gauss_Jordan())
        		for (int i = 1; i <= n; i++)
        			printf("%.2lf\n", a[i][n + 1]);
        	else puts("No Solution");
        }
      

矩阵求逆

  • 逆矩阵

    • 对于方阵 $A$, 若存在方阵 $B$, 使得 $A \times B=B \times A=I$, 则称 $B$ 为 $A$ 的逆矩阵, 记为 $A^{-1}$ 。

      • 单位矩阵 $I$

        • $$
          \left[\begin{array}{ccccc}
          1 & \cdots & 0 & \cdots & 0 \
          0 & \cdots & 1 & \cdots & 0 \
          0 & \cdots & 0 & \cdots & 1
          \end{array}\right]
          $$
  • 给出 $n$ 阶方阵 $A$, 求解其逆矩阵 $A^{-1}$ 的方法

      1. 构造 $n \times 2 n$ 的矩阵 $(A, I)$
      2. 用高斯-约旦消元法将其化简为 $\left(I, A^{-1}\right)$, 即可得到 $A$ 的逆矩阵 $A^{-1}$ 。
    • 证明:
      • $A X=I$ 可以看做 $n$ 组 $n$ 元线性方程组的矩阵形式
      • 增广矩阵的初等行变换不改变解的值
        • 对 $A X=I$ 两边同乘以 $A^{-1}$,得 $A^{-1}(A X)=A^{-1} I$,即$I X=A^{-1}$ 和 $A X=I$ 的解是一致的。
        • 只要对增广矩阵 $(A,I)$ 进行初等行变换成,使得左边的 $A$ 变成 $I$,右边的增广部分就是 $A^{-1}$ 。
    • 如果左部出现了全 0 行, 则矩阵 $A$ 不可逆。
  • 代码

    •   #include<iostream>
        #include<cstdio>
        #include<cmath>
        #define LL long long
        using namespace std;
        
        const int N = 405, P = 1e9 + 7;
        int n;
        LL a[N][N << 1];
        
        LL quickpow(LL a, LL b) {
        	LL ans = 1;
        	while (b) {
        		if (b & 1) ans = ans * a % P;
        		a = a * a % P;
        		b >>= 1;
        	}
        	return ans;
        }
        bool Gauss_Jordan() {
        	for (int i = 1; i <= n; ++i) { //枚举主元的行列
        		int r = i;
        		for (int k = i; k <= n; ++k) //找非0行
        			if (a[k][i]) {
        				r = k;
        				break;
        			}
        		if (r != i) swap(a[r], a[i]); //换行
        		if (!a[i][i]) return 0;
        
        		int x = quickpow(a[i][i], P - 2); //求逆元
        		for (int k = 1; k <= n; ++k) { //对角化
        			if (k == i) continue;
        			int t = a[k][i] * x % P;
        			for (int j = i; j <= 2 * n; ++j)
        				a[k][j] = ((a[k][j] - t * a[i][j]) % P + P) % P;
        		}
        		for (int j = 1; j <= 2 * n; ++j) //除以主元
        			a[i][j] = (a[i][j] * x % P);
        	}
        	return 1;
        }
        int main() {
        	scanf("%d", &n);
        	for (int i = 1; i <= n; ++i)
        		for (int j = 1; j <= n; ++j)
        			scanf("%lld", &a[i][j]), a[i][i + n] = 1;
        	if (Gauss_Jordan())
        		for (int i = 1; i <= n; ++i) {
        			for (int j = n + 1; j <= 2 * n; ++j)
        				printf("%lld ", a[i][j]);
        			puts("");
        		} else puts("No Solution");
        	return 0;
        }
      
  • 练习 洛谷 P4783

组合数学

组合数的计算

  • 递推

    • 问题

      • 有 $q(q \leq 10000)$ 组询问, 每组询问两个整数 $n, m(1 \leq m \leq n \leq 2000)$, 求 $C_n^m \bmod \left(10^9+7\right)$ 的值。
    • 思路

      • $C_nm=C_{n-1}m+C_{n-1}^{m-1}$
      • $C_n^m$ 构成了杨辉三角
    • 代码

      •   #include <iostream>
          using namespace std;
          
          const int N = 2010, P = 1e9 + 7;
          int C[N][N];
          
          void init() {
          	for (int i = 0; i < N; i++) C[i][0] = 1;
          	for (int i = 1; i < N; i++)
          		for (int j = 1; j <= i; j++)
          			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % P;
          }
          int main() {
          	init();
          	int T, n, m;
          	cin >> T;
          	while (T--) {
          		cin >> n >> m;
          		printf("%d\n", C[n][m]);
          	}
          	return 0;
          }
        
    • 时间复杂度 $O(n^2)$

  • 快速幂

    • 问题

      • 有 $q(q \leq 10000)$ 组询问, 每组询问两个整数 $n, m\left(1 \leq m \leq n \leq 10^5\right)$, 求 $C_n^m \bmod \left(10^9+7\right)$ 的值。
    • 思路

      • 用 $C_n^m=\frac{n !}{(n-m) ! m !}$ 直接计算。
      • 开两个数组分别存模意义下的阶乘和阶乘的逆元
        • 用 $f[x]$ 存 $x !(\bmod p)$ 的值,
        • 用 $g[x]$ 存 $(x !)^{-1}(\bmod p)$ 的值。
      • 因为 $p$ 是质数且 $n, m$ 都小于 $p$, 即 $n, m$ 与 $p$ 互质,所以 根据费马小定理 $a \cdot a^{p-2} \equiv 1(\bmod p)$,可以用 快速幂 求逆元。
      • 查询 $C_n^m(\bmod p)=f[n] * g[n-m] * g[m](\bmod p)$
    • 代码

      •   #include <iostream>
          using namespace std;
          
          typedef long long LL;
          const int N = 100010, P = 1e9 + 7;
          LL f[N], g[N];
          
          LL qpow(LL a, int b) {
          	LL res = 1;
          	while (b) {
          		if (b & 1) res = res * a % P;
          		a = a * a % P;
          		b >>= 1;
          	}
          	return res;
          }
          void init() {
          	f[0] = g[0] = 1;
          	for (int i = 1; i < N; i++) {
          		f[i] = f[i - 1] * i % P;
          		g[i] = g[i - 1] * qpow(i, P - 2) % P;
          	}
          }
          LL getC(LL n, LL m) {
          	return f[n] * g[m] % P * g[n - m] % P;
          }
          int main() {
          	init();
          	int q, n, m;
          	cin >> q;
          	while (q--) {
          		cin >> n >> m;
          		printf("%lld\n", getC(n, m));
          	}
          	return 0;
          }
        
    • 时间复杂度 $O(n\log P)$

  • 卢卡斯定理

    • 问题

      • 给定整数 $n, m, p$ 的值, 求出 $C_n^m(\bmod p)$ 的值。 其中 $1 \leq m \leq n \leq 10^{18}, 1 \leq p \leq 10^5$ ,保证 $p$ 为质数。
    • 卢卡斯定理

      • $C_n^m \equiv C_{n / p}^{m / p} C_{n \bmod p}^{m \bmod p}(\bmod p)$ ,其中 $p$ 为质数。

        • $n \bmod p$ 和 $m \bmod p$ 一定是小于 $p$ 的数, 可以直接求解
        • $C_{n / p}^{m / p}$ 可以继续用 Lucas 定理求解。
      • 边界条件

        • 当 $m=0$ 时, 返回 1 。
      • 证明

        • 引理1

          • ${C}_p^x \equiv {0} (\mod {p}), \quad 0<x<p$
            • 证明
              • 因 $C_p^x=\frac{p !}{x !(p-x) !}=\frac{p(p-1) !}{x(x-1) !(p-x) !}=\frac{p}{x} C_{p-1}^{x-1}$,故 $C_p^x \equiv p \cdot \operatorname{inv}(x) C_{p-1}^{x-1} \equiv 0(\bmod p)$ 。
        • 引理2

          • $(1+x)^p \equiv 1+x^p(\bmod p)$
            • 证明
              • 由二项式定理 $(1+x)p=\sum_{i=0}p C_p^i x^i$,由引理1知, 只剩 $i=0, p$ 两项, 得证。
        • 令 $n=a p+b, m=c p+d$

        • 一方面

          • $ (1+x)^n \equiv \sum_{i=0}^n C_n^i x^i \quad (\mod p) $
        • 另一方面

          • $
            \begin{aligned}
            & (1+x)^n \equiv(1+x)^{a p+b} \
            & \equiv\left((1+x)p\right)a \cdot(1+x)^b \
            & \equiv\left(1+xp\right)a \cdot(1+x)^b \
            & \equiv \sum_{i=0}^a C_a^i x^{i p} \cdot \sum_{j=0}^b C_b^j x^j \quad (\mod p) \
            &
            \end{aligned}
            $
        • 由对应次幂的系数相等,可得

          • $C_n^m \equiv C_a^c C_b^d(\bmod p)$
        • 即 $C_n^m \equiv C_{n / p}^{m / p} C_{n \bmod p}^{m \bmod p}(\bmod p)$ 。

    • 代码

      •   #include <iostream>
          using namespace std;
          
          typedef long long LL;
          const int N = 100010;
          LL f[N], g[N];
          
          LL qpow(LL a, int b, int p) {
          	LL res = 1;
          	while (b) {
          		if (b & 1) res = res * a % p;
          		a = a * a % p;
          		b >>= 1;
          	}
          	return res;
          }
          void init(int p) {
          	f[0] = g[0] = 1;
          	for (int i = 1; i <= p; i++) {
          		f[i] = f[i - 1] * i % p;
          		g[i] = g[i - 1] * qpow(i, p - 2, p) % p;
          	}
          }
          LL getC(int n, int m, int p) {
          	return f[n] * g[m] * g[n - m] % p;
          }
          int lucas(LL n, LL m, int p) {
          	if (m == 0) return 1;
          	return lucas(n / p, m / p, p) * getC(n % p, m % p, p) % p;
          }
          int main() {
          	int q, n, m, p;
          	cin >> q;
          	while (q--) {
          		cin >> n >> m >> p;
          		init(p);
          		printf("%d\n", lucas(n + m, n, p));
          	}
          	return 0;
          }
        
    • 时间复杂度 $O\left(p \log p+\log _p n\right)$

    • 练习 洛谷 P3807

隔板法

  • 应用

    • 求线性不定方程的整数解的组数
    • 求相同元素分组的方案数
  • 正整数和的组数

    • 问题:
      • 若 $x_i \geq 1$, 求 $x_1+x_2+\cdots+x_k=n$ 的整数解的组数。
      • 现有 $n$ 个 完全相同 的元素, 将其分为 $k$ 组,保证每 组至少有一个元素, 一共有多少种分法?
    • 答案
      • 把 $n$ 个相同的球排成一行, 有 $n-1$ 个空。 拿 $k-1$ 块板子插入到 $n-1$ 个空里, 把球分成 $k$ 组 即在 $n-1$ 个空里选择 $k-1$ 个空插板子, 所以答案 就是 $C_{n-1}^{k-1}$。
  • 非负整数和的组数

    • 问题:
      • 若 $x_i \geq 0$, 求 $x_1+x_2+\cdots+x_k=n$ 的整数解的组数。
    • 答案
      • 令 $y_i=x_i+1$, 则 $y_i \geq 1$,则 $y_1+y_2+\cdots+y_k=n+k=m$,转化为情况1, 所以答案是 $C_{m-1}{k-1}=C_{n+k-1}$
  • 不同下界整数和的组数

    • 问题:
      • 求 $x_1+x_2+\cdots+x_k=n$ 的整数解的组数, 其中 $x_i \geq a_i \geq 0, \quad \sum a_i \leq n_0$
    • 答案
      • 令 $y_i=x_i-a_i+1$, 则 $y_i \geq 1$,则 $y_1+y_2+\cdots+y_k=n-\sum a_i+k=m$,转化为情况1, 所以答案为 $C_{m-1}{k-1}=C_{n-\sum} a_i+k-1$
  • 练习 洛谷 P1771

容斥原理

  • 集合的并

    • $$
      \left|\bigcup_{i=1}^n S_i\right|=\sum_{m=1}n(-1) \sum_{a_i<a i+1}\left|\bigcap_{i=1}^m S_{a_i}\right|
      $$
  • 集合的交

    • $$
      \left|\bigcap_{i=1}^n S_i\right|=|U|-\left|\bigcup_{i=1}^n \overline{S_i}\right|
      $$

卡特兰数

  • 从格点 $(0,0)$ 走到格点 $(n, n)$, 只能向右或向上走, 并且不能越过 对角线的路径的条数,就是卡特兰数,记为 $H_n$ 。

  • 求解

    • 先求路径总数, 在 $2 n$ 次移动中选 $n$ 次向右移动, 即 $C_{2 n}^n$ 。
    • 再求非法路径, 即越过对角线的路径。
      • 把 $y=x+1$ 这条线画出来, 碰到即说明是一条非法路径。所有的非法路径与这条线有至少一个交点, 把第一个交点设为 $(a, a+1)$。
      • 把 $(a, a+1)$ 之后的路径全部按照 $y=x+1$ 这条线对称过去, 这样, 最后 的终点就会变成 $(n-1, n+1)$ 。
      • 所有非法路径对称后都唯一对应着一条到 $(n-1, n+1)$ 的路径,
      • 所以非法路径数就是 $C_{2 n}^{n-1}$
    • 合法路径数就是 $C_{2 n}^n-C_{2 n}^{n-1}$ 。
  • 卡特兰数的相关问题

    • 两种操作A,B,要求任意时刻B的次数不能超过A的次数
      • 有 $2 n$ 个人排成一行进入剧场。入场费 5 元。其中只有 $n$ 个人有一张 5 元钞票,另外 $n$ 人只有 10 元钞票,剧院无其它钞票,问有多少种方法使得只要有 10 元的人买票,售票处就有 5 元的 钞票找零?
      • 一个有 $n$ 个 0 和 $n$ 个 1 组成的字串, 且所有的前缀字串皆满足 1 的个 数不超过 0 的个数。这样的字串个数有多少?
      • 包含 $n$ 组括号的合法运算式的个数有多少?
      • 一个栈的进栈序列为 $1,2,3, \cdots, n$, 有多少个不同的出栈序列?
    • 图形划分
      • 在圆上选择 $2 n$ 个点, 将这些点成对连接起来使得所得到的 $n$ 条弦不相 交的方法数?
      • 通过连结顶点而将 $n+2$ 边的凸多边形用$n-1$条对角线分隔成互不重叠的三角形,有多少种方案?
  • 卡特兰数的通项公式

    • $H_n=C_{2 n}^n-C_{2 n}^{n-1}$

    • $H_n=\frac{1}{n+1} C_{2 n}^n$

    • $H_n=\frac{4 n-2}{n+1} H_{n-1}$

    • $H_n= \begin{cases}\sum_{i=1}^n H_{i-1} H_{n-i} & n \geq 2, n \in \mathbf{N}_{+} \ 1 & n=0,1\end{cases}$

                                                                        >
      
  • 代码

    •   #include <iostream>
        using namespace std;
        
        int n;
        long long f[20];
        
        int main() {
        	cin >> n;
        	f[0] = 1;
        	for (int i = 1; i <= n; i++)
        		f[i] = f[i - 1] * (4 * i - 2) / (i + 1);
        	cout << f[n] << endl;
        	return 0;
        }
      
  • 练习 洛谷 P1044

博弈论

概念

  • 公平组合游戏
    • 公平组合游戏(Impartial Combinatorial Game, ICG) 是满足以下特征的一类问题:
      • (1) 有两个玩家,游戏规则对两人是公平的;
      • (2) 游戏的状态有限, 能走的步数也有限;
      • (3) 两人轮流走步, 一个玩家不能走步时,游戏结束;
      • (4) 游戏的局势不能区分玩家身份, 像围棋这样有黑、白两方的游戏, 就不属于此类 问题。
    • ICG 问题有一个特征:给定初始局势, 并且指定先手玩家, 如果双方都采取最优策略, 那么获胜者就已经确定了。也就是说, ICG 问题存在必胜策略。
  • 必败态、必胜态
    • P-Position(必败态):当前玩家的先手必败状态
    • N-Position(必胜态):当前玩家的先手必胜状态

Bash游戏

  • 问题

    • 有 $n$ 颗石子, 甲先取, 乙后取, 每次可以拿 $1 \sim m$ 颗石子, 轮流拿下去, 拿 到最后一颗的人获胜。
  • 分析

    • (1) 如果 $n %(m+1)=0$, 即 $n$ 是 $m+1$ 的整数倍, 那么不管甲拿多少, 如 $k$ 个, 乙都拿 $m+1-k$ 个, 使剩下的永远是 $m+1$ 的整数倍, 直到最后的 $m+1$ 个, 所以后拿的乙一定赢。
    • (2) 如果 $n %(m+1) \neq 0$, 即 $n$ 不是 $m+1$ 的整数倍, 还有余数 $r$, 那么甲拿走 $r$ 个, 剩 下的是 $m+1$ 的倍数, 这样就转移到了情况 (1), 相当于甲乙互换, 结果是甲赢。
  • 扩展:如果取石子的数量不是1~m而是只能在一个集合中选择,例如${1,4}$

    • 如果当前状态经过一步,全部到达必胜态,则当前是状态是必败态。(状态x=5)

    • 如果当前状态经过一个,可以到达一个必败态,则当前状态是必胜态。(状态x=6,8)

    • pos的值是周期变化的,周期为5。

Nim游戏

  • 问题

    • 地上有 $n$ 堆石子, 甲、乙两人交替取石子。每人每次只能从任意一堆石子里面 取, 至少取 1 枚, 不能不取。最后没石子可取的人就输了。假如甲是先手, 且已知每堆石子的数量 $a_i$, 问 是否存在先手必胜的策略。
  • 结论

    • 若初态为必胜态 $\left(a_1 \wedge a_2 \wedge \cdots \wedge a_n \neq 0\right)$ ,则先手必胜;
    • 若初态为必败态 $\left(a_1 \wedge a_2 \wedge \cdots \wedge a_n=0\right)$, 则先手必败。
  • 定理1:必胜态的后继状态至少存在一个必败态

    • 证明:
      • 设 $a_1 \wedge \cdots \wedge a_n=s \neq 0$, 设 $s$ 的二进制位为 1 的最高位是第 $k$ 位, 则 $a_1, \cdots, a_n$ 一定有奇数个 $a_i$ 的二进制的第 $\mathrm{k}$ 位为 1 。
        用 $a_i{ }^{\wedge} s$ 去替换 $a_i$(因为 $a_i^{\wedge} s<a_i$, 所以是合法的替换), 则

    $
    \begin{aligned}
    & a_1{ }^{\wedge} \cdots \wedge a_i{ }^{\wedge} s^{\wedge} \cdots \wedge a_n \
    =&a_1 \wedge \cdots^{\wedge} a_i \wedge \cdots \wedge a_n \wedge s \
    =&s^{\wedge} s=0
    \end{aligned}
    $

  • 定理2: 必败态的后继状态均为必胜态

    • 证明:
      • 设 $a_1{ }^{\wedge} \cdots \wedge a_n=0$, 则相同位上 1 的个数为偶数。此时, 无论减少 哪个数, 都会使得异或和 $\neq 0$ 。
      • 必胜态与必败态交替出现, 终态 $(0,0, \ldots, 0)$ 是必败态。
  • 练习 洛谷 P2197

  • 练习 洛谷 P1247

SG函数

TODO