正睿25国庆 Day2 数论

组合数学与容斥

组合数

几个比较出名的公式:

\[\binom{n}{m} = \frac{n!}{m!(n-m)!} \]

\(\sum_{i=1}^m a_i = n, a_i \geq 1: \binom{n-1}{m-1}\)
线性递推常用: \(\binom{n}{m} = \binom{n-1}{m} + \binom{n-1}{m-1}\)
上指标前缀和: \(\sum_{i=0}^n \binom{i}{m} = \binom{n+1}{m+1}\)
范德蒙德卷积: \(\sum_{i=0}^n \binom{n}{i}\binom{m}{k-i} = \binom{n+m}{k}\)
Lucas 定理: \(\binom{n}{m} \equiv \binom{n \bmod p}{m \bmod p}\binom{\lfloor n/p \rfloor}{\lfloor m/p \rfloor} \pmod p\)

洛谷 P7481 梦现时刻

给定 \(n,m\) ,保证 \(m\le n\),令 \(F(a,b)=\sum_{i=0}^{b}\binom{b}{i}\binom{n-i}{a}\)

\(\bigoplus_{a=1}^{m}\bigoplus_{b=1}^{m}(F(a,b) \bmod 998244353)\)

其中 \(\oplus\) 表示异或运算。

\(m\le 5000,n\le 10^9\)

稍微分析一下,我们希望把这个 \(F(a,b)\) 用若干个 \(F(a\pm 1, b\pm 1)\) 表示出来,这样就能递推了。

先把 \(F(a,b)\) 拆成 \(F(a,b)=[{b-1\choose i}+{b-1\choose i-1}]{n-i\choose a}\),这样我们发现括号里的第一项与右边这一项乘起来恰好就是 \(F(a,b-1)\)。但是我们还剩下一个 \({b-1\choose i-1}\) 没有处理。由于它有一个 \(i-1\),所以我们想要右边也出现一个 \(i-1\),于是将右边拆成 \({n-i+1\choose a+1}-{n-i\choose a+1}\)

于是你发现,有 \(F(a,b)=\sum_i [{b - 1 \choose i}+{b-1\choose i-1}][{n-i+1 \choose a+1}-{n-i \choose a+1}]\)。不妨从左到右标成 \(A,B,C,D\),我们要求的 \(F(a,b)\) 就等于 \((A+B)(C-D)\),再分析一下发现有 \(F(a,b-1)=AC-AD,F(a+1,b-1)=AD=BC,F(a+1,b)=AD+BD\),于是凑一下系数就得到:

\[\boxed{F(a,b)=F(a,b-1)-F(a+1,b)+2F(a+1,b-1)} \]

于是就可以递推了。时间复杂度 \(O(m^2)\)

const int MAXN = 5e3 + 5, mod = 998244353;
int n, m, f[MAXN][MAXN], C[MAXN][MAXN], T[MAXN]; // T[i] = (n-i \choose m)

int quickpow(int a, int b) {
	int ret = 1;
	while (b) {
		if (b & 1) ret = ret * a % mod;
		a = a * a % mod; b >>= 1;
	}
	return ret;
}

void work() {
	cin >> n >> m;
	f[1][0] = n;
	for (int a = 2; a <= m; ++a) {
		f[a][0] = f[a - 1][0] * (n - a + 1) % mod * quickpow(a, mod - 2) % mod;
	}
	for (int i = 0; i <= m; ++i) 
		C[i][0] = 1;
	for (int i = 1; i <= m; ++i) {
		for (int j = 1; j <= m; ++j) {
			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
		}
	}
	int facm = 1; T[0] = 1;
	for (int i = 1; i <= m; ++i) {
		facm = facm * i % mod;
		T[0] = T[0] * (n - m + i) % mod;
	}
	T[0] = T[0] * quickpow(facm, mod - 2) % mod;
	for (int i = 1; i <= m; ++i) {
		T[i] = T[i - 1] * (n - i + 1 - m) % mod * quickpow(n - i + 1, mod - 2) % mod;
	}
	for (int b = 1; b <= m; ++b) {
		for (int i = 0; i <= b; ++i) {
			f[m][b] = (f[m][b] + C[b][i] * T[i] % mod) % mod;
		}
	}
	for (int b = 1; b <= m; ++b) {
		for (int a = m - 1; a; --a) {
			f[a][b] = (f[a][b - 1] - f[a + 1][b] + 2 * f[a + 1][b - 1]) % mod;
			f[a][b] = (f[a][b] + mod) % mod;
		}
	}
	int ans = 0;
	for (int a = 1; a <= m; ++a) {
		for (int b = 1; b <= m; ++b) {
			ans ^= f[a][b];
		}
	}
	cout << ans << endl;
}

/*
g++ -o LG7481 LG7481.cpp -O2 -std=c++14 -Wall -DLOCAL_TEST && LG7481.exe < test.in
*/

组合数

求 $ \sum_{a_1=l_1}^{r_1} \dots \sum_{a_n=l_n}^{r_n} \binom{m}{a_1+a_2+\dots+a_n} \pmod p$

