试除法判定质数

  1. bool is_prime(int x)
  2. {
  3. if (x < 2) return false;
  4. for (int i = 2; i <= x / i; i ++ )
  5. if (x % i == 0)
  6. return false;
  7. return true;
  8. }

试除法分解质因数

  1. void divide(int x)
  2. {
  3. for (int i = 2; i <= x / i; i ++ )
  4. if (x % i == 0)
  5. {
  6. int s = 0;
  7. while (x % i == 0) x /= i, s ++ ;
  8. cout << i << ' ' << s << endl;
  9. }
  10. if (x > 1) cout << x << ' ' << 1 << endl;
  11. cout << endl;
  12. }

朴素筛法求素数

  1. int primes[N], cnt; // primes[]存储所有素数
  2. bool st[N]; // st[x]存储x是否被筛掉
  3. void get_primes(int n)
  4. {
  5. for (int i = 2; i <= n; i ++ )
  6. {
  7. if (st[i]) continue;
  8. primes[cnt ++ ] = i;
  9. for (int j = i + i; j <= n; j += i)
  10. st[j] = true;
  11. }
  12. }

线性筛法求素数

  1. int primes[N], cnt; // primes[]存储所有素数
  2. bool st[N]; // st[x]存储x是否被筛掉
  3. void get_primes(int n)
  4. {
  5. for (int i = 2; i <= n; i ++ )
  6. {
  7. if (!st[i]) primes[cnt ++ ] = i;
  8. for (int j = 0; primes[j] <= n / i; j ++ )
  9. {
  10. st[primes[j] * i] = true;
  11. if (i % primes[j] == 0) break;
  12. }
  13. }
  14. }

试除法求所有约数

  1. vector<int> get_divisors(int x)
  2. {
  3. vector<int> res;
  4. for (int i = 1; i <= x / i; i ++ )
  5. if (x % i == 0)
  6. {
  7. res.push_back(i);
  8. if (i != x / i) res.push_back(x / i);
  9. }
  10. sort(res.begin(), res.end());
  11. return res;
  12. }

约数个数和约数之和

  1. 如果 N = p1^c1 * p2^c2 * ... *pk^ck
  2. 约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
  3. 约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)

欧几里得算法

  1. int gcd(int a, int b)
  2. {
  3. return b ? gcd(b, a % b) : a;
  4. }

求欧拉函数

  1. int phi(int x)
  2. {
  3. int res = x;
  4. for (int i = 2; i <= x / i; i ++ )
  5. if (x % i == 0)
  6. {
  7. res = res / i * (i - 1);
  8. while (x % i == 0) x /= i;
  9. }
  10. if (x > 1) res = res / x * (x - 1);
  11. return res;
  12. }

筛法求欧拉函数

  1. int primes[N], cnt; // primes[]存储所有素数
  2. int euler[N]; // 存储每个数的欧拉函数
  3. bool st[N]; // st[x]存储x是否被筛掉
  4. void get_eulers(int n)
  5. {
  6. euler[1] = 1;
  7. for (int i = 2; i <= n; i ++ )
  8. {
  9. if (!st[i])
  10. {
  11. primes[cnt ++ ] = i;
  12. euler[i] = i - 1;
  13. }
  14. for (int j = 0; primes[j] <= n / i; j ++ )
  15. {
  16. int t = primes[j] * i;
  17. st[t] = true;
  18. if (i % primes[j] == 0)
  19. {
  20. euler[t] = euler[i] * primes[j];
  21. break;
  22. }
  23. euler[t] = euler[i] * (primes[j] - 1);
  24. }
  25. }
  26. }

快速幂

  1. m^k mod p,时间复杂度 O(logk)。
  2. int qmi(int m, int k, int p)
  3. {
  4. int res = 1 % p, t = m;
  5. while (k)
  6. {
  7. if (k&1) res = res * t % p;
  8. t = t * t % p;
  9. k >>= 1;
  10. }
  11. return res;
  12. }

扩展欧几里得算法

  1. // 求x, y,使得ax + by = gcd(a, b)
  2. int exgcd(int a, int b, int &x, int &y)
  3. {
  4. if (!b)
  5. {
  6. x = 1; y = 0;
  7. return a;
  8. }
  9. int d = exgcd(b, a % b, y, x);
  10. y -= (a/b) * x;
  11. return d;
  12. }

高斯消元

  1. // a[N][N]是增广矩阵
  2. int gauss()
  3. {
  4. int c, r;
  5. for (c = 0, r = 0; c < n; c ++ )
  6. {
  7. int t = r;
  8. for (int i = r; i < n; i ++ ) // 找到绝对值最大的行
  9. if (fabs(a[i][c]) > fabs(a[t][c]))
  10. t = i;
  11. if (fabs(a[t][c]) < eps) continue;
  12. for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]); // 将绝对值最大的行换到最顶端
  13. for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c]; // 将当前行的首位变成1
  14. for (int i = r + 1; i < n; i ++ ) // 用当前行将下面所有的列消成0
  15. if (fabs(a[i][c]) > eps)
  16. for (int j = n; j >= c; j -- )
  17. a[i][j] -= a[r][j] * a[i][c];
  18. r ++ ;
  19. }
  20. if (r < n)
  21. {
  22. for (int i = r; i < n; i ++ )
  23. if (fabs(a[i][n]) > eps)
  24. return 2; // 无解
  25. return 1; // 有无穷多组解
  26. }
  27. for (int i = n - 1; i >= 0; i -- )
  28. for (int j = i + 1; j < n; j ++ )
  29. a[i][n] -= a[i][j] * a[j][n];
  30. return 0; // 有唯一解
  31. }

