数论整理

1 同余

1.

\(a \equiv b \pmod m\),当且仅当\(m \mid (a - b)\)

2.同加性:

\(a \equiv b \pmod m\),则\(a + c \equiv b + c \pmod m\)

3.同乘性:

\(a \equiv b \pmod m\),则\(a * c \equiv b * c \pmod m\)\(a \equiv b \pmod m, c \equiv d \pmod m\),则\(a * c \equiv b * d \pmod m\)

4.

不满足同除性,但若\(gcd(c, m) = 1\),则当\(a * c \equiv b * c \pmod m\)时,有\(a \equiv b \pmod m\)

5.同幂性:

\(a \equiv b \pmod m\),则\(a ^ n \equiv b ^ n \pmod m\)

6.推论1:

\(a * b \bmod k = (a \bmod k) * (b \bmod k) \bmod k\)

7.推论2:

\(p, q\)互质,且\(a \bmod p = x\)\(a \bmod q = x\),则\(a \bmod (p * q) = x\)

2 素数

2.1 有关素数的定理

1.唯一分解定理:任何一个大于1的正整数都能被唯一分解为有限个素数的乘积,可写作:\(N = {p_1} ^ {c_1}{p_2} ^ {c _ 2} \cdots {p _ m} ^ {c _ m}\), 其中\({c_i}\)都是正整数,\({p _ i}\)都是素数且满足\({p_1} < {p_2} < \cdots <{p_m}\)

2.N中最多只能含有一个大于\(\sqrt{N}\)的因子

3.分解质因数:试除法:枚举\(2 \sim \sqrt{n}\),遇到质因子就除净并且记录质因子的个数。如果最后\(n > 1\),这就说明这就是大于\(\sqrt{n}\)的因子

int n, a[10001];  //质因子的个数 
inline void decompose(int x) {  //分解质因数 
	for (rg int i = 2; i * i <= x; i++) {
		while (x % i == 0) {
			a[i]++;
			x /= i;
		}
	}
	if (x > 1) a[x]++;
}

2.2 素数判定

1.按照素数的定义模拟

inline bool is_prime(int x) {
	if (x == 0 || x == 1) return false;
	for (rg int i = 2; i * i <= x; i++) {
		if (x % i == 0) return false;
	}
	return true;
}

2.埃式筛法

int prime[N], cnt;
bool is_prime[N];
inline void Eratosthenes(int n) {
	is_prime[0] = is_prime[1] = false;
	for (rg int i = 2; i <= n; i++) {
		is_prime[i] = true;
	}
	for (rg int i = 2; i <= n; i++) {
		if (is_prime[i]) {
			prime[++cnt] = i;
			if ((long long)i * i > n) continue;
			for (rg int j = i * i; j <= n; j += i) {
				is_prime[j] = false;
			}
		}
	}
}

3.线性筛法(欧拉筛法):每个合数只被它最小的质因子筛掉

int vis[N];  //划掉合数
int prime[N];  //记录质数
int cnt;  //质数个数
inline void get_prime(int n) {
	for (rg int i = 2; i <= n; i++) {
		if (!vis[i]) prime[++cnt] = i;  //如果当前数没被划掉,必定是质数,记录 
		for (rg int j = 1; 1ll * i * prime[j] <= n; j++) {  //枚举已记录的质数,如果合数越界则中断 
			vis[i * prime[j]] = true;  //划掉合数 
			if (i % prime[j] == 0) break;  //控制每个合数只被它的最小素数因子筛掉一次 
		}
	}
}

2.3 欧拉函数

1.定义:\(1 \sim n\)中与\(n\)互质的数的个数,用字母\(\varphi\)表示。

\(\varphi(n) = n \times \prod _ {i = 1} ^ {k} (1 - \frac{1}{p _ i})\),其中\(p_1, p_2, \cdots, p_k\)为n的所有质因数。

2.性质:

(1)\(\varphi(1) = 1\)

(2)若p是素数,则\(\varphi(p) = p - 1\)

(3)若p是素数,则\(\varphi(p ^ k) = (p - 1) \times p ^ {k - 1}\)

(4)欧拉函数为积性函数:$\forall a, b \in N^* \(,且\)gcd(a, b) = 1\(,则\)\varphi(a \times b) = \varphi(a) \times \varphi(b)\(。特别的,对于奇数n,\)\varphi(2n) = \varphi(n)$

3.欧拉函数性质变形:

(1)p为素数,若n % p \(= 0\),则\(\varphi(n \times p) = p \times \varphi(n)\)。(\(n \times p\)的素数因子和n是一样的,所以用欧拉函数公式把\(\varphi(n \times p)\)展开)

(2)p为素数,若n % p \(\ne 0\),则\(\varphi(n \times p) = (p - 1) \times \varphi(n)\)。(n和p互质,满足积性函数)

(3)与n互质的数都是成对出现的,且每对的和为n,所以大于2的数的\(\varphi(n)\)都为偶数。

4.求欧拉函数:

(1)试除法:如果只要求一个数的欧拉函数值,那么直接根据定义,在质因数分解的同时求解

inline int euler_phi(int n) {
	rg int ans = n;
	for (rg int i = 2; i * i <= n; i++) {
		if (n % i == 0) {
			ans = ans / i * (i - 1);
			while (n % i == 0) n /= i;  //把这个因数除尽
		}
	}
	if (n > 1) ans = ans / n * (n - 1);
	return ans;
}


(2)筛法:在线性筛中,每一个合数都是被最小的质因子筛掉。比如设\(p_1\)是n的最小质因子,\(i = \frac{n}{p_1}\),那么线性筛的过程中n通过\(i \times p_1\)筛掉。

如果\(i \bmod p_1 = 0\),那么i包含了n的所有质因子:

\[\begin{aligned}\varphi(n) = n \times \prod_{i= 1}^s{\frac{p_i - 1}{p_i}} = p_1 \times i \times \prod_{i = 1}^s{\frac{p_i - 1}{p_i}}& = p_1 \times \varphi(i)\end{aligned} \]

那如果\(i \bmod p_1 \ne 0\)呢,这时i和\(p_1\)是互质的,根据欧拉函数性质,我们有:

\(\begin{aligned}\varphi(n) & = \varphi(p_1) \times \varphi(i) = (p_1 - 1) \times \varphi(i) \end{aligned}\)

int p[N];  //存质数
int cnt;
int phi[N];
bool not_prime[N];  //存合数
inline void get_phi(int n) {
	phi[1] = 1;
	for (rg int i = 2; i <= n; i++) {
		if (!not_prime[i]) {  //是质数
			p[++cnt] = i;
			phi[i] = i - 1;
		}
		for (rg int j = 1; i * p[j] <= n; j++) {
			rg int m = i * p[j];
			not_prime[m] = 1;  //标记为合数
			if (i % p[j] == 0) {
				phi[m] = p[j] * phi[i];
				break;
			} else {
				phi[m] = (p[j] - 1) * phi[i];
			}
		}
	}
}

2.4 线性筛求约数个数与约数和

1.约数个数:

\(d_i\)表示i的约数个数,\(num_i\)表示i的最小质因子出现次数。

\(n = \prod_{i = 1}^m {p_i} ^ {c_i}\),则\(d_i = \prod_ {i = 1}^m (c_i + 1)\)

int prime[N], cnt;
bool not_prime[N];
int d[N];  //约数个数 
int num[N];  //最小质因数的个数 
int n;
inline void get_prime(int n) {
	d[1] = 1;
	for (rg int i = 2; i <= n; i++) {
		if (!not_prime[i]) {  //i是质数 
			prime[++cnt] = i;
			d[i] = 2;  //1和i 
			num[i] = 1;  //i自己,共1个 
		}
		for (rg int j = 1; i * prime[j] <= n; j++) {
			rg int m = i * prime[j];
			not_prime[m] = true;
			if (i % prime[j] == 0) {
				d[m] = d[i] / (num[i] + 1) * (num[i] + 2);
				num[m] = num[i] + 1;
				break;
			} else {
				d[m] = d[i] * 2;
				num[m] = 1;
			}
		}
	}
}

2.约数和:\(sd_i\)表示i的约数和,\(num_i\)表示最小质因子的\(p_0 + p_1 + \cdots + p_k\)

\(sd_i = (p_1^0 + p_2^1 + \cdots + p_1^{r_1}) * (p_2^0 + p_2^1 + \cdots + p_2^{r_2}) * \cdots* (p_k^0 + p_k^1 + \cdots + p_k ^ {r _ k})= \prod_{i = 1}^k \sum_{j = 0}^{r_k} {p_i ^ j}\)

分治求等比数列的和:\(s = a^0 + a^1 + a^2 + \cdots + a^n\)

当n为奇数时:\(s = (1 + a^{\frac{n}{2} + 1}) * (a^0 + a^1 + \cdots + a^{\frac{n}{2}})\)

当n为偶数时:\(s = (1 + a^{\frac{n}{2} + 1}) * (a^0 + a^1 + \cdots + a^{\frac{n}{2} - 1}) + a^{\frac{n}{2}}\)

int prime[N], cnt;
bool not_prime[N];
int num[N];
int sd[N];
int n;
inline void get_primes(int n) {
	sd[1] = 1;
	for (rg int i = 2; i <= n; i++) {
		if (!not_prime[i]) {
			sd[i] = num[i] = i + 1;
			prime[++cnt] = i;
		}
		for (rg int j = 1; i * prime[j] <= n; j++) {
			rg int m = i * prime[j];
			not_prime[m] = true;
			if (i % prime[j]) {
				sd[m] = sd[i] * sd[prime[j]];  //积性函数
				num[m] = prime[j] + 1; 
			} else {
				sd[m] = sd[i] / num[i] * (num[i] * prime[j] + 1);
				num[m] = num[i] * prime[j] + 1;
				break;
			}
		}
	}
}

2.5 欧拉定理/费马小定理

1.费马小定理:若p为素数,\(\gcd(a, p) = 1\),则\(a^{p - 1} \equiv 1 \pmod p\)