\(n \le 18, 1 \le m \le 10^{18}, 1 \le l_i \le r_i \le 10^{16}, p \in \{2, 3, 5, 7\}\)

这个题是正睿私题没地方交,所以口胡一下。

首先我们遇到这种取值范围是一个区间的题可以容斥一下,但是这个题容斥比较厉害的地方在于他是将答案容斥为 \([a_i \ge l_i] - [a_i>r_i]\),这么做是为了方便后面数位 DP 计数。然后现在问题转换为每一个 \(a_i\) 有一个 \(a_i\ge b_i\) 的限制。

接下来,我们枚举 \(c=\sum a_i\),对于一个固定的 \(c\),我们发现其实他的贡献是确定的,也就是 \({m\choose c}{c-sum+n-1\choose n-1}\),其中 \(sum=\sum b_i\)。第一项是题目给的系数,第二项是把这多出来的 \(c-sum\) 个分配到这些 \(a_i\) 的方案数。

然后现在也就是要算这个东西对所有 \(c\) 的贡献,直接用 Lucas 定理+数位 DP \(c\)\(p\) 进制下的每一位即可。

时间复杂度 \(O(2^n p\log_{p} m)\)。由于是口胡没有代码。

LOJ6300 博弈论与概率统计

Alice 和 Bob 在玩一个双人游戏。每一轮中,Alice 有 \(p\) 的概率胜利,\(1 - p\) 的概率失败,不会出现平局。

双方初始时各有 \(0\) 分,当一个人胜利的时候,他会获得一分,失败则扣掉一分。遗憾的是,博弈论世界的人目前是无法理解负数的,因此,如果某个人输掉一轮比赛的时候他只有 \(0\) 分,那么他就不会被扣分(对方会照常加一分)。游戏一共要进行 \(N + M\) 轮,Alice 想请你帮她算算在游戏结束时她的得分的数学期望。

“这算啥,我小 \(L\) 分分钟搞定!”。比小 \(L\) 更熟练的你当然也是随手就算出来了,但就在你打算告诉 Alice 答案之前,博弈论世界之神——temporaryDO 出现了,他给大家带来了一个重要信息:这 \(N + M\) 轮游戏中,Alice 恰好赢了 \(N\) 轮!

熟知条件概率那套理论的你立刻注意到,你需要修改自己的计算方法来得到正确的答案了。

为了避免精度问题,请将结果对 \(10^9 + 7\) 取模。即,我们的数据保证答案是一个有理数 \(\frac{p}{q}\),且有 \(10^9 + 7 \nmid q\),你只需要找到一个整数 \(x \in [0, 10^9 + 7)\) 使得 \(qx \equiv p \pmod{10^9 + 7}\) 即可。

这个题的转化非常神。

首先发现给的这个 \(p\) 是没有用的,因为赢了输了多少局都是确定的。

然后,如果我们把初始状态看成一个坐标系上的 \((0,0)\),终点看成 \((n,m)\),相当于是我们每次赢就往右走一步,输就往上走一步。每次赢了就 \(x\gets x+1\),输了就 \(x\gets \max(x-1,0)\)。我们现在要求 \(E(x)\)

发现得分不好确定的原因是,我们要对 \(0\)\(\max\)。如果没有这个对 \(0\)\(\max\) 的操作,我们的得分就是 \(n-m\)

考虑这个对 \(0\)\(\max\) 的操作,在我们的坐标系上是长成什么样的。考虑第一次得分为 \(0\) 且下一局输了的情况,这时由于得分为 \(0\),我们一定在 \(y=x\) 这条线上,然后再往上走一步,但是得分不会 \(-1\),于是,相当于是第一次碰到 \(y=x+1\) 这条线的时候,我们就给得分 \(+1\)。同理,第二次得分为 \(0\) 且下一局输了的情况可以看成是在 \(y=x+1\) 这条线上再向上走一步,也就是第一次碰到 \(y=x+2\)

由于我们有 $E(x)=P(x\ge 1)+P(x\ge 2)+\cdots $,于是相当于是要求有多少条路径满足碰到了 \(y=x+1,y=x+2,\cdots\) 这些直线。

怎么求呢?根据反射原理(就是用来推卡特兰数的那个),如果我们想求经过了 \(y=x+k\) 上方到达 \((n,m)\) 的路径数,我们可以把它按照第一次碰到 \(y=x+k\) 的点分成前后两部分,第一部分路径不变,第二部分路径对 \(y=x+k\) 这条路径做轴对称反射,这样原先在 \((n,m)\) 的终点就变成了 \((m-k,n+k)\)。于是我们发现,任意一条最终终点在 \((m-k,n+k)\) 的路径,在通过 \(y=x+k\) 反射回去后,就一一对应了一条原先的合法路径。所以,经过 \(y=x+k\) 的路径数,就是 \(\binom{m+n}{n+k}\)