递推法求组合数

  1. // c[a][b] 表示从a个苹果中选b个的方案数
  2. for (int i = 0; i < N; i ++ )
  3. for (int j = 0; j <= i; j ++ )
  4. if (!j) c[i][j] = 1;
  5. else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

通过预处理逆元的方式求组合数

  1. 首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
  2. 如果取模的数是质数,可以用费马小定理求逆元
  3. int qmi(int a, int k, int p) // 快速幂模板
  4. {
  5. int res = 1;
  6. while (k)
  7. {
  8. if (k & 1) res = (LL)res * a % p;
  9. a = (LL)a * a % p;
  10. k >>= 1;
  11. }
  12. return res;
  13. }
  14. // 预处理阶乘的余数和阶乘逆元的余数
  15. fact[0] = infact[0] = 1;
  16. for (int i = 1; i < N; i ++ )
  17. {
  18. fact[i] = (LL)fact[i - 1] * i % mod;
  19. infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
  20. }

Lucas定理

  1. p是质数,则对于任意整数 1 <= m <= n,有:
  2. C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)
  3. int qmi(int a, int k, int p) // 快速幂模板
  4. {
  5. int res = 1 % p;
  6. while (k)
  7. {
  8. if (k & 1) res = (LL)res * a % p;
  9. a = (LL)a * a % p;
  10. k >>= 1;
  11. }
  12. return res;
  13. }
  14. int C(int a, int b, int p) // 通过定理求组合数C(a, b)
  15. {
  16. if (a < b) return 0;
  17. LL x = 1, y = 1; // x是分子,y是分母
  18. for (int i = a, j = 1; j <= b; i --, j ++ )
  19. {
  20. x = (LL)x * i % p;
  21. y = (LL) y * j % p;
  22. }
  23. return x * (LL)qmi(y, p - 2, p) % p;
  24. }
  25. int lucas(LL a, LL b, int p)
  26. {
  27. if (a < p && b < p) return C(a, b, p);
  28. return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
  29. }

分解质因数法求组合数

  1. 当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
  2. 1. 筛法求出范围内的所有质数
  3. 2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! p的次数是 n / p + n / p^2 + n / p^3 + ...
  4. 3. 用高精度乘法将所有质因子相乘
  5. int primes[N], cnt; // 存储所有质数
  6. int sum[N]; // 存储每个质数的次数
  7. bool st[N]; // 存储每个数是否已被筛掉
  8. void get_primes(int n) // 线性筛法求素数
  9. {
  10. for (int i = 2; i <= n; i ++ )
  11. {
  12. if (!st[i]) primes[cnt ++ ] = i;
  13. for (int j = 0; primes[j] <= n / i; j ++ )
  14. {
  15. st[primes[j] * i] = true;
  16. if (i % primes[j] == 0) break;
  17. }
  18. }
  19. }
  20. int get(int n, int p) // 求n!中的次数
  21. {
  22. int res = 0;
  23. while (n)
  24. {
  25. res += n / p;
  26. n /= p;
  27. }
  28. return res;
  29. }
  30. vector<int> mul(vector<int> a, int b) // 高精度乘低精度模板
  31. {
  32. vector<int> c;
  33. int t = 0;
  34. for (int i = 0; i < a.size(); i ++ )
  35. {
  36. t += a[i] * b;
  37. c.push_back(t % 10);
  38. t /= 10;
  39. }
  40. while (t)
  41. {
  42. c.push_back(t % 10);
  43. t /= 10;
  44. }
  45. return c;
  46. }
  47. get_primes(a); // 预处理范围内的所有质数
  48. for (int i = 0; i < cnt; i ++ ) // 求每个质因数的次数
  49. {
  50. int p = primes[i];
  51. sum[i] = get(a, p) - get(b, p) - get(a - b, p);
  52. }
  53. vector<int> res;
  54. res.push_back(1);
  55. for (int i = 0; i < cnt; i ++ ) // 用高精度乘法将所有质因子相乘
  56. for (int j = 0; j < sum[i]; j ++ )
  57. res = mul(res, primes[i]);

卡特兰数

:::tips 给定n个0和n个1,它们按照某种顺序排成长度为2n的序列
满足任意前缀中0的个数都不少于1的个数的序列的数量为:
Cat(n) = C(2n, n) / (n + 1) :::

NIM游戏

:::tips 给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。 :::

公平组合游戏ICG

:::tips 若一个游戏满足:

由两名玩家交替行动;
在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。 :::

有向图游戏

:::tips 给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。 :::

Mex运算

:::tips 设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S :::

SG函数

:::tips 在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。 :::

有向图游戏的和

:::tips 设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm) :::

定理

:::tips 有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。 :::