另:对于任意整数a,有\(a ^ p \equiv a \pmod p\)

2.欧拉定理:若\(gcd(a, m) = 1\),则\(a^{\varphi(m)} \equiv 1 \pmod m\)

3.扩展欧拉定理:

\(a^b \equiv \begin{cases} a^{b \bmod \varphi(m)}, &\gcd(a,m) = 1, \\ a^b, &\gcd(a,m)\ne 1, b < \varphi(m), \\ a^{(b \bmod \varphi(m)) + \varphi(m)}, &\gcd(a,m)\ne 1, b \ge \varphi(m). \end{cases} \pmod m\)

4.秦九昭算法:

\[\begin{aligned} f(x) &= a _ n x ^ n + a _ {n - 1} x ^ {n - 1} + \cdots + a _ 1 x + a_0 \\ &= (a_n x^{n - 1} + a_{n - 1} x^{n - 2} + \cdots + a_2 x + a_1)x + a_0 \\ &= ((a_n x ^ {n - 2} + a_{n - 1} x ^ {n - 3} + \cdots + a _ 3 x + a _ 2) x + a_1) x + a_0 \\ &\vdots \\ &= (\cdots((a_n x + a _ {n - 1}) x + a _ {n - 2} ) x + \cdots + a_1)x + a_0 \end{aligned} \]

求多项式的值时,首先计算最内层括号内一次多项式的值,即:

\[\begin{aligned} v_0 = a_n \\ v_1 = a _ n &x + a _ {n - 1} \end{aligned} \]

然后由内向外逐层计算一次多项式的值,即:

\[\begin{aligned} v_2 = v_1 x + a_{n - 1} \\ v_3 = v_2 x + a_{n - 3} \\ \vdots \\ v_n = v_{n - 1} x + a_0 \end{aligned} \]

这样,求n次多项式\(f(x)\)的值就转化为求n个一次多项式的值。

3 最大公约数

3.1 欧几里得算法

1.辗转相除法:\(gcd(x, y) = gcd(y, x \% y)\)

//递归形式 
inline int gcd(int x, int y) {
	return (y == 0 ? x : gcd(y, x % y));
}
//非递归形式 
inline int gcd(int x, int y) {
	rg int r = x % y;
	while (r) {
		x = y;
		y = r;
		r = x % y;
	}
	return y;
}

(速度:非递归 \(>\) 递归 \(>\) __gcd(),\(10^7\)数据下前两个差距不大,__gcd()慢0.1s左右)

2.最小公倍数:\(lcm(a, b) * gcd(a, b) = a * b\)

3.裴蜀定理(贝祖定理):设\(a, b\)为不全为0的整数,则\(\exists x, y\),使得:\(ax + by = gcd(a, b)\)

4.裴蜀定理推广:

(1)一定存在整数x, y,满足\(ax + by = gcd(x, y) \times n\)

(2)一定存在整数\(X_1 , X_2, \cdots , X_n\),满足\(\sum _ {i = 1} ^ n A_iX_i = gcd(A_1, A_2, \cdots , A_n)\)

3.2 扩展欧几里得

1.作用:求\(ax + by = gcd(a, b)\)的一组整数解。

2.证明:

\(ax_1 + by_1 = gcd(a, b)\)

\(bx_2 + (a \% b)y_2 = gcd(b, a \% b)\)

\(\because gcd(a, b) = gcd(b, a \% b)\)

\(\therefore ax_1 + by_1 = bx_2 + (a \% b)y_2\)

\(\because a \% b = a - \lfloor \frac a b \rfloor \times b\)

\(\therefore ax_1 + by_1 = bx_2 + (a - \lfloor \frac a b \rfloor \times b)y_2 = ay_2 + b(x_2 - \lfloor \frac a b \rfloor y_2)\)

因为系数相同,所以我们可以让\(x_1 = y_2\)\(y_1 = x_2 - \lfloor \frac a b \rfloor y_2\)

显然,可以利用递归,先求出下一层的解\(x_2\)\(y_2\),再回到上一层,求出特解\((x_1, y_1)\),顺便求出\(gcd(a, b)\)

//求解 ax+by = gcd(a,b) 
//返回gcd(a,b)的值
inline int exgcd(int a, int b, int &x, int &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	int ret = exgcd(b, a % b, x, y);
	int t = x;
	x = y;
	y = t - a / b * y;
	return ret;
}

3.构造通解:

\[\begin{cases}x = x_0 + \frac b {gcd(a, b)} \times k \\ y = y_0 - \frac a {gcd(a, b)} \times k\end{cases} \]

4.求不定方程\(ax + by = c\)的一组整数解:

\(gcd(a, b) | c\),则有整数解。先用扩欧求得\(ax+by = gcd(a, b)\)的解,再乘以\(\frac c {gcd(a, b)}\),即原方程的特解。

\(gcd(a, b) \nmid c\),则无整数解。

inline int exgcd(int a, int b, int &x, int &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	rg int ret = exgcd(b, a % b, x, y);
	rg int t = x;
	x = y;
	y = t - a / b * y;
	return ret;
}
int main() {
	int a, b, c, x, y;
	cin >> a >> b >> c;
	rg int d = exgcd(a, b, x, y);
	if (c % d == 0) {
		cout << c / d * x << " " << c / d * y << "\n";
	} else {
		cout << "none" << "\n";
	}
	return qwq;
}

5.扩欧求解线性同余方程:

(1)把同余方程转化为不定方程:由\(ax \equiv b \pmod m\)\(ax = -my + b\),即\(ax + my = b\),由裴蜀定理得,当\(gcd(a, m) \mid b\)时有解

(2)扩欧求\(ax + my = gcd(a, m)\)的解,之后把x乘以\(\frac b {gcd(a, m)}\),即原始方程的特解。

(3)x的最小整数解:\(x = (x \% m + m) \% m\)

4 乘法逆元

4.1 定义

如果一个线性同余方程\(ax \equiv 1 \pmod b\),则称x为a在模b意义下的乘法逆元,记作\(a^{-1}\)

并非所有情况下都存在乘法逆元,但当\(gcd(a, b) = 1\),即a,b互质时,存在乘法逆元。

4.2 逆元的用途

因为对于除法取模不成立,即\((a \div b) \% p \ne ((a \% p) \div (b \% p)) \% p\),所以用逆元来解决这个问题。

逆元就是把除法取模运算转化为乘法取模运算。

对于\((a \div b) \% p = m\),假设存在一个数x满足\((a \times x) \% p = m\),两边同乘b得到:\(a \% p = (m \times (b \% p)) \% p\);若a和b均小于p的话,上式改为:\(a = (m \times b) \% p\),等式两边同时乘以x,联立(2)式得到:\((a \times x) \% p = (x \times m \times b) \% p = (x \times m \times b) \% p\),因此可以得到:\((b \times x) \% p = 1\)。可以看出x是b在模p意义下的逆元。

由以上过程我们看到,求\((a \div b) \% p\) 等同于求$a \times (b $ 的逆元 \() \% p\)。因此求模运算的除法问题就转化为求一个数的逆元问题了。

4.3 如何求乘法逆元

1.费马小定理(模数为素数且\(p \nmid a\)

因为 \(ax \equiv 1 \pmod p\)

所以 \(ax \equiv a^{p-1} \pmod p\)

所以 \(x \equiv a^{p-2} \pmod p\)

inline ll quickpow(ll a, ll n, ll p) {  //求 a ^ n % p
	ll ans = 1;
	while (n) {
		if (n & 1) ans = ans * a % p;
		a = a * a % p;
		n >>= 1;
	}
	return ans;
}
inline ll niyuan(ll a, ll p) {  //求 a ^ (p - 2) % p
	return quickpow(a, p - 2, p);
}

2.欧拉定理求逆元(a与p互质)

\(gcd(a, p) = 1\),则\(a^{\varphi (p)} \equiv 1 \pmod p\)

那么\(a^{\varphi(p) - 1}\)即为a在模p意义下的逆元。

inline ll inv(ll a, ll p) {
	ll phi = p, mod = p;
	for (rg int i = 2; i * i <= p; i++) {
		if (p % i == 0) {
			phi = phi / i * (i - 1);
			while (p % i == 0) p /= i;
		}
	}
	if (p > 1) phi = phi / p * (p - 1);
	phi--;
	ll res = 1, t = a;
	while (phi) {
		if (phi & 1) res = (res * t) % mod;
		t = (t * t) % mod;
		phi >>= 1;
	}
	return res;
}

3.扩展欧几里得求逆元(a与p互质)

我们将\(ax \equiv 1 \pmod p\)转换成\(ax = p(-y) + 1\),移项得 \(ax + py = 1\),可用扩欧求解,逆元为
\((res \% p + p) \% p\)

inline ll exgcd(ll a, ll b, ll &x, ll &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	rg ll res = exgcd(b, a % b, x, y);
	rg ll t = x;
	x = y;
	y = t - a / b * y;
	return res;
}

4.线性求逆元 (模数为质数)

首先,\(1^{-1} \equiv 1 \pmod p\)。设\(p = k \times i + r\),其中\(1 < i < p, r < i\),再放到模p意义下,则有:\(k \times i + r \equiv 0 \pmod p\)。两边同时乘以\(i^{-1} \times r^{-1}\),则:

\[\begin{aligned} k \times i \times i^{-1} \times r^{-1} + r \times i^{-1} \times r^{-1} \equiv 0 \pmod p \\\ k \times r^{-1} + i^{-1} \equiv 0 \pmod p \\\ i^{-1} \equiv -k \times r ^ {-1} \pmod p \\\ i^{-1} \equiv -\lfloor \frac p i \rfloor \times r^{-1} \pmod p \\\ i^{-1} \equiv -\lfloor \frac p i \rfloor \times (p \bmod i) ^ {-1} \pmod p\end{aligned} \]

\(t = \frac p i, k = p \% i\),有:\(p = i \times t + k\),即$i \times t + k \equiv 0 \pmod p \(,即\)k \equiv -i \times t \pmod p\(。两边同时除以\)i \times k\(,有\)\frac 1 i \equiv -\frac t k \pmod p\(,将k,t带入有\)inv[i] \equiv -\frac p i \times inv[p % i] \pmod p\(,为防止有负数,有\)inv[i] = (p - \frac p i) \times inv[p % i]) % p$。

