USACO 2020 January Contest, Platinum Problem 2. Non-Decreasing Subsequences
题目链接:http://usaco.org/index.php?page=viewproblem2&cpid=997
题解:矩阵在DP中的应用。
对于区间1-i,我们假设我们的合法子序列方案数为∑f[i][0][j], f[i][j][k] 就表示在这个区间里以j开头以k结尾的方案数。也就说我们有N个K*K的矩阵。但很明显这样是不满足区间减法的。我们可以发现,每加入一个元素x,我们有一个贡献矩阵把矩阵f[i-1]转移到f[i],假设这个贡献矩阵是A[i],那么f[i]=A[i]*A[i-1]......*A[1],假如我想知道区间的贡献矩阵之积,我们可以用“逆矩阵”。我们设invf[i]=A'[1]*A'[2].....*A'[i],那么区间的贡献矩阵是f[R]*invf[L-1],这样我们可以做到O((Q+N)K^3),这只能过一部分数据。研究一下这些贡献矩阵发现,它是一个三角矩阵,并且只有一行(或者一列,取决于你的贡献矩阵是左乘还是右乘)和单位矩阵不一样,也就说我们可以在预处理f[i]和invf[i]的时候把矩阵乘法加速到K^2,然后答案的区间矩阵我们只需要知道一列,然后我们可以通过预处理加速到K。总的时间复杂度是O(NK^2+QK)。
#include <bits/stdc++.h> using namespace std; const int modu = 1e9 + 7; typedef long long ll; const ll inverse2 = (1e9+8)/2; ll f[22][22], invf[22][22]; ll pre[50005][22], ipre[50003][22]; int N, K, Q, cur; int A[50003]; int main() { freopen("nondec.in", "r", stdin); freopen("nondec.out", "w", stdout); scanf("%d%d", &N, &K); memset(f, 0, sizeof(f)); memset(invf, 0, sizeof(invf)); for (int i = 0; i <= K; ++i) f[i][i] = invf[i][i] = 1; ipre[0][0] = 1; for (int i = 1; i <= N; ++i) { scanf("%d", &A[i]);
//手动模拟矩阵乘法,因为有系数2,所以每个贡献矩阵左乘拆成两种基本行变换,把A[i]行*2,和把前面的行加到这一行。 for (int j = 0; j < A[i]; ++j) { ll sumf = 0; for (int k = 0; k < A[i]; ++k) { sumf = (sumf + f[k][j]*inverse2) % modu; } f[A[i]][j] = (f[A[i]][j] + sumf) % modu; } for (int j = 0; j <= K; ++j) f[A[i]][j] = f[A[i]][j]*2 % modu; for (int j = 0; j <= K; ++j) for (int k = 0; k <= K; ++k) pre[i][j] = (pre[i][j] + f[k][j]) % modu;
//右乘可以看成基本列变换 for (int j = 0; j <= K; ++j) invf[j][A[i]] = invf[j][A[i]]*inverse2 % modu; for (int j = A[i]; j <= K; ++j) { for (int k = 0; k < A[i]; ++k) invf[j][k] = (invf[j][k] - invf[j][A[i]]) % modu; } for (int j = 0; j <= K; ++j) ipre[i][j] = invf[j][0]; } scanf("%d", &Q); while (Q--) { int L, R; scanf("%d%d", &L, &R); ll ans = 0; for (int i = 0; i <= K; ++i) ans = (ans + pre[R][i]*ipre[L-1][i]) % modu; printf("%lld\n", (ans + modu) % modu); } return 0; }