P5239 回忆京都

题目描述


查询\(q\)次,每次输入\((n,m)\),计算输出:

\[\sum_{i=1}^n\sum_{j=1}^mC_j^i\pmod{19260817}\quad\quad C_j^i=\left(\begin{matrix}j\\i\end{matrix}\right) \]

尝试的数学方法


  • 我们知道对于上述题目描述中的多次求和,我们可以交换求和顺序:

    \[S(n,m)=\sum_{i=1}^n\sum_{j=1}^m C_j^i\pmod{19260817}=\sum_{j=1}^m\sum_{i=1}^n C_j^i\pmod{19260817} \]

  • 由二项式系数的全体和可知:

    \[\sum_{i=0}^n C_n^i=2^i\rightarrow \sum_{i=1}^n C_n^i=2^i-1 \]

  • 拿到这道题目中就有:

    \[S(n,m)=\sum_{j=1}^{min\{n,m\}}(2^j-1)+\sum_{j=n+1}^m\sum_{i=1}^nC_j^i\pmod{19260817} \]

    而加法中的第一项,我们的上界就是\(min\{n,m\}\),但是如果出现了\(m>n\)的情况,我们就不能用\(2^j-1\)来统一内层的求和了,此时正确的结果为\(2^j-1-\sum_{i=n+1}^j C_j^i\),而后者的计算我们又得回到二项式系数本身上来。
  • 整理得:

    \[S(n.m)=\sum_{j=1}^m(2^j-1)-\sum_{j=n+1}^m\sum_{i=n+1}^jC_j^i \]

  • 关于如何计算二项式系数这个问题,编程语言常通过杨辉三角或者说是动态规划进行求解。这里仅从代数的角度进行递推公式的计算:

    \[\left(\begin{matrix}n\\k\end{matrix}\right)=\left(\begin{matrix}n-1\\k-1\end{matrix}\right)+\left(\begin{matrix}n-1\\k\end{matrix}\right);\left(\begin{matrix}n\\0\end{matrix}\right)=\left(\begin{matrix}n\\n\end{matrix}\right)=1 \]

// C[j][i] → j 选 i
class Solution {
public:
	long long nCr(int n, int r) {
		if (r > n) return 0LL;
		if (n == r || r == 0) return 1LL;
		vector<vector<long long>> C(n + 1, vector<long long>(r + 1, 0));
		for (int j = 0; j <= n; j++) {
			C[j][0] = 1;
			for (int i = 1; i <= j; i++) {
				C[j][i] = C[j-1][i-1] + C[j-1][i];
			}
		}
		return C[n][r];
	}
}
  • 最终的题目代码实现:
#include <iostream>

const int MOD = 19260817;

int dp[1005][1005];
int sumPow[1005];

void init() {
	long long p = 2;
	sumPow[0] = 0;
	for (int j = 1; j <= 1000; j++) {
		sumPow[j] = (sumPow[j-1] + p - 1) % MOD;
		p = (p * 2) % MOD;
	}
	// 存储 j 选 i
	for (int j = 0; j <= 1000; j++) {
		dp[j][0] = 1;
		for (int i = 1; i <= j; i++) {
			dp[j][i] = (dp[j-1][i-1]+dp[j-1][i]) % MOD;
		}
	}
	// 存储每一行的后缀和(横向从右往左卷)
	for (int j = 1; j <= 1000; j++) {
		for (int i = j - 1; i >= 1; i--) {
			dp[j][i] = (dp[j][i+1] + dp[j][i]) % MOD;
		}
	}
	
	// 存储后缀和的纵向累加(纵向从上往下卷)
	for (int i = 1; i <= 1000; i++) {
		for (int m = i + 1; m <= 1000; m++) {
			dp[m][i] = (dp[m-1][i] + dp[m][i]) % MOD;
		}
	}
}

int main() {
	init();
	int q = 0;
	scnaf("%d", &q);
	for (int i = 0; i < q; i++) {
		int n = 0;
		int m = 0;
		scanf("%d %d", &n, &m);
		long long ans = 0;
		if (m <= n) {
			ans = sumPow[m];
		} else {
			ans = (sumPow[m] - dp[m][n+1]) % MOD;
			ans = (ans + MOD) % MOD;
		}
		printf("%lld", ans);
	}
	return 0;
}

  • 初始化时间复杂度:\(O(max^2\{n,m\})\);
  • 单次查询时间复杂度:\(O(1)\);
  • 总时间复杂度:\(O(max^2\{n,m\}+q)\);
  • 空间复杂度:\(O(max^2\{n,m\})\);

二维前缀和


另外,由于\(n,m\leq 10^3\),我们可以利用二维前缀和直接计算出所有的\((n,m)\)的答案,在查询时以\(O(1)\)的时间复杂度进行响应。

根据前面的探讨,我们在计算\(\left(\begin{matrix}n\\r\end{matrix}\right)\)中,其实已经计算出完整的\(C_j^i\)的二维数组了,即\(C[i][j]=C_i^j\),而我们要求解的就是它的一个左上角矩阵和,即二维前缀和:

\[S[n][m]=\sum_{i=1}^n\sum_{j=1}^m C[i][j] \]

利用二维前缀和的经典递推公式:

\[S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+C[j][i] \]

整个计算过程中,涉及到的计算有:

  • \(O(n\times m)\)利用\(C_j^i=C_{j-1}^{i-1}+C_{j-1}^i\)递推生成杨辉三角矩阵;
  • \(O(n\times m)\)计算出所有的\(S[i][j]\);
  • 对于\(q\)次查询,直接输出\(S[n][m]\)即可,每次查询都是\(O(1)\)。
#include <iostream>
#include <vector>

const int MOD = 19260817;
std::vector<std::vector<int>> C(1005, std::vector<int>(1005, 0));
std::vector<std::vector<int>> S(1005, std::vector<int>(1005, 0));

void init() {
	for (int j = 0; j <= 1000; j++) {
		C[j][0] = 1;
		for (int i = 1; i <= j; i++) {
			C[j][i] = (C[j-1][i-1] + C[j-1][i]) % MOD;
		}
	}
	for (int i = 1; i <= 1000; i++) {
		for (int j = 1; j <= 1000; j++) {
			S[i][j] = (1LL * S[i-1][j] + S[i][j-1] - S[i-1][j-1] + C[j][i]) % MOD;
            S[i][j] = (S[i][j] + MOD) % MOD; // 在真实数值(不取模)的情况下,二维前缀和必然满足 S[i-1][j] >= S[i-1][j-1]。 但是取模之后,这个大小关系就不一定成立了。
		}
	}
}

int main() {
	init();
	int q = 0;
	scanf("%d", &q);
	for (int i = 0; i < q; i++) {
		int n = 0;
		int m = 0;
		scanf("%d %d", &n, &m);
		printf("%d\n", S[n][m]);
	}
	return 0;
}
posted @ 2026-09-08 19:53  chen_xing  阅读(10)  评论(0)    收藏  举报