ll ny[MAXN];
inline void get_inv(int n, int p) {
	ny[1] = 1;
	for (rg int i = 2; i <= n; i++) {
		ny[i] = (ll)(p - p / i) * ny[p % i] % p;
	}
}

4.4 补充

1.逆元是(完全)积性函数,所以在求\(2 ^ m x \equiv 1 \pmod b\)时可以直接求出2在\(\bmod b\)意义下的逆元,这个数的m次方\(\bmod b\)就是\(2^m\)\(\bmod b\)意义下的逆元。

5 计数原理

1.加法原理:

完成一件事情共有n类方法,第一类方法有\(n_1\)种方案,第二类有\(n_2\)种方案\(\cdots\)那么完成这件事情共有\(n_1 + n_2 + \cdots\)种方法,注意分类需要不重不漏。

2.乘法原理:

完成一件事情需要分成n步进行,第一步有\(n_1\)种方法,第二步有\(n_2\)种方法\(\cdots\)那么完成这件事共有\(n_1 * n_2 * \cdots\)种方法。

3.排列与组合

(1)排列:从n个元素中取出m个元素进行排序,用符号\(A_n^m\)表示。

\(A(n, m)=n * (n-1) * (n-2) * \cdots * (n - m + 1) = \frac {n!} {(n-m)!}\)

(2)组合:从n个元素中仅仅取出m个元素,不考虑排序,用符号\(C_n^m\)表示。

\(C(n, m) = A(n, m) / A(m, m) = \frac {n!} {m!(n-m)!}\)

4.常用策略方法

(1)特殊元素和特殊位置优先策略

(2)相邻元素捆绑策略

(3)不相邻问题插空策略

(4)定序问题倍缩空位插入策略

例:7人排队,其中甲乙丙3人顺序一定,共有多少不同的排法?

共有\(\frac {A(7, 7)}{A(3,3)}\)

(5)排列问题求幂策略

(6)环排问题线排策略

(7)多排问题直排策略

(8)排列组合混合问题先选后排策略

(9)平均分组问题除法策略

(10)重排列

(11)错位排序

错排问题指一个有n个元素的排列,如若一个排列使得所有的元素不在原来的位置上,则称这个排列为原排列的一个错排。

\(D(n)\)表示n个元素的排列的错排数。

\(D(n)=(n-1)* (D(n-1)+D(n-2))\)

特殊地,\(D(1)=0,D(2)=1\)

6 组合数取模

6.1 求组合数

1.递推法:

有q\((q \le 10000)\)组询问,每组询问两个整数n,m\((1 \le m \le n \le 2000)\),求\(C_{n} ^ {m} \bmod (10 ^ 9 + 7)\)

递推式:\(C_{n} ^ {m} = C_{n - 1} ^ {m} + C_{n - 1} ^ {m - 1}\)

从n个不同数中选出m个不同方案数是\(C_{n} ^ {m}\),对第1个数有选和不选两种决策:若不选,则从剩下的\(n -1\)中选m个,即\(C_{n - 1} ^ {m}\)。若选,则从剩下的\(n - 1\)中选\(m - 1\)个,即\(C_{n - 1} ^ {m - 1}\)

const int N = 2010, mod = 1e9 + 7;
int c[N][N];
inline void init() {
	for (rg int i = 0; i < N; i++) {
		c[i][0] = 1;
	}
	for (rg int i = 1; i < N; i++) {
		for (rg int j = 1; j <= i; j++) {
			c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
		}
	}
}

2.计算法:

有q\((q \le 10000)\)组询问,每组询问两个整数n,m\((1 \le m \le n \le 10^5)\),求\(C_{n} ^ {m} \bmod (10 ^ 9 + 7)\)

\(C_{n} ^ {m} = \frac {n!} {(n - m)!m!}\)直接计算。

开两个数组分别存模意义下的阶乘和阶乘的逆元。

\(f[x]\)\(x!\bmod p\)的值。

\(g[x]\)\((x!)^{-1} \bmod p\)的值

根据费马小定理\(a \times a ^ {p - 2} \equiv 1 \pmod p\),可以用快速幂求逆元:

\(C _ n ^ m \pmod p = f[n] \times g[n - m] \times g[m] \pmod p\)

