[算法] Lucas 定理


定理内容

对于给定非负整数 \(n,m\) 和质数 \(p\),将 \(n,m\) 转化为 \(p\) 进制表达,即为:

\(n=n_kp^k+n_{k-1}p^{k-1}+\dots+n_1p+n_0\)

\(m=m_kp^k+m_{k-1}p^{k-1}+\dots+m_1p+m_0\)

则有恒等式:

\(\dbinom{n}{m}\equiv\prod^k_{i=0}\dbinom{n_i}{m_i} (mod\) \(p)\)

hdu3037 Saving Beans

题目链接。

题目大意

\(n\) 个盒子,你可以拥有 \(1\)\(m\) 个小球,将这些小球放入盒子中,有多少方案,对于 \(p\) 取模。

思路

对于其中一个 \(m\),即有 \(n-1\) 个板,\(m\) 个球,用隔板发得到方案数为 \(\dbinom{n+m-1}{n-1}\)

则总方案数为:\(\sum^m_{i=0}\dbinom{n+i-1}{n-1}=\sum^{n+m-1}_{i=n-1}\dbinom{i}{n-1}\)

由边下项公式得到:\(\dbinom{n+m}{n}\)

由于题目中 \(n\)\(m\) 较大,则用 Lucas 定理来优化组合数计算。

Code

#include 
#include 
using namespace std;
#define Irelia 0
const int MAXN = 1e5 + 5;
int fac[MAXN], inv[MAXN];
int n, m, p;
int ksm(int x, int y) {
	int res = 1;
	while (y) {
		if (y & 1) res = (1LL * res * x) % p;
		x = (1LL * x * x) % p;
		y >>= 1;
	}
	return res;
}
void Init() {
	fac[0] = 1;
	for (int i = 1; i < p; i++) fac[i] = (1LL * fac[i - 1] * i) % p;
	inv[p - 1] = ksm(fac[p - 1], p - 2);
	for (int i = p - 2; i >= 0; i--) inv[i] = (1LL * inv[i + 1] * (i + 1)) % p;
}
int C(int x, int y) {
	if (x < y) return 0;
	return 1LL * fac[x] * inv[x - y] % p * inv[y] % p;
}
void Work() {
	scanf("%d %d %d", &n, &m, &p);
	Init();
	n = n + m;
	int ans = 1;
	for (int i = 0; i <= 31; i++) {
		ans = 1LL * ans * C(n % p, m % p) % p;
		n /= p;
		m /= p;
	}
	printf("%d\n", ans);
}
int main() {
	int T;
	for (scanf("%d", &T); T; T--) Work();
	return Irelia;
}

HDU5794 A Simple Chess

题目链接。

题意

再平面直角坐标系中,从 \((1,1)\) 用马走日的规则走到 \((h,w)\) ,且有 \(n\) 个黑点不能走,每次只能向上或向右走,则共有多少种方案数。答案对 \(110119\) 取模。

思路

\(dp[i]\) 为从 \((1,1)\) 走到 \((x[i],y[i])\) 且不经过其他黑点的方案数。

简化版问题

若从点 \((0,0)\) 走到 \((a,b)\),且没有任何障碍的方案数。

\((+1,+2)\) 走了 \(x\) 步,\((+2,+1)\) 走了 \(y\) 步,则有方程:

\[\begin{cases} a=x+2y\\ b=y+2x \end{cases} \]

解之得:

\[\begin{cases} x=\frac{2b-a}{3}\\ y=\frac{2a-b}{3} \end{cases} \]

那么答案为 \(\dbinom{x+y}{x}\),即为共有 \(x+y\) 步,选其中 \(x\) 步走 \((+2,+1)\)。注意,其中 \(x,y\) 为正整数。

然后是这道题的状态转移。

首先算出当前点 \(i\) 不考虑有黑点,有多少种方案,转化为简化版问题解决即可。

然后考虑算重的部分。在这个点的左下方的一个点 \(j\) ,第一个经过的点为 \(j\) 且最终到达了 \(i\) 。那么路径就拆成了两部分,\((1,1)\)\(j\)\(j\)\(i\) ,第一部分就为 \(dp[j]\),第二部分转化为简化版问题求解。

由于求出 \(dp[i]\) 时应先求到 \(dp[j]\),所以要对原数组排序,保证 \(i\) 的左下方的点都已经求过了。

由于模数较小且 \(h,w\) 太大以至于无法预处理出组合数,所以用到 Lucas 定理来优化。

Code

为了方便设置起点为 \((0,0)\)

#include 
#include 
using namespace std;
#define int long long
#define Irelia 0
const int MAXN = 2e5 + 5;
const int MOD = 110119;
int fac[MAXN], inv[MAXN], dp[MAXN];
int h, w, n;
int Case = 0;
struct Node {
	int x, y;
	friend bool operator < (Node x, Node y) {
		return x.x != y.x ? x.x < y.x : x.y < y.y;
	}
};
Node a[MAXN];
int ksm(int x, int y) {
	int res = 1;
	while (y) {
		if (y & 1) res = (1LL * res * x) % MOD;
		x = (1LL * x * x) % MOD;
		y >>= 1;
	}
	return res;
}
int c(int x, int y) {
	if (x < y) return 0;
	return 1LL * fac[x] * inv[x - y] % MOD * inv[y] % MOD;
}
int C(int x, int y) {
	int res = 1;
	for (int i = 0; i <= 20; i++) {
		res = 1LL * res * c(x % MOD, y % MOD) % MOD;
		x /= MOD;
		y /= MOD;
	}
	return res;
}
void Work() {
	n++;
	a[n].x = h, a[n].y = w;
	for (int i = 1; i <= n; i++) dp[i] = 0;
	sort(a + 1, a + 1 + n);
	for (int i = 1; i <= n; i++) {
		int A = a[i].x;
		int B = a[i].y;
		if ((2 * A - B) % 3 != 0 || (2 * B - A) % 3 != 0 || 2 * A - B < 0 || 2 * B - A < 0) continue;
		int x = (2 * B - A) / 3;
		int y = (2 * A - B) / 3;
		dp[i] = C(x + y, x);
		for (int j = 1; j < i; j++) {
			if (a[j].x > a[i].x || a[j].y > a[i].y) continue;
			A = a[i].x - a[j].x;
			B = a[i].y - a[j].y;
			if ((2 * A - B) % 3 != 0 || (2 * B - A) % 3 != 0 || 2 * A - B < 0 || 2 * B - A < 0) continue;
			x = (2 * B - A) / 3;
			y = (2 * A - B) / 3;
			dp[i] = (-1LL * dp[j] * C(x + y, x) % MOD + dp[i]) % MOD;
		}
	}
	dp[n] = (dp[n] + MOD) % MOD;
	printf("Case #%lld: %lld\n", ++Case, dp[n]);
}
signed main() {
	fac[0] = 1;
	for (int i = 1; i < MOD; i++) fac[i] = (1LL * fac[i - 1] * i) % MOD;
	inv[MOD - 1] = ksm(fac[MOD - 1], MOD - 2);
	for (int i = MOD - 2; i >= 0; i--) inv[i] = (1LL * inv[i + 1] * (i + 1)) % MOD;
	while (scanf("%lld", &h) != EOF) {
		scanf("%lld %lld", &w, &n);
		h--, w--;
		for (int i = 1; i <= n; i++) {
			scanf("%lld %lld", &a[i].x, &a[i].y);
			a[i].x--;
			a[i].y--;
		}
		Work();
	}
	return Irelia;
}