[算法] 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;
}