ll fac[N], inv[N];
inline ll qpow(ll a, int b) {
	rg ll res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
inline void init() {
	fac[0] = inv[0] = 1;
	for (rg int i = 1; i < N; i++) {
		fac[i] = fac[i - 1] * i % mod;
		inv[i] = inv[i - 1] * qpow(i, mod - 2) % mod;
	}
}
inline ll C(ll n, ll m) {
    if(n < m) return 0;
	return fac[n] * inv[m] % mod * inv[n - m] % mod;
}

另有\(O(n)\)推法:

inline void init() {
	fac[0] = 1;
	for (rg int i = 1; i < N; i++) fac[i] = fac[i - 1] * i % mod;
	inv[N - 1] = qpow(fac[N - 1], mod - 2);
	for (rg int i = N - 2; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
}
inline int C(int a, int b) {
	if (a < b) return qwq;
	return fac[a] * inv[b] % mod * inv[a - b] % mod;
}

求逆元也可以递推:阶乘的逆元

\(\frac 1 {i!} \pmod p = \frac 1 i \times \frac 1 {(i - 1)!} \pmod p = pow(i, p - 2) \times g[i - 1] \pmod p\)

6.2 大组合数取模(Lucas定理)

1.给定整数n,m,p的值,求\(C _ n ^ m \pmod p\)的值。其中\(1 \le m \le n \le 10^{18}\)\(1 \le p \le 10 ^ 5\),保证p为素数。

2.求解:

对于质数p,有:

\(C _ n ^ m \equiv C _ {\frac n p} ^ {\frac m p} \times C _ {n \bmod p} ^ {m \bmod p} \pmod p\)

上式中,\(n \bmod p\)\(m \bmod p\)一定小于p,可以直接求解,\(C _ {\frac n p} ^ {\frac m p}\)可以继续用Lucas定理求解。这也就要求p的范围不能够太大,一般在\(10^5\)左右。边界条件:当\(m = 0\)时,返回1。

3.引理

(1)\(C _ p ^ x \equiv 0 \pmod p\)\((0 < x < p)\)

(2)\((1 + x)^p \equiv 1 + x ^ p \pmod p\)

//模版
#include<bits/stdc++.h>
#define rg register
#define qwq 0
using namespace std;
typedef long long ll;
const int N = 100010;
ll f[N], g[N];
ll p;
inline ll qpow(ll a, ll b, ll p) {
	rg ll res = 1;
	while (b) {
		if (b & 1) res = res * a % p;
		a = a * a % p;
		b >>= 1;
	}
	return res;
}
inline void init() {
	f[0] = g[0] = 1;
	for (rg int i = 1; i <= p; i++) {
		f[i] = f[i - 1] * i % p;
		g[i] = g[i - 1] * qpow(i, p - 2, p) % p;
	}
}
inline ll C(ll n, ll m) {
	if (n < m) return 0;
	return f[n] * g[m] * g[n - m] % p;
}
inline ll lucas(ll n, ll m) {
	if (m == 0) return 1;
	return lucas(n / p, m / p) * C(n % p, m % p) % p;
}
ll q, n, m;
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> q;
	while (q--) {
		cin >> n >> m >> p;
		init();
		cout << lucas(n + m, n) << "\n";
	}
	return qwq;
}

7 中国剩余定理(CRT)

7.1 简介

「物不知数」问题:有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?

中国剩余定理可求解如下形式的一元线性同余方程组。

\[\begin{cases} x \equiv r_1 \pmod {m_1} \\ x \equiv r_2 \pmod {m_2} \\ \vdots \\ x \equiv a_k \pmod {n_k} \end{cases} \]

求x的最小非负整数解。(其中模数\(m_1, m_2, \cdots m_k\)为两两互质整数)

7.2 过程

1.计算所有模数的乘积\(M = m_1 \times m_2 \times m_3 \times \cdots \times m_n\)

2.对于第i个方程:

a.计算\(c_i = \frac M {m_i}\)

b.计算\(c_i\)在模\(m_i\)意义下的逆元\(c_i ^ {-1}\)。(逆元一定存在,因为\(c_i\)中已经把\(m_i\)除掉了)

3.方程组的唯一解为:\(x = \sum _ {i = 1} ^ n r_i c_i c_i^{-1} \bmod M\)

例:

\[\begin{cases} x \equiv 2 \pmod 3 \\ x \equiv 3 \pmod 4 \\ x \equiv 1 \pmod 5 \end{cases} \]

1.\(M = 3 \times 4 \times5 = 60\)

2.\(c_1 = 20\)\(c_1 ^ {-1} = 2\)

\(c_2 = 15\)\(c_2^{-1} = 3\)

\(c_3 = 12\)\(c_3^{-1} = 3\)

3.\(x = (2 \times 20 \times 2 + 3 \times 15 \times + 1 \times 12 \times 3) \% 60 = 11\)

#include<bits/stdc++.h>
#define rg register
#define qwq 0
using namespace std;
typedef long long ll;
ll n, a[11], b[11];
inline ll exgcd(ll a, ll b, ll &x, ll &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	rg ll ret = exgcd(b, a % b, x, y);
	rg ll t = x;
	x = y;
	y = t - a / b * y;
	return ret;  //x就是逆元
}
inline ll CRT(ll m[], ll r[]) {
	ll M = 1, ans = 0;
	for (rg int i = 1; i <= n; i++) M *= m[i];
	for (rg int i = 1; i <= n; i++) {
		rg 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() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n;
	for (rg int i = 1; i <= n; i++) {
		cin >> a[i] >> b[i];
	}
	cout << CRT(a, b) << "\n";
	return qwq;
}

7.3 扩展卢卡斯定理

Lucas定理中要求模数p必须为素数,那么对于p不是素数的情况,就要用到exLucas。

1.题目:(P4720 【模板】扩展卢卡斯定理/exLucas)

\(C _ n ^ m \bmod P\)

2.过程:

(1)第一部分:中国剩余定理

考虑利用中国剩余定理合并答案。

根据唯一分解定理,将P质因数分解:

\(P = p_1^{a_1} \times p_2^{a_2} \times \cdots \times p_r^{a_r} = \prod_{i = 1}^{r} p_i^{a_i}\)

对于任意i,j,有 \(p_i^{a_i}\)\(p_j^{a_j}\) 互质,所以可以构造出如下r个同余方程:

\[\begin{cases} a_1 \equiv C_m^n \pmod {p_1^{a_1}} \\ a_2 \equiv C_m^n \pmod {p_2^{a_2}} \\ \vdots \\ a_r \equiv C_m^n \pmod {p_r^{a_r}} \end{cases} \]

(2)第二部分:移除分子分母中的素数

根据同余的定义,\(a_i = C_m^n \bmod p_i^{a_i}\),问题转化成,求\(C_m^n \bmod p^a\)(p为质数)的值。

根据组合数定义,\(C_m^n \bmod p^a = \frac {n!} {m!(n-m)!} \bmod p^a\)

由于式子是在模\(p^a\)意义下,所以分母要算乘法逆元。

同余方程\(ax \equiv 1 \pmod p\)(即乘法逆元)有解的充要条件为\(\gcd(a,p) = 1\),然而无法保证有解,发现无法直接求\(inv_{m!}\)\(inv_{(n-m)!}\),所以将原式转化为:
\(\frac{\frac{n!}{p^x}}{\frac{m!}{p^y}\frac{(n-m)!}{p^z}}p^{x-y-z} \bmod p^a\)

x表示n!中包含多少个p因子,y,z同理。

(3)第三部分:Wilson定理的推论

问题转化成,求形如:\(\frac {n!} {q^x} \bmod q^k\)的值。这时可以利用Wilson定理的推论。

解释:

一个示例:\(22!\bmod 3^2\)

\(22! = 3^7 \times (1 \times 2 \times 3 \times 4 \times 5 \times 6 \times 7) \times (1 \times 2 \times 4 \times 5 \times 7 \times 8 \times 10 \times 11 \times 13 \times 14 \times 16 \times 17 \times 20 \times 22)\)

可以看到,式子分为三个整式的乘积:

1> 是\(q^{\lfloor \frac {n}{q} \rfloor}\)

2> 是\(\lfloor \frac n q \rfloor!\),由于阶乘中仍然有可能有q的倍数,考虑递归求解;

3> 是\(n!\)中与q互质的部分的乘积,具有如下性质:

\(1 \times 2 \times 4 \times 5 \times 7 \times 8 \equiv 10 \times 11 \times 13 \times 14 \times 16 \times 17 \pmod {3^2}\),即:

\(\displaystyle \prod_{i,\gcd(i,q)=1}^{q^k}i\equiv\prod_{i,\gcd(i,q)=1}^{q^k}(i+tq^k) \pmod{q^k}\)(t是任意正整数)。

\(\displaystyle \prod_{i,\gcd(i,q)=1}^{q^k}i\) 一共循环了\(\displaystyle \lfloor\frac{n}{q^k}\rfloor\) 次,暴力求出\(\displaystyle \prod_{i,\gcd(i,q)=1}^{q^k}i\),然后用快速幂求\(\displaystyle \lfloor\frac{n}{q^k}\rfloor\) 次幂。
最后要乘上\(\displaystyle \prod_{i,\gcd(i,q)=1}^{n \bmod q^k}i\),即 \(19\times 20\times 22\),显然长度小于 q^k,暴力乘上去。

上述三部分乘积为\(n!\)。最终要求的是\(\frac{n!}{q^x} \bmod {q^k}\)

所以有:\(n! = q^{\lfloor \frac n q \rfloor} \times (\lfloor \frac n q \rfloor)! \times (\displaystyle \prod _ {i, \gcd(i,q) = 1} ^ {q^k}i)^{\lfloor \frac{n} {q^k} \rfloor} \times (\prod _ {i, \gcd(i,q) = 1} ^ {n \bmod q^k}i)\)

于是:

\(\frac{n!}{q^{\lfloor \frac n q \rfloor}} = (\lfloor \frac n q \rfloor)! \times (\displaystyle \prod _ {i, \gcd(i,q) = 1} ^ {q^k}i)^{\lfloor \frac{n} {q^k} \rfloor} \times (\prod _ {i, \gcd(i,q) = 1} ^ {n \bmod q^k}i)\)

\((\lfloor \frac{n}{q} \rfloor)!\)同样是一个阶乘,所以也可以分为上述三个部分,于是可以递归求解。

等式的右边两项不含素数 q。事实上,如果直接把 n 的阶乘中所有 q 的幂都拿出来,等式右边的阶乘也照做,这个等式可以直接写成:

\(\frac{n!}{q^{x}} = \frac{\left(\left\lfloor\frac{n}{q}\right\rfloor\right)!}{q^{x'}} \cdot {\left(\prod_{i,\gcd(i,q)=1}^{q^k}i\right)}^{\left\lfloor\frac{n}{q^k}\right\rfloor} \cdot \left(\prod_{i,\gcd(i,q)=1}^{n\bmod q^k}i\right)\)

式中的 x 和 x' 都表示把分子中所有的素数 q 都拿出来。改写成这样,每一项就完全不含 q 了。

递归的结果,三个部分中,左边部分随着递归结束而自然消失,中间部分可以利用 Wilson 定理的推论 0,右边部分就是推论 2 中的 \(\prod_{j\geq 0}(N_j!) _ p\)

#include<bits/stdc++.h>
#define rg register
#define qwq 0
using namespace std;
typedef long long ll;
inline void exgcd(ll a, ll b, ll &x, ll &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return ;
	}
	exgcd(b, a % b, x, y);
	rg ll t = x;
	x = y;
	y = t - a / b * y;
}
inline ll gcd(ll a, ll b) {
	if (b == 0) return a;
	return gcd(b, a % b);
}
inline ll inv(ll a, ll p) {
	rg ll x, y;
	exgcd(a, p, x, y);
	return (x + p) % p;
}
inline ll lcm(ll a, ll b) {
	return a / gcd(a, b) * b;
}
inline ll mabs(ll x) {
	return (x > 0 ? x : -x);
}
inline ll qmul(ll a, ll b, ll p) {
	rg ll ret = 0;
	a %= p;
	b %= p;
	while (b) {
		if (b & 1ll) ret = (ret + a) % p;
		b >>= 1ll;
		a = (a + a) % p;
	}
	return ret;
}
inline ll qpow(ll a, ll b, ll p) {
	rg ll ret = 1;
	a %= p;
	while (b) {
		if (b & 1ll) ret = ret * a % p;
		a = a * a % p;
		b >>= 1ll;
	}
	return ret;
}
inline ll F(ll n, ll P, ll PK) {
	if (n == 0) return 1;
	rg ll rou = 1;  //循环节
	rg ll rem = 1;  //余项
	for (rg ll i = 1; i <= PK; i++) {
		if (i % P) rou = rou * i % PK;
	}
	rou = qpow(rou, n/ PK, PK);
	for (rg ll i = PK * (n / PK); i <= n; i++) {
		if (i % P) rem = rem * (i % PK) % PK;
	}
	return F(n / P, P, PK) * rou % PK * rem % PK;
}
inline ll G(ll n, ll P) {
	if (n < P) return 0;
	return G(n / P, P) + n / P;
}
inline ll C_PK(ll n, ll m, ll P, ll PK) {
	rg ll fz = F(n, P, PK), fm1 = inv(F(m, P, PK), PK), fm2 = inv(F(n - m, P, PK), PK);
	rg ll mi = qpow(P, G(n, P) - G(m, P) - G(n - m, P), PK);
	return fz * fm1 % PK * fm2 % PK * mi % PK;
}
ll A[1005], B[1005];
inline ll exlucas(ll n, ll m, ll P) {
	rg ll ljc = P, tot = 0;
	for (rg ll i = 2; i * i <= P; i++) {
		if (ljc % i == 0) {
			rg ll PK = 1;
			while (ljc % i == 0) {
				PK *= i;
				ljc /= i;
			}
			A[++tot] = PK;
			B[tot] = C_PK(n, m, i, PK);
		}
	}
	if (ljc != 1) {
		A[++tot] = ljc;
		B[tot] = C_PK(n, m, ljc, ljc);
	}
	rg ll ans = 0;
	for (rg int i = 1; i <= tot; i++) {
		rg ll M = P / A[i], T = inv(M, A[i]);
		ans = (ans + B[i] * M % P * T % P) % P;
	}
	return ans;
}
ll n, m, P;
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m >> P;
	cout << exlucas(n, m, P) << "\n";
	return 0;
}

7.4 扩展中国剩余定理

1.问题:求解线性同余方程组

\[\begin{cases} x \equiv r_1 \pmod {m_1} \\ x \equiv r_2 \pmod {m_2} \\ \vdots \\ x \equiv a_k \pmod {n_k} \end{cases} \]

求x的最小非负整数解。(其中模数\(m_1, m_2, \cdots m_n\)不一定两两互质的整数)

2.思路:

前两个方程:\(x \equiv r_1 \pmod m_1\)\(x \equiv r_2 \pmod m_2\)

转化为不定方程:\(x = m_1 p + r1 = m_2 q + r_2\),则:\(m_1p - m_2q = r_2 - r_1\)

由裴蜀定理,

\(\gcd(m_1, m_2) \nmid (r_2 - r_1)\)时,无解

\(\gcd(m_1, m_2) \mid (r_2 - r_1)\)时,有解

由扩欧算法,

得特解 \(p = p \times \frac{r_2 - r_1} {gcd}\)\(q = q \times \frac{r_2 - r_1} {gcd}\)

其通解 \(P = p + \frac {m_2} {gcd} \times k\)\(Q = q - \frac{m_1} {gcd} \times k\)

所以\(x = m_1P + r_1 = \frac{m_1 m_2} {gcd} \times k + m_1 p + r_1\)

前两个方程等价合并为一个方程\(x \equiv r \pmod m\),其中\(r = m_1p + r_1\)\(m = lcm(m_1, m_1)\)

所以n个同余方程只要合并\(n - 1\)次,即可求解。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
using namespace std;
typedef __int128 ll;
const int N = 100005;
ll n, m[N], r[N];
inline ll exgcd(ll a, ll b, ll &x, ll &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	rg ll ret = exgcd(b, a % b, x, y);
	rg ll t = x;
	x = y;
	y = t - a / b * y;
	return ret;
}
inline ll EXCRT(ll m[], ll r[]) {
	rg ll m1, m2, r1, r2, p, q;
	m1 = m[1], r1 = r[1];
	for (rg int i = 2; i <= n; i++) {
		m2 = m[i];
		r2 = r[i];
		rg ll d = exgcd(m1, m2, p, q);  //d = gcd(m1, m2)
		if ((r2 - r1) % d) return -1;
		p = p * (r2 - r1) / d;  //特解 
		p = (p % (m2 / d) + m2 / d) % (m2 / d);  //因为p可能小于0,变成最小正整数 
		r1 = m1 * p + r1;
		m1 = m1 * m2 / d;
	}
	return (r1 % m1 + m1) % m1;
}
int main() {
	scanf("%lld", &n);
	for (rg int i = 1; i <= n; i++) {
		scanf("%lld%lld", m + i, r + i);
	}
	printf("%lld\n", EXCRT(m, r));
	return qwq;
}

8 容斥原理

8.1 集合的并

1.\(A_1, A_2,\cdots ,A_n\)是n个集合,则这n个集合并集的元素个数是:

\(\left| \bigcup_{i=1}^{n}A_i\right| =\sum_{i = 1} ^n |A_i|-\sum_{i<j}|A_i\cap A_j|+\sum_{i<j<k}|A_i\cap A_j\cap A_k|-\cdots +(-1)^{n-1}|A_1\cap A_2 \cap \cdots\cap A_n|\)

解析:

\(\sum _ {i = 1} ^ n |A_i|\):将每个集合中的元素个数相加,该值大于总元素数,需修正;(系数为\((-1)^0\)

\(\sum _ {i < j} |A_i \cap A_j|\):减去两两相交的元素个数,该值小于总元素数,需修正;(系数为\((-1)^1\)

\(\sum _ {i < j < k} |A_i \cap A_j \cap A_k|\):加上三个集合相交的元素个数,该值大于总元素数,需修正;(系数为\((-1)^2\)

\(\vdots\)

\((-1)^{n - 1} |A_1 \cap A_2 \cap \cdots \cap A_n|\):加上\((-1)^{n - 1}\)乘以\(n - 1\)个集合相交的元素个数;(系数为\((-1)^{n - 1}\)

上述 奇数个集合的交集的元素个数前系数是正数,偶数个集合的交集的元素个数前系数是负数。

2.例题:给定一个整数n和m个不同的质数\(p_1, p_2, \cdots , p_m\)。求:\(1 \sim n\)中能被\(p_1, p_2, \cdots , p_m\)中的至少一个数整除的整数有多少个。

(1)交集的大小等于n除以质数的乘积。即\(|S_1| = \frac n {p_1}\)\(|S_1 \cap S_2| = \frac n {p_1 \times p_2}\)\(|S_1 \cap S_2 \cap S_3| = \frac n {p_1 \times p_2 \times p_3}\)

(2)使用的二进制位来表示每个集合选与不选的状态。
如 若有3个质数,就需要3个二进制位来表示所有状态。

\(001 \to S_1,010\to S_2, 100\to S_3\)\(011\to S_1\cap S_2,101\to S_1\cap S_3,110\to S_2\cap S_3\)\(111\to S_1 \cap S_2 \cap S_3\)

#include<bits/stdc++.h>
#define rg register
#define qwq 0
using namespace std;
typedef long long ll;
const int N = 20;
int n, m, prim[N];
inline int calc() {
	rg int res = 0;
	for (rg int i = 1; i < 1 << m; i++) {
		rg int t = 1, sign = -1;
		for (rg int j = 0; j < m; j++) {
			if (i & 1 << j) {
				if ((ll)t * prim[j] > n) {
					t = 0;
					break;
				}
				t *= prim[j];
				sign = -sign;
			}
		}
		if (t) res += n / t * sign;
	}
	return res;
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for (rg int i = 0; i < m; i++) {
		cin >> prim[i];
	}
	cout << calc();
	return qwq;
}

8.2 集合的交

集合的交等于全集减去补集的并

\(| \bigcap_{i=1}^{n} S_i| = |U| - |\bigcup_{i=1}^ {n}\overline{S_i}|\)

右边的补集的并使用容斥原理求解。

9 卡特兰数

9.1 介绍

卡特兰数是组合数学中一个常出现于各种计数问题中的数列。其前几项为(从第零项开始):\(1,1,2,5,14,42,132,429,1430,4862,\cdots\)

卡特兰数\(C[n]\)满足以下递推关系:

\(C[n] = C[0]C[n-1]+C[1]C[n-2]+\cdots+C[n-1]C[0]\)

9.2 例

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

1.通项公式:

(1)\(H_n = C_{2n}^n - C_{2n}^{n-1}\)

(2)\(H_n = \frac{1}{n+1} C_{2n}^n\)

(3)\(H_n = \frac{4n-2}{n+1}H_{n-1}\)

2.证明:

(1)证明(1)式:

先求路径总数,在2n次移动中选n次向右移动,即\(C_{2n}^n\)
再求非法路径,即越过对角线\(y=x\)的路径。如果跨越了\(y=x\)那么一定触碰了\(y=x+1\)
所有的非法路径与这一条线有至少一个交点,把第一个交点设为(a,a+1),把(a,a+1)之后的路径全部按照\(y=x+1\)这条线对称过去,这样,最后的终点就会变成(n-1,n+1)。

所有的非法路径对称后都唯一对应着一条到(n-1,n+1)的路径,所以非法路径数就是\(C_{2n}^{n-1}\),合法路径数就是\(C_{2n}^n - C_{2n}^{n-1}\)

(2)证明(2)式:

\(H_n = C_{2n}^{n}-C_{2n}^{n-1} = \frac{(2n)!}{n!n!}-\frac{(2n)!}{(n+1)!(n-1)!}=\frac{(2n)!}{n!(n-1)!}(\frac{1}{n}-\frac{1}{n+1})=\frac{(2n)!}{n!n!(n+1)}=\frac{1}{n+1}C_{2n}{n}\)

9.3 应用

1.n个元素进栈序列为:\(1,2,3, \cdots ,n\),则有多少种出栈序列。

我们将进栈表示为+1,出栈表示为-1,则1 3 2的出栈序列可以表示为:+1,-1,+1,+1,-1,-1。

根据栈本身的特点,每次出栈的时候,必定之前有元素入栈,即对于每个-1前面都有一个+1相对应。因此,出栈序列的所有前缀和必然大于等于0,并且+1的数量等于-1的数量。

观察一下n=3的一种出栈序列:+1,-1,-1,+1,-1,+1。序列前三项和小于0,显然这是个非法的序列。如果将第一个前缀和小于0的前缀,即前三项元素都进行取反,就会得到:-1,+1,+1,+1,-1,+1。此时有3+1个+1以及3-1个-1。

因为这个小于0的前缀和必然是-1,且-1比+1多一个,取反后,-1比+1少一个,则+1变为n+1个,且-1变为n-1个。进一步推广,对于n元素的每种非法出栈序列,都会对应一个含有n+1个+1以及n-1个-1的序列。

假设非法序列为A,对应的序列为B。每个A只有一个“第一个前缀和小于0的前缀”,所以每个A只能产生一个B。而每个B想要还原到A,就需要找到“第一个前缀和大于0的前缀”,显然B也只能产生一个A。

每个B都有n+1个+1以及n-1个-1,因此B的数量为\(C_{2n}^{n+1}\),相当于在长度为2n的序列中找到n+1个位置存放+1。相应的,非法序列的数量也就等于\(C_{2n}^{n+1}\)

出栈序列的总数量共有\(C_{2n}^{n}\),因此,合法的出栈序列的数量为\(C_{2n}^n-C_{2n}^{n+1}=\frac{1}{n+1}C_{2n}^n\)

2.n对括号,则有多少种“括号匹配”的括号序列。

n对括号相当于有2n个符号,n个左括号、n个右括号,可以设问题的解为f(2n)。第0个符号肯定为左括号,与之匹配的右括号必须为1第2i+1个字符。

于是得到递推式:\(f(2n)=f(0) \times f(2n-2) + f(2) \times f(2n-4) + \cdots +f(2n-2) \times f(0)\)

假设\(f(0)=1\),计算一下开始几项,\(f(2)=1,f(4)=2,f(6)=5\)。结合递推式,不难发现f(2n)等于h(n)。

3.n个节点构成的二叉树,共有多少种情形?

首先,根肯定会占用一个结点,那么剩余的n-1个结点可以有如下的分配方式:\((0,n-1),(1,n-2),\cdots ,(n-1,0)\),其中(i,j)表示根的左子树含i个结点,右子树含j个结点。

于是\(f(n)=f(0) \times f(n-1) + f(1) \times f(n-2)+ \cdots + f(n-1) \times f(0)\)

假设f(0)=1,那么\(f(1)=1,f(2)=2,f(3)=5\)。结合递推式,不难发现f(n)等于h(n)。

4.求一个凸多边形区域划分成三角形区域的方法数。

以凸多边形的一边为基,设这条边的2个顶点为A和B。从剩余顶点中选1个,可以将凸多边形分成三个部分,中间是一个三角形,然后求解左右两个凸多边形。

设n个顶点的凸多边形的解为f(n),于是\(f(n)=f(2) \times f(n-1) + f(3) \times f(n-2) + \cdots + f(n-1) \times f(2)\)

\(f(2) \times f(n-1)\)表示三个相邻的顶点构成一个三角形,那么另外两个部分的顶点数分别为2和n-1。设f(2)=1,那么\(f(3)=1,f(4)=2,f(5)=5\)。结合递推式,不难发现f(n)等于h(n-2)。

5.在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数。

以其中一个点为基点,编号为0,然后按顺时针方向将其他点依次编号。那么与编号为0相连点的编号一定为奇数,否则,这两个编号间含有奇数个点,势必会有点被孤立,即在一条线段的两侧分别有一个孤立点,从而导致两线段相交。设选中的基点为A,与它连接的点为B,那么A和B将所有点分成两部分,一部分位于AB的左边,另一部分位于右边,然后分别求解即可。

于是\(f(n)=f(0) \times f(n-2) + f(2) \times f(n-4) + \cdots + f(n-2) \times f(0)\)

\(f(0) \times f(n-2)\)表示编号0的点与编号1的点相连,此时位于它们右边的点的个数为0,而位于它们左边的点为2n-2。以此类推,\(f(0)=1,f(2)=1,f(4)=2\)。结合递推式不难发现f(2n)等于h(n)。

10 Prüfer序列

10.1 引入

prufer序列是一种将带标号的树用一个唯一的整数序列表示的方法。

prufer序列可以将一个带标号n个节点的树用[1,n]中的n-2个整数表示。你也可以理解为完全图的生成树与数列之间的双射。常用于组合计数问题中。

10.2 对树建立prufer序列

prufer序列是这样建立的:①每次选择一个编号最小的叶结点并删掉它;②在序列中记录下它连接到的那个节点,即它的父结点;③重复n-2次后只剩下两个结点,算法结束。

例如,这是一个7个结点的树的prufer序列的构造过程:

显然,维护一个小根堆,将叶子结点都丢入。每次从堆顶取出一个叶子结点,将其父结点编号加入序列,然后删去该叶子结点并减小其父结点的度。当父结点度为1时变为叶子结点,丢入小根堆。

vector<vector<int> > adj;
vector<int> pruefer_code() {
	rg int n = adj.size();
	set<int> leafs;
	vector<int> degree(n);
	vector<bool> killed(n, false);
	for (rg int i = 0; i < n; i++) {
		degree[i] = adj[i].size();
		if (degree[i] == 1) leafs.insert(i);
	}
	vector<int> code(n - 2);
	for (rg int i = 0; i < n - 2; i++) {
		rg int leaf = *leafs.begin();
		leafs.erase(leafs.begin());
		killed[leaf] = true;
		rg int v;
		for (rg int j = 0; j < adj[leaf].size(); j++) {
			rg int u = adj[leaf][j];
			if (!killed[u]) v = u;
		}
		code[i] = v;
		if (--degree[v] == 1) leafs.insert(v);
	}
	return code;
}

10.3 prufer序列的线性构造算法

初始时\(p\)的值为编号最小的叶子结点。

①将\(p\)的父结点\(f\)加入序列;②若删去\(p\)结点后\(f\)结点变为叶子结点,且\(f < p\),则此时可以将\(f\)作为新选择的叶子结点进行重复操作直到\(f > p\);③将\(p\)自增直到找到下一个未被删除的叶子结点。

int degree[N], fa[N], prufer[N];
int p, cnt;
inline void make_prufer() {
	for (rg int i = 1; i <= n; i++) {  //寻找编号最小的叶子结点 
		if (degree[i] == 1) {
			p = i;
			break;
		}
	}
	rg int leaf = p;  //leaf为当前选择要删去的叶子结点 
	while (cnt < n - 2) {
		rg int f = fa[leaf];
		prufer[++cnt] = f;
		degree[f]--;
		if (degree[f] == 1 && f < p) {
			leaf = f;
		} else {
			p++;
			while (degree[p] != 1) p++;
			leaf = p;
		}
	}
}

10.4 prufer序列的性质

1.在构造完prufer序列后原树中会剩下两个结点,其中一个一定是编号最大的点n。

2.每个结点在序列中出现的次数是其度数减1(没有出现就是叶子结点)。

10.5 由prufer序列重建树

由性质2可知,每个结点的度数为prufer序列中出现次数+1。

①选择度数为1的最小的节点编号,与当前枚举到的prufer序列的点连接,然后同时减掉两个点的度。
②若度数变成1,则该结点也成为叶子结点。
③到最后剩下两个度数为1的点,其中一个是n。把他们连接。

10.6 线性时间重建树

初始时\(p\)的值为编号最小的未出现在序列中的结点。

①将\(p\)的父结点\(f\)设为prufer序列里尚未使用的第一个元素;②若删去\(p\)结点后\(f\)结点变为叶子结点且\(f < p\),则此时可以将\(f\)作为新选择的叶子结点进行重复操作直到\(f > p\)。③将\(p\)自增直到找到下一个还没删除的叶子结点。

int degree[N], fa[N], prufer[N];
int p, cnt;
inline void make_tree() {
	for (rg int i = 1; i <= n; i++) degree[i] = 1;
	for (rg int i = 1; i <= n - 2; i++) degree[prufer[i]]++;  //度数为出现次数+1 
	for (rg int i = 1; i <= n; i++) {  //找到编号最小的叶子结点 
		if (degree[i] == 1) {
			p = i;
			break;
		}
	}
	rg int leaf = p;  //leaf为当前选择要删去的叶子结点 
	while (cnt < n - 2) {
		rg int f;
		f = fa[leaf] = prufer[++cnt];  //当前结点的父亲为序列未使用的第一位 
		degree[f]--;
		if (degree[f] == 1 && f < p) {
			leaf = f;
		} else {
			p++;
			while (degree[p] != 1) p++;
			leaf = p;
		}
	}
	fa[leaf] = n;  //最后剩下两个结点,一个是n,把它们俩相连即可 
}

10.7 prufer序列的应用

1.无向完全图的不同生成树数:

若该完全图有n个结点,则任意一个长度为n-2的prufer序列都可以重构出其一棵生成树,且各不相同。又因为prufer序列的值域在[1,n],故总数为\(n^{n-2}\)

这就是Cayley公式。

2.n个点的无根树计数:

同上问题。

3.n个点的有根树计数:

对每棵无根树来说,每个点都可能是根,故总数为\(n^{n-2} \times n = n^{n-1}\)

4.n个点,每点度分别为\(d_i\)的无根树计数:

总排列数为\((n-2)!\),即\(A_{n-2}^{n-2}\)

其中数字\(i\)出现了\(d_i-1\)次,故其重复的有\((d_i-1)!\)种排列,即\(A_{d_i-1}^{d_i-1}\)

应当除去重复的,故总个数为\(\frac{(n-2)!}{\prod_{i=1}^{n}(d_i-1)!}\)

11 BSGS

11.1 用处

求解高次同余方程

给定整数a,b,p,a、p互质,求满足\(a^x \equiv b \pmod p\)的最小非负整数x。

11.2 推导

由扩展欧拉定理:\(a^x \equiv a^{x \bmod \varphi(p)} \pmod p\)
可知\(a^x\)模p意义下的最小循环节为\(\varphi(p)\)。因为\(\varphi(p) < p\),故考虑\(x \in [0,p]\),必能找到最小整数x。

如果暴力枚举,时间是O(p)的。

\(x=i \times m-j\),其中\(m=\lceil \sqrt p \rceil\)\(i \in [1,m]\)\(j \in [0,m-1]\)

\(a^{i \times m - j} \equiv b \pmod p\)

\((a^m)^i \equiv b \times a^j \pmod p\)

①先枚举j,把\((b \times a^j, j)\)丢入哈希表,如果key重复,用更大的j替代旧的;

②再枚举i,计算\((a^m)^i\),到哈希表中查找是否有相等的key,找到第一个即结束。

则最小的\(x = i \times m - j\)

因为j与i的次数都是\(\sqrt p\)的,所以时间是\(O(\sqrt p)\)

11.3 模版

P3846 [TJOI2007] 可爱的质数/【模板】BSGS

#include<bits/stdc++.h>
#define rg register
#define qwq 0
using namespace std;
typedef long long ll;
inline ll bsgs(ll a, ll b, ll p) {
	a %= p;
	b %= p;
	if (a == 0 && b != 0) return -1;
	if (b == 1) return qwq;
	rg ll m = ceil(sqrt(p)), t = b;
	unordered_map<ll, ll> hash;
	/*
	unordered_map是一个将key和value关联起来的容器,它可以高效的根据单个key值查找对应的value。
	key值应该是唯一的,key和value的数据类型可以不相同。
	unordered_map存储元素时是没有顺序的,只是根据key的哈希值,将元素存在指定位置,所以根据key查找单个value时非常高效,平均可以在常数时间内完成。
	unordered_map查询单个key的时候效率比map高,但是要查询某一范围内的key值时比map效率低。
	可以使用[]操作符来访问key值对应的value值。
	*/
	hash[b] = 0;
	for (rg int j = 1; j < m; j++) {
		t = t * a % p;  //求 b * a^j
		hash[t] = j; 
	}
	rg ll mi = 1;
	for (rg int i = 1; i <= m; i++) {
		mi = mi * a % p;  //求 a^m 
	}
	t = 1;
	for (rg int i = 1; i <= m; i++) {
		t = t * mi % p;
		if (hash.count(t)) return i * m - hash[t];
	}
	return -1;  //无解 
}
int a, p, b;
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> p >> a >> b;
	rg int res = bsgs(a, b, p);
	if (res == -1) {
		cout << "no solution\n";
	} else {
		cout << res << "\n";
	}
	return qwq;
}

例题1:[CSP-S2019] Emiya 家今天的饭

dp+容斥。

84 pts

我们发现一个方案不合法,有且只会有一个主要食材\(>\)总数的一半,所以我们不妨考虑容斥,用所有方案数量\(-\)不合法数量。

求解所有方案数量

所有方案数量很好求,做一个分组背包即可:
\(f[i][j]\)表示前i种烹饪方式,做了j道菜的方案数。
状态转移:

  • 第i种烹饪方式不做菜:\(f[i][j] += f[i-1][j]\)
  • 第i种烹饪方法做1道主要食材是k的菜:\(f[i][j] += f[i-1][j-1]\times a_{i,k}\)

所有方案数量\(=\displaystyle\sum_{j=1}^{n}f[n][j]\)

优化:我们可以\(O(nm)\)预处理\(s_i=a_{i,1}+a_{i,2}+\cdots+a_{i,m}\)。每个状态即可\(O(1)\)转移。

求解不合法数量

由于性质:所有不合法方案中有且只会有一个主要食材\(>\)总数的一半,我们称那个主要食材为越界食材,我们设越界食材为c。
所以我们不妨先用\(O(m)\)枚举c。那么我们可以把其他食材归结为符合条件的食材,我们便可以用一个维度来记录它选了多少。
\(dp[i][j][k]\)为前i种烹饪方式,第c种(越界食材)选了j道,其他食材选了k道的方案数。
状态转移:

  • 第i种烹饪方法不做菜:\(dp[i][j][k] += dp[i - 1][j][k]\)\(O(1)\)转移
  • 选第c种(越界食材):\(dp[i][j][k] += dp[i - 1][j - 1][k]\times a_{i,c}(j>0)\)\(O(1)\)转移
  • 选其他食材:\(dp[i][j][k] += \displaystyle\sum_{u=1,u!=c}^{m}dp[i-1][j][k-1]\times a_{i,u}\)\(O(m)\)转移

对答案的贡献:\(\sum dp[n][j][k](j>k)\)

优化:可以滚掉第一维。第三种转移最耗时,考虑用求解所有方案数量优化的思想:\(\displaystyle\sum_{u=1,u!=c}^{m}dp[i-1][j][k-1]\times a_{i,u}=dp[i-1][j][k-1]\times\displaystyle\sum_{u=1,u!=c}^{m}a_{i,u}=dp[i-1][j][k-1]\times(s_i-a_{i,c})\),我们就做到了\(O(1)\)转移。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 103, M = 2003, mod = 998244353;
int n, m;
int a[N][M];
int ans, res, ret;
int f[N][M], dp[N][N][N];
int s[N];
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> n >> m;
	for (rg int i = 1; i <= n; i++) {
		for (rg int j = 1; j <= m; j++) {
			cin >> a[i][j];
			s[i] = (s[i] + a[i][j]) % mod;
		}
	}
	//求解合法数量
	ans = 1;
	for (rg int i = 1; i <= n; i++) {
		ans = ans * (s[i] + 1) % mod;
	}
	ans--;
	//求解不合法数量
	for (rg int c = 1; c <= m; c++) {
		memset(dp, 0, sizeof(dp));
		ret = 0;
		dp[0][0][0] = 1;
		for (rg int i = 1; i <= n; i++) {
			for (rg int j = 0; j <= i; j++) {
				for (rg int k = 0; k <= i - j; k++) {
					dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k]) % mod;
					if (j > 0) dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - 1][k] * a[i][c] % mod) % mod;
					if (k > 0) dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k - 1] * (s[i] - a[i][c]) % mod) % mod;
				}
			}
		}
		for (rg int i = 1; i <= n; i++) {
			for (rg int j = 0; j <= n - i; j++) {
				if(i > j) res = (res + dp[n][i][j]) % mod;
			}
		}
	}
	cout << ((ans - res) % mod + mod) % mod << "\n";
	return qwq;
}

