P5239 回忆京都
- 题目地址: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;
}

P5239 回忆京都
浙公网安备 33010602011771号