\(n\ge m\) 的时候,总的贡献数相当于是 \((n-m)\times \binom{n+m}{n} + \sum\limits_{k=1}^m \binom{m+n}{n+k}\),后面这项化简一下得到 \(\sum\limits_{k=0}^{m-1}\binom{m+n}{k}\)。当 \(n<m\) 的时候同理,此时 \(k\) 需要 \(\ge m-n\)(不然得分就会为负数,显然不合法),此时答案为 \(\sum\limits_{k=0}^{n-1}\binom{n+m}{k}\)。由于我们要求的是期望,所以上面这两种情况都要除以一个 \(\binom{n+m}{n}\) 的总方案数。

于是现在问题就变成了,给定 \(x,y\),如何快速求 \(\sum\limits_{k=0}^x \binom{y}{k}\)。这个问题可以用莫队解决。当 \(x\) 变成 \(x\pm 1\) 的时候,相当于是增加或减少了一项 \(\binom{y}{x/x+1}\),直接统计即可。当 \(y\) 变成 \(y+1\) 的时候,相当于多出了 \(\sum\limits_{k=0}^x \binom{y+1}{k}-\binom{y}{k}=\sum\limits_{k=0}^x \binom{y}{k-1}\),观察到这个东西相当于是原先我们的 \(ans\) 少了 \(\binom{y}{x}\) 这一项,所以答案就会增加 \(ans-\binom{y}{x}\),也就是 \(ans=2ans-\binom{y}{x}\)。减少同理,最后 \(ans=\frac{ans+\binom{y-1}{x}}2\)

最终时间复杂度 \(O(n\sqrt n)\)

const int MAXN = 5e5 + 5, mod = 1e9 + 7, N = 5e5;
int q, _, bel[MAXN], L[MAXN], R[MAXN], ans[MAXN], now, inv2;
int fac[MAXN], ifac[MAXN];

struct _query {
	int x, y, n, m, id;
	bool operator < (const _query b) const {
		if (bel[x] == bel[b.x]) {
			if (bel[x] & 1) return y < b.y;
			else return y > b.y;
		}
		return bel[x] < bel[b.x];
	}
} qr[MAXN];

int quickpow(int a, int b) {
	int ret = 1;
	while (b) {
		if (b & 1) ret = 1ll * ret * a % mod;
		a = 1ll * a * a % mod; b >>= 1;
	}
	return ret;
}

int C(int n, int m) { // n choose m
	if (n < m) return 0;
	return 1ll * fac[n] * ifac[n - m] % mod * ifac[m] % mod;
}

int Cinv(int n, int m) { // 1 / (n choose m)
	if (n < m) return 0;
	return 1ll * ifac[n] * fac[n - m] % mod * fac[m] % mod;
}

void addx(int x, int y) {
	now = (now + C(y, x)) % mod;
}

void delx(int x, int y) {
	now = ((now - C(y, x)) % mod + mod) % mod;
}

void addy(int x, int y) {
	now = ((2ll * now % mod - C(y - 1, x)) % mod + mod) % mod;
}

void dely(int x, int y) {
	now = 1ll * (now + C(y - 1, x)) % mod * inv2 % mod;
}

void work() {
	cin >> q >> _;
	int B = sqrt(N), cnt = 0;
	for (int i = 1; i <= N; i += B) {
		cnt++;
		L[cnt] = i;
		R[cnt] = min(i + B - 1, N);
		for (int j = L[cnt]; j <= R[cnt]; ++j) {
			bel[j] = cnt;
		}
	}
	fac[0] = 1;
	for (int i = 1; i <= N; ++i)
		fac[i] = 1ll * fac[i - 1] * i % mod;
	ifac[N] = quickpow(fac[N], mod - 2);
	for (int i = N - 1; i >= 0; --i)
		ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mod;
	inv2 = quickpow(2, mod - 2);

	for (int i = 1; i <= q; ++i) {
		int n, m; cin >> n >> m;
		if (n < m) {
			qr[i] = {n - 1, n + m, n, m, i};
		} else {
			qr[i] = {m - 1, n + m, n, m, i};
		}
	}
	sort(qr + 1, qr + 1 + q);

	int l = 0, r = 1; now = 1;
	for (int i = 1; i <= q; ++i) {
		while (r < qr[i].y) {
			++r; addy(l, r);
		}
		while (l > qr[i].x) {
			delx(l, r); l--;
		}
		while (r > qr[i].y) {
			dely(l, r); r--;
		}
		while (l < qr[i].x) {
			l++; addx(l, r);
		}
		ans[qr[i].id] = 1ll * now * Cinv(qr[i].n + qr[i].m, qr[i].n) % mod;
		if (qr[i].n > qr[i].m) ans[qr[i].id] = (ans[qr[i].id] + (qr[i].n - qr[i].m)) % mod;
	} 
	for (int i = 1; i <= q; ++i)
		cout << ans[i] << endl;
}
posted @ 2025-11-10 16:27  小蛐蛐awa  阅读(8)  评论(0)    收藏  举报