100 pts

延续84pts的思想,求解不合法数量的\(O(n^3m)\)拖累了我们,我们考虑优化。
我们不关系具体越界食材与其他食材选了多少,只用保证越界食材数\(>\)其他食材数即为不合法状态,不妨把这两个的差作为一个维度,这样既可让dp状态降一维:
\(dp[i][j]\)表示前i种烹饪方法,越界食材数\(-\)其他食材数为j的方案数。

状态转移:

  • 第i种烹饪方法不选:\(dp[i][j] += dp[i-1][j]\)
  • 选越界食材:\(dp[i][j] += dp[i - 1][j-1]\times a_{i,c}\)
  • 选其他食材:\(dp[i][j] += dp[i - 1][j+1]\times(s_i-a_{i,c})\)

答案贡献:\(\sum dp[n][j](j>0)\)
总时间复杂度\(O(n^2m)\)

注意:做差可能为负数,我们把所有状态加一个n就可以了。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 103, M = 2003, mod = 998244353;
int n, m;
int a[N][M];
int ans, res, ret;
int f[N][M], dp[N][N + N];
int s[N];
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> n >> m;
	for (rg int i = 1; i <= n; i++) {
		for (rg int j = 1; j <= m; j++) {
			cin >> a[i][j];
			s[i] = (s[i] + a[i][j]) % mod;
		}
	}
	//求解合法数量
	ans = 1;
	for (rg int i = 1; i <= n; i++) {
		ans = ans * (s[i] + 1) % mod;
	}
	ans--;
	//求解不合法数量
	for (rg int c = 1; c <= m; c++) {
		memset(dp, 0, sizeof(dp));
		//dp[0][0] = 1;
		dp[0][n] = 1;
		for (rg int i = 1; i <= n; i++) {
			for (rg int j = 0; j <= 2 * n; j++) {
				dp[i][j] = (dp[i][j] + dp[i - 1][j]) % mod;
				dp[i][j] = (dp[i][j] + dp[i - 1][j - 1] * a[i][c]) % mod;
				dp[i][j] = (dp[i][j] + dp[i - 1][j + 1] * (s[i] - a[i][c]) % mod) % mod; 
			}
		}
		for (rg int i = n + 1; i <= 2 * n; i++) {
			res = (res + dp[n][i]) % mod;
		}
	}
	cout << ((ans - res) % mod + mod) % mod << "\n";
	return qwq;
}

