抽屉原理
往3个抽屉里放一支铅笔, 必然会有一个抽屉中铅笔的数量大于等于2.
更一般地, 向n个抽屉中分配m支铅笔, 所有抽屉中铅笔的最大值大于等于m / n. 也叫鸽巢原理.
应用
- 对于任意 个整数 , 必然存在一个连续子段,其和是 的倍数. 这是因为我们对这个数组模n再做前缀和, 前缀和有 种取值(注意 ), 所以必然有相同的两项, 则这两项之间的子段和模n为0, 即为n的倍数.
- 奇偶性: 在二维平面上给你 5 个整点(坐标都是整数), 证明其中必有一对点, 它们的中点也是整点. 注意到模 2 意义下的取值只有 4 种(奇偶, 偶奇, 奇奇, 偶偶), 却有 5 个点, 故一定有两个点的(各个维度的)奇偶性都相同, 故这两个点的中点必然是整点.
- 存在性证明: 证明某种解一定存在, 从而可以写暴力/贪心解决该问题.
- 优化搜索空间: 看似范围很大, 实则可以根据抽屉原理减小搜索范围.
容斥原理
更一般地,
即
// CSES-Prime Multiplesvoid solution() { ll n; int k; cin >> n >> k; vector<ll> arr(k); for (int i = 0; i < k; i++) { cin >> arr[i]; } int limit = 1 << k; ll ret = 0; for (int i = 1; i < limit; i++) { i128 now = 1; bool ok = 1; for (int j = 0; j < k; j++) { if ((i >> j) & 1) { now *= arr[j]; } if (now > n) { ok = 0; break; } } int cnt = __builtin_popcount(i); int sig = (cnt % 2 == 0) ? -1 : 1; if (ok) { ret += sig * (n / now); } } cout << ret << '\n';}斐波那契数列
常规求法, 利用倍增递推式或者矩阵快速幂可以优化到. 倍增递推式:
pair<int, int> fib(int n) { if (n == 0) { return { 0, 1 }; } auto p = fib(n >> 1); int c = p.first * (2 * p.second - p.first); int d = p.first * p.first + p.second * p.second; if (n % 2 == 1) { return { d, c + d }; } else { return { c, d }; }}常见性质
- 卡西尼性质:
- gcd性质:
- 相邻项互质, 斐波那契数列的相邻项是辗转相除法最差的起始情况.
- 前缀和性质:
- 奇偶项求和:
- 加法规则:
这可以引出: 对于任意正整数k, 是的倍数(反过来也成立, 若是的倍数, 则一定是的倍数).
齐肯多夫定理与斐波那契编码
任意正整数n都能表示成若干不连续的斐波那契数的和. 于是我们可以使用这个性质给正整数编码. 即n表示为. 注意这里末尾手动添加一个1, 这样会强制出现两个连续的1, 表示编码结束. 若为1表示被使用, 否则表示未使用.
给n编码的过程可以使用贪心算法:
- 从大到小找到最大的, 添加1, 从n中减去.
- 继续往下找, 添加0, 否则添加1并减去. 直到为0.
- 在末尾添加1表示结束.
解码过程同理.
Pisano周期
斐波那契数列模任意正整数m下会形成周期数列. 证明如下: 我们知道斐波那契数列只要确定了相邻两项, 就能确定后续所有项. 模m意义下任意相邻两项只会有个取值, 所以对于前个相邻项来说, 必然会有两个相同的相邻项. 那么从这两项出发形成的斐波那契数列相同, 即形成周期, 最小正周期长度小于等于.
Pisano证明了这个周期数列的最小正周期长度不会超过6m, 称为Pisano周期. OEIS A001175
二项式系数(组合数)的计算
杨辉三角
C(n, m) % p = (C(n - 1, m) + C(n - 1, m - 1)) % p, 没什么好说的, 复杂度.
mint _C[N][N];constexpr int N = 2026;void init_pascal() { static bool inited = 0; if (inited) { return; } for (int i = 0; i < N; i++) { _C[i][0] = _C[i][i] = 1; for (int j = 1; j < i; j++) { _C[i][j] = _C[i - 1][j] + _C[i - 1][j - 1]; } } inited = 1;}mint nCr(ll n, ll m) { if (m < 0 || m > n) { return 0; } init_pascal(); return _C[n][m];}线性预处理逆元
- 求出前
n个整数的阶乘和阶乘逆元fact[n + 1], factinv[n + 1]. - 使用定义
C(n, m) = fact[n] * factinv[m] * factinv[n - m]即可.
模数应为大于n的素数, 否则无法保证逆元存在.
复杂度
constexpr int N = 2e5 + 50;ll MOD = 998244353;vector<ll> fact, factinv;void init_fact() { static bool inited = 0; if (inited) { return; } int n = N; inited = 1; ll p = MOD; fact.resize(n + 1); factinv.resize(n + 1); fact[0] = 1; for (int i = 1; i <= n; i++) { fact[i] = mul(fact[i - 1], i, p); } factinv[n] = inv(fact[n], p); for (int i = n - 1; i >= 0; i--) { factinv[i] = mul(factinv[i + 1], i + 1, p); } inited = 1;}
ll nCr(ll n, ll m) { if (m < 0 || m > n) { return 0; } ll p = MOD; init_fact(); ll ret = fact[n]; ret = mul(ret, factinv[m], p); ret = mul(ret, factinv[n - m], p); return ret;}直接计算
注意到C(n, m) = _fact[n] / (_fact[m] * _factinv[n - m]) = (n * (n - 1) * ... * (n - m + 1)) / (m * (m - 1) * ... * 1)(上面m项, 下面m项). 所以只需要的复杂度即可直接计算. 在n很大, m很小的时候很好用.
ll MOD = 998244353;ll nCr(ll n, ll m, ll p = MOD) { if (m < 0 || m > n) { return 0; } if (m > n / 2) { m = n - m; } ll A = 1; ll B = 1; for (ll i = 0; i < m; i++) { A = mul(A, n - i, p); B = mul(B, m - i, p); } return mul(A, inv(B, p), p);}Lucas定理
C(n, m) % p = C(n / p, m / p) * C(n % p, m % p). 在p较小的时候有用.
ll Lucas(ll n, ll m, ll p) { if (m == 0) { return 1; } return mul(Lucas(n / p, m / p, p), nCr(n % p, m % p, p), p); // 通常使用预处理版本的nCr, 注意这里的nCr要求在m > n时返回0, 别忘记了.}高精度求组合数
直接用BigInt乘超级慢. 这里介绍一种使用Legendre定理优化的方法.
- 先筛n以下的质数.
- 注意到
C(n, m) = fact[n] / (fact[m] * fact[n - m]. 且fact[n]中一定包含且只包含所有小于等于n的质数作为质因数. 我们枚举小于等于n的质数p, 处理出fact[n]中p的幂次, 减去fact[m]中的幂次, 再减去fact[n - m]中的幂次, 即为C(n, m)中质数p的幂次. - 快速得到
fact[i]的幂次可以使用Legendre定理, 即fact[i]中p的幂次为i / p + i / p^2 + ... + i / p^k. 这是个有限项的求和.
vector<int> primes;int N = 2e6 + 50;void sieve(int n = N) { // 埃氏筛或线性筛}ll legendre(ll n, ll p) { // 返回 n! 中, p的幂次 ll ret = 0; while (n > 0) { ret += n / p; n /= p; } return ret;}BigInt nCr(ll n, ll m) { if (m < 0 || m > n) { return 0; } BigInt ret = 1; for (int i = 0; primes[i] <= n; i++) { int p = primes[i]; int exp = legendre(n, p) - legendre(m, p) - legendre(n - m, p); ret *= BigInt(p).pow(exp); } return ret;}概率和期望
本章涵盖概率dp和期望dp的内容.
期望的线性性
计算期望最有用的工具, 比如计算某集合中满足某性质的子集的元素个数的期望是多少. 可以设定指示器变量(若在中为1, 否则为0). 求元素个数期望即转化为求. 由于期望的线性型, 我们可以把拆开, 转化成求, 设为在中的概率, . 这时候对求和即为所求结果.
例题: 牛客126319F
简单来记的话就是”概率dp正推, 期望dp倒推”. 这是因为算概率的时候通常需要计算从起点到某状态的可能性, 而算期望的时候通常计算从当前状态到终点的期望代价.
概率的转移(概率dp)
我们把状态看作点(每个状态拥有一个状态概率), 状态的转移看作有向边, 终点为, 每一条有向边赋予一个转移概率(流量). 所有初始状态的概率和为1. 若存在终止状态, 则所有终止状态的概率和也为1. 每个节点的所有转移概率的和为1. 大致可以分成以下三类.
DAG上
假设当前在点, 状态概率为. 对于每条出边概率有. 同理可以倒过来得到等于所有入边概率乘以前驱的状态概率.
带自环的DAG上
假设当前点状态概率为, 自环转移概率为, 对于除自环外的出边, 来说, 我们需要对所有概率进行归一化, 即所有出边的概率都要除以, 这时候就消除了自环. (很好理解, 因为如果小于1, 那就不可能一直走自环, 总有一天会走到另外的出边上). 所以. 注意到这种情况是不能倒过来用前驱来计算当前节点的, 因为每条入边归一化的分母都不一样(应为前驱节点的, 而非本节点的).
可能带环的有向图
需要比较复杂的算法, 待补充.
期望的转移(期望dp)
DAG上
先计算终点期望, 然后反推到起点.
对于每条出边y, p:
其中 是从 转移到 的概率, 是这次转移的代价.
带自环的DAG上
假设点 有 的概率走自环(代价 ).有 的概率走到 (代价 ):
移项化简可得: 注: 若每步代价均为 1, 则公式简化为 .
可能带环的有向图
也就是马尔科夫链. 需要高斯消元, 待补充.
trick合集
- 任何一个排列, 都可以被唯一地分解为若干个”极大连续递增块”的拼接.