例题2:[cf869C] The Intriguing Obsession

因为两个同色小岛的距离要大于3,所以:

  • 1.同色岛之间不能相连。
  • 2.一个岛不能同时和两个颜色相同的岛相连。

那么只有三种连法:\(A-B,B-C,A-C\),而且这三种方法都是独立的,所以最终的答案就是这三种方法数相乘。
比如求A色岛与B色岛之间的方法数:
因为一个A色岛只能连一个B色岛,所以它的方法数就是:从a个岛里选0个的方法数\(\times\)从b个岛里选0个的方法数 \(\times\) 0个数的总排列数 \(+\) 从a个岛里选1个的方法数 \(\times\) 从b个岛里选1个的方法数 \(\times\) 1个数的总排列数 \(+\cdots +\) 从a个岛里选a个的方法数 \(\times\) 从b个岛里选a个的方法数 \(\times\) a个数的总排列数。( \(a \le b\) ),
也就是\(\displaystyle\sum_{i=0}^{a}C_a^i\times C_b^i \times i!\)

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 5003, mod = 998244353;
int f[N], g[N];
inline int qpow(int a, int b) {
	rg int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
inline void init() {
	f[0] = g[0] = 1;
	for (rg int i = 1; i < N; i++) {
		f[i] = f[i - 1] * i % mod;
		g[i] = g[i - 1] * qpow(i, mod - 2) % mod;
	}
}
inline int C(int n, int m) {
	return f[n] * g[m] % mod * g[n - m] % mod;
}
inline int solve(int a, int b) {
	rg int res = 0;
	for (rg int i = 0; i <= min(a, b); i++) {
		res = (res + C(a, i) * C(b, i) % mod * f[i] % mod) % mod;
	}
	return res % mod;
}
int a, b, c;
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	init();
	cin >> a >> b >> c;
	cout << solve(a, b) * solve(a, c) % mod * solve(b, c) % mod << "\n";
	return qwq;
}

例题3:Lengthening Sticks

首先计算出所有情况数,然后减去不合法的情况数即可。
对于总情况数:
枚举可发现规律:\(tot=\displaystyle\sum_{i=1}^{l+1}\frac{(i + 1)\cdot i}{2}\)
对于不合法情况:
依次枚举三条边作为最长边的不合法情况,即最长边\(\ge\)其余两边之和。
注意特判即使加上全部l也比最长边小的情况,直接输出0。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long 
using namespace std;
const int N = 3e5 + 3;
int a, b, c, l;
int tot;
inline int solve(int a, int b, int c) {  //最长边为a 
	rg int d = b + c - a, nl = l, res = 0;
	if (d > 0) {
		nl -= d;
		a += d;
	}
	for (rg int i = 0; i <= nl; i++) {  //分给a的长度
		/*for (rg int j = 0; j <= nl - i && b + c + j <= a + i; j++) {
			res += j + 1;
		}*/
		rg int maxx = min(nl - i, a + i - b - c);
		res += (maxx + 2) * (maxx + 1) / 2;
	}
	return res;
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> a >> b >> c >> l;
	rg int maxx = max(a, max(b, c));
	if (maxx >= a + b + c - maxx + l) {
		cout << 0 << "\n";
		return qwq;
	}
	for (rg int i = 1; i <= l + 1; i++) {
		tot += (i + 1) * i / 2;
	}
	cout << tot - solve(a, b, c) - solve(b, a, c) - solve(c, a, b) << "\n";
	return qwq;
}

例题4:[ABC221E] LEQ

因为只要求选出的序列中第一个数小于等于最后一个数,所以中间的数就不用管了,所以满足\(a_i\le a_j\)的点对\((i,j)\)的贡献为\(2^{j-i-1}\)
可以使用离散化加树状数组维护。令\(ans_j\)表示\(a'_k=a_j\)时的答案有:
\(\begin{aligned}\displaystyle ans_j&=\sum 2^{j-i-1}\\&=2^{j-1}\sum2^{-i}\\ &= 2^j-1\sum inv(2^i)\end{aligned}\)
这样就将求和分成了分别只关于\(i,j\)的项的乘机,每次在树状数组上添加\(inv(2^i)\)即可。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
#define lowbit(x) x & (-x)
using namespace std;
const int N = 3e5 + 3, mod = 998244353;
int n, tot;
int a[N], b[N], t[N];
int ans;
inline int qpow(int a, int b) {
	rg int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
inline int inv(int x) { return qpow(x, mod - 2); }
inline void add(int k, int x) {
	for (rg int i = k; i <= n; i += lowbit(i)) {
		t[i] = (t[i] + x) % mod;
	}
}
inline int query(int k) {
	rg int res = 0;
	for (rg int i = k; i; i -= lowbit(i)) {
		res = (res + t[i]) % mod;
	}
	return res;
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> n;
	for (rg int i = 1; i <= n; i++) {
		cin >> a[i];
		b[i] = a[i];
	}
	sort(b + 1, b + n + 1);
	tot = unique(b + 1, b + n + 1) - (b + 1);
	for (rg int i = 1; i <= n; i++) {
		a[i] = lower_bound(b + 1, b + tot + 1, a[i]) - b;
	}
	for (rg int j = 1; j <= n; j++) {
		ans = (ans + qpow(2, j - 1) * query(a[j]) % mod) % mod;
		add(a[j], inv(qpow(2, j)));
	}
	cout << ans << "\n";
	return qwq;
}

例题5:[ABC243F] Lottery

设一个事件A发生的概率为\(P(A)\),则取p次这个事件,恰好发生k次的概率是\(C_{p}^{k}\times P(A)^k\times (1-P(A))^{p-k}\)
\(dp_{i,j,k}\)表示前i个奖品,抽了j次,有k种不同奖品的概率,\(f_i=\frac{w_i}{\sum w}\)
设在前j次抽奖中抽到了p个奖品i,则它的贡献为\(C_{j}^{p}\times f_i^p\times dp_{i-1,j-p,k-1}\),这里的\(dp_{i-1,j-p,k-1}\)表示剩下的数全抽i以前的奖品的概率。
初始值\(dp_{0,0,0}=1\),答案就是\(dp_{n,k,m}\)

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 53, mod = 998244353;
int n, m, k;
int w[N], sum, f[N];
int dp[N][N][N];
inline int qpow(int a, int b) {
	rg int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
inline int inv(int x) { return qpow(x, mod - 2); }
int fac[N];
inline void init() {
	fac[0] = 1;
	for (rg int i = 1; i <= k; i++) {
		fac[i] = fac[i - 1] * i % mod;
	}
}
inline int C(int a, int b) {
	if (a < b) return qwq;
	return fac[a] * inv(fac[b]) % mod * inv(fac[a - b]) % mod;
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> n >> m >> k;
	init();
	for (rg int i = 1; i <= n; i++) {
		cin >> w[i];
		sum += w[i];
	}
	rg int ret = inv(sum);
	for (rg int i = 1; i <= n; i++) {
		f[i] = w[i] * ret % mod;
	}
	dp[0][0][0] = 1;
	for (rg int i = 1; i <= n; i++) {
		dp[i][0][0] = dp[i - 1][0][0];
		for (rg int j = 1; j <= k; j++) {
			for (rg int kk = 1; kk <= i; kk++) {
				dp[i][j][kk] = dp[i - 1][j][kk];
				for (rg int p = 1; p <= j; p++) {
					dp[i][j][kk] = (dp[i][j][kk] + dp[i - 1][j - p][kk - 1] * C(j, p) % mod * qpow(f[i], p) % mod) % mod;
				}
			}
		}
	}
	cout << dp[n][k][m] << "\n";
	return qwq;
}

例题6:Claris的剑

打了好久的公式没保存被卡掉了,不想打了……
粘个题解吧……

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 2e6 + 3, mod = 1e9 + 7;
int n, m;
int c[N], cnt, tot;
int ans = 1;
inline int qpow(int a, int b) {
	rg int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
int fac[N], inv[N];
inline void init() {
	fac[0] = 1;
	for (rg int i = 1; i < N; i++) {
		fac[i] = fac[i - 1] * i % mod;
	}
	inv[N - 1] = qpow(fac[N - 1], mod - 2);
	for (rg int i = N - 2; i >= 0; i--) {
		inv[i] = inv[i + 1] * (i + 1) % mod;
	}
}
inline int C(int a, int b) {
	if (a < b) return qwq;
	return fac[a] * inv[b] % mod * inv[a - b] % mod;
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> n >> m;
	init();
	for (rg int j = 0; j <= min(m - 2, n - 2); j++) {
		ans = (ans + C((n - j - 2) / 2 + j + 1, j + 1)) % mod;
	}
	for (rg int j = 0; j <= min(m - 2, n - 3); j++) {
		ans = (ans + C((n - j - 3) / 2 + j + 1, j + 1)) % mod;
	}
	cout << ans << "\n";
	return qwq;
}

例题7:Array

有趣的组合题。
首先,由于单调不增和单调不降是对称的,所以我们只需要计算一个即可。
我们用集合\(S=\{x_1,x_2,\cdots,x_n\}\)来唯一的表示这个序列,其中\(x_i\)表示数字i的数量。所以\(x_1+x_2+\cdots+x_n=n\)。因为S和原序列存在对应关系,所以求出有多少种S就能知道有多少种不同的序列。
因为\(\displaystyle\sum_{i=1}^{n}x_i=n\),所以问题又可以转化为有n个1,在中间插入n-1个隔板的方案数(每相邻两个隔板间的1的个数就是对应的\(x_i\)的值)。比如,对于下面这种方案\(|11||1|\),有\(n=3,x_1=2,x_2=0,x_3=1\)。当然实际计算的时候为了简明就不计两端的隔板。
于是对于这个问题可以这么理解:假设一共有\(2n-1\)个位置,其中有n个位置放1,另外n-1个位置放隔板,显然有\(C_{2n-1}^{n}\)种方法放1。
回到原问题。由上面得到的单调不降(单调不升)序列有\(C_{2n-1}^{n}\)种不同的序列,那两种情况合并就有\(2\times C_{2n-1}^{n}\)种。又因为当所有数一样(如\(1,1,1\))的情况会算重,因此要减n。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 2e5 + 3, mod = 1e9 + 7;
int n;
inline int qpow(int a, int b) {
	rg int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
int fac[N], inv[N];
inline void init() {
	fac[0] = 1;
	for (rg int i = 1; i < N; i++) fac[i] = fac[i - 1] * i % mod;
	inv[N - 1] = qpow(fac[N - 1], mod - 2);
	for (rg int i = N - 2; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
}
inline int C(int a, int b) {
	if (a < b) return qwq;
	return fac[a] * inv[b] % mod * inv[a - b] % mod;
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> n;
	init();
	rg int res = C(2 * n - 1, n);
	rg int ans = (res * 2 % mod - n) % mod;
	cout << ans << "\n";
	return qwq;
}

补充:在这个题中,还有一种\(O(n)\)做法:\(\displaystyle ans=\sum_{i=1}^{n} C_{n-1}^{i-1}\cdot C_n^i\),于是可以得到一个神奇的式子:\(\displaystyle\sum_{i=1}^{n} C_{n-1}^{i-1}\cdot C_n^i=C_{2n-1}^{n}\)。但是严谨证明并不会,猜测或许和范德蒙德卷积有关。

例题8:Lucky Subsequence

\(f_{i,j}\)表示前i种幸运数中选j个的方案数。可以得到:\(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}\times num_i\),其中\(num_i\)表示第i种幸运数的个数。
可以将第一位滚掉,这样就开得下了。
于是答案为\(\displaystyle \sum_{i=0}^{k}f_i\times C_{sum}^{k-i}\),其中sum为非幸运数的个数。

#include<bits/stdc++.h>
#define rg register
#define qwq 0
#define int long long
using namespace std;
const int N = 1e5 + 3, mod = 1e9 + 7;
int n, k;
inline int qpow(int a, int b) {
	rg int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}
int fac[N], inv[N];
inline void init() {
	fac[0] = 1;
	for (rg int i = 1; i < N; i++) fac[i] = fac[i - 1] * i % mod;
	inv[N - 1] = qpow(fac[N - 1], mod - 2);
	for (rg int i = N - 2; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
}
inline int C(int a, int b) {
	if (a < b) return qwq;
	return fac[a] * inv[b] % mod * inv[a - b] % mod;
}
inline bool check(int x) {
	while (x) {
		if (x % 10 != 4 && x % 10 != 7) {
			return false;
		}
		x /= 10;
	}
	return true;
}
int ans, res;
int a[N];
int num[N], cnt, last, sum;
int f[N];
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	init();
	cin >> n >> k;
	for (rg int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	sort(a + 1, a + n + 1);
	for (rg int i = 1; i <= n; i++) {
		if (check(a[i])) {
			if (a[i] == last) {
				num[cnt]++;
			}
			else {
				num[++cnt]++;
				last = a[i];
			}
		}
	}
	f[0] = 1;
	for (rg int i = 1; i <= cnt; i++) {
		for (rg int j = cnt; j >= 1; j--) {
			f[j] = (f[j] + f[j - 1] * num[i]) % mod;
		}
	}
	for (rg int i = 1; i <= n; i++) {
		sum += num[i];
	}
	sum = n - sum;
	for (rg int i = 0; i <= k; i++) {
		ans = (ans + f[i] * C(sum, k - i) % mod) % mod;
	}
	cout << ans << "\n";
	return qwq;
}
posted @ 2024-06-11 22:24  柏_yj  阅读(38)  评论(0)    收藏  举报