The solution of「CF2204G Grid Path」
\(\textup{CF2204G Grid Path}\)
\(\textup{Luogu}\) | \(\textup{Codeforces}\) | 矩阵优化 dp。
基础的矩阵快速幂可以看这个。
笑点

\(\textup{Description}\)
在 \(n \times m\) 的网格中,可以在网格内向左、右、下移动任意次,可随时停止。
问路径上格子的集合的种数。
\(\textup{Solution}\)
\(\textup{Step1}\):如何通过性质写出朴素的 dp?
注意到只能向下走,这意味着不能回头去上一行。
且向左向右的时候每一行中踩过的格子是连续的,也就是一个区间。
从而令 \(dp_{i, l, r}\) 表示到了第 \(i\) 行,恰好在该行走过 \([l,r]\) 这几个格子的方案数。
转移即枚举上一行的子区间,此时是 \(O(m^4 \cdot n )\) 的,考虑优化。
\(\textup{Step2}\):用什么优化?怎么优化?
通过
开头的标签暴力的转移方程我们发现,新的一行的状态只和前一行有关,也许可以用矩阵优化?直接对上面的暴力做矩阵快速幂,具体这个可能和后面没关系,而且比较繁琐,就不展开了。
只需要知道是 \(O(m ^ 6 \cdot \log n)\) 的就可以了,因为区间数的总数是 \(O(m^2)\) 的,而以这个矩阵做一个快速幂就这样了。
\(\textup{Step3}\):加大优化力度?!
考虑到我们在做什么,发现一直在求“上一行与 \([l,r]\) 有交集的区间之和”这么一件事,但是左右边界都要考虑较难处理。
但是如果我们反过来考虑补集呢?
这时候只需要考虑上一列的区间完全在左侧和右侧了,似乎可以简单很多。
自然地,我们定义前缀后缀和来辅助转移:
- \(pre_{i, x}\) 表示第 \(i\) 行右端点 \(r < x\) 的所有方案数之和,即前缀和;
- \(suf_{i, x}\) 同理啊,就是 \(l > x\) 的方案数之和,后缀和;
- 再记 \(tot_i\) 为第 \(i\) 行的方案总数。
那么 \(f_{i, l, r} = tot_{i - 1} - pre{i - 1, l} - suf_{i - 1, r}\),也就是,我们不用记录 \(f\) 了!
\(\textup{Step4}\):如何对它们转移?
\[pre_{i, x} = \frac{x \cdot (x - 1)}{2} pre_{i - 1, m + 1} - \sum_{l < x} (x-l) pre_{i - 1, l} - \sum_{r < x} r \cdot suf_{i - 1, r} \]\[suf_{i, x} = \frac{(m - x) \cdot (m - x + 1)}{2} pre_{i - 1, m + 1} - \sum_{x < l} (m - l + 1) pre_{i - 1, l} - \sum_{x < r} (r - x) suf_{i - 1, r} \]推导
考虑从定义入手: $$ \begin{aligned} pre_{i, x} &= \sum_{1 \le l \le r < x} f_{i, l, r} \\ &=\sum_{1 \le l \le r < x} (tot_{i-1} - pre_{i-1, l} - suf_{i-1, r}) \\ &= \sum_{1 \le l \le r < x} tot_{i-1} - \sum_{1 \le l \le r < x} pre_{i-1, l} - \sum_{1 \le l \le r < x} suf_{i-1, r} \end{aligned} $$ 对于第一项,我们只需要计算有多少个区间满足 $1 \le l \le r < x$,即 $\frac{x \cdot ( x - 1 )}{2}$ 个。而 \(tot_{i - 1}\) 是常数,则第一项化简为 \(\frac{x \cdot (x - 1)}{2} pre_{i - 1, m + 1}\)。
对于第二项,因为只与 \(l\) 有关,当我们固定 \(l\) 而枚举 \(r\) 的时候,会发现每个 \(r\) 可以取 \(l, l + 1, \cdots x - 1\),即 \(x - l\) 个。
所以化作 \(\sum_{l < x} (x - l) \cdot pre_{i - 1, l}\)。
第三项与前者相似,只与 \(r\) 有关,同理得 \(\sum_{r < x} r \cdot suf_{i-1, r}\)。
后者同理。
我们又观察到,这两个转移也只和上一行有关,那么又可以矩阵优化呐。
\(\textup{Step5}\):矩阵转移?
于是我们把 \(pre \ (1 \to m),suf \ (m + 1 \to 2m),tot \ (2m + 1)\) 三项拼成一行矩阵,当然 \(tot_i = pre_{i, m + 1}\)。
为了方便计算答案,我们再最后加上一个前缀和 \(sum = \sum_{j = 1} ^ i pre_{j, m + 1}\),此时矩阵长度为 \(2m + 2\)。
初始化的话,\(g_1 = [\underbrace{0, 1, 2, \dots, m-1}_{pre}, \quad \underbrace{0, 0, \dots, 0}_{suf}, \quad \underbrace{m}_{tot}, \quad \underbrace{m}_{sum}]\)
详细推一波转移矩阵,比较长就折起来了。
这样就可以做到 $O( ( 2m ) ^ 3 \cdot n)$ 了,常数稍微卡卡就能过,反正也是对的。矩阵转移
为了方便,我们令 $g_i = g_{i - 1} \times A$。小心我的系数变量和前面不统一。
在 \(1 \to m\) 列中,计算 \(pre_{i, y}\) 根据上面的公式(可以对着前面看),分别在矩阵 \(A\) 中填写这三项。
- 系数为 \(\frac{y \cdot (y - 1)}{2}\),即 \(A_{2m + 1, y}\);
- 系数为 \(-(y - l)\),令 \(x = l\),则在 \(x < y\) 时,\(A_{x, y} = -( y - x )\);
- 系数为 \(-r\),令 \(x = r\),在 \(x < y\) 的时候,\(A_{m + x, y} = -x\)。
\(suf\) 是同理的:
- 系数为 \(\frac{(m - y)(m - y + 1)}{2}\),即 \(A_{2m + 1, m + y}\);
- 系数为 \(-(m - l + 1)\),在 \(x > y\) 时,\(A_{x, m + y} = -(m - x + 1)\);
- 系数为 \(-(r - y)\),在 \(x > y\) 时,\(A_{m + x, m + y} = -(x - y)\)。
\(tot_i\) 也同理。
- 系数为 \(\frac{m \cdot (m + 1)}{2}\),即 \(A_{2m + 1, 2m + 1} = \frac{m \cdot (m + 1)}{2}\);
- 系数为 \(-(m + 1 - l)\),对所有 \(1 \le x \le m\),\(A_{x, 2m + 1} = -(m + 1 - x)\);
- 系数为 \(-r\),对所有 \(1 \le x \le m\),\(A_{m + x, 2m + 1} = -x\)。
回收 \(sum_i\) 的伏笔,由于其定义是前 \(i\) 行的方案数前缀和,也就是 \(sum_i = sum_{i - 1} + pre_{i, m + 1}\),在矩阵中的系数就呼之欲出了。
又因为 \(pre_{i, m + 1} = g_{i - 1, 1} \cdot A_{1, 2m + 1} + \cdots + g_{i - 1, 2m + 1} \cdot A_{2m + 1, 2m + 1}\),所以系数相同。
那么有对于 \(x \le 2m + 1\),\(A_{x, 2m + 2} = A_{x, 2m + 1}\),以及最后从上一行继承的 \(sum\),即 \(A_{2m + 2, 2m + 2} = 1\)。
于是我们转移完了。
\(\textup{Code}\)
回收开头笑点的伏笔,似乎会卡常,所以开 int128 然后最后统一取模会快很多,快速幂也可以像我代码这样写会快一点。
#include<bits/stdc++.h>
#define lll __int128
#define ll long long
using namespace std;
const int MAXN = 310;
int N, M, MOD, siz;
lll tmp[MAXN][MAXN];
// #define int long long
struct matrix{
int a[MAXN][MAXN];//转移矩阵A
void unit(){
for( int i = 1; i <= siz; i ++ )
for( int j = 1; j <= siz; j ++ )
a[i][j] = ( i == j );
}
matrix operator * ( const matrix & b ) const{
memset( tmp, 0, sizeof tmp );
matrix ans;
for( int i = 1; i <= siz; i ++ )
for( int k = 1; k <= siz; k ++ ){
if( !a[i][k] ) continue;
for( int j = 1; j <= siz; j ++ )
tmp[i][j] += 1ll * a[i][k] * b.a[k][j];
// ans.a[i][j] = 0;
// ans.a[i][j] = ( a[i][k] * b.a[k][j] % MOD + ans.a[i][j] % MOD ) % MOD;
}
for( int i = 1; i <= siz; i ++ )
for( int j = 1; j <= siz; j ++ )
ans.a[i][j] = tmp[i][j] % MOD;
return ans;
}
}a, g;
matrix qpow( matrix a, int x ){
matrix ans;
// base = *this;
ans.unit();
while( x ){
if( x & 1ll ) ans = ans * a;
a = a * a;
x >>= 1;
}
return ans;
}
signed main(){
cin >> N >> M >> MOD;
siz = M * 2 + 2;
for( int y = 1; y <= M; y ++ ){//pre
a.a[2 * M + 1][y] = ( 1ll * y * ( y - 1 ) / 2 + MOD) % MOD;
for( int l = 1; l < y; l ++ )
a.a[l][y] = ( - y + l + MOD ) % MOD;
for( int r = 1; r < y; r ++ )
a.a[M + r][y] = ( -r + MOD ) % MOD;
}
for( int y = 1; y <= M; y ++ ){//suf
a.a[2 * M + 1][y + M] = ( 1ll * ( M - y ) * ( M - y + 1 ) / 2 + MOD) % MOD;
for( int l = y + 1; l <= M; l ++ )
a.a[l][y + M] = ( -( M - l + 1 ) + MOD ) % MOD;
for( int r = y + 1; r <= M; r ++ )
a.a[M + r][y + M] = ( -( r - y ) + MOD ) % MOD;
}
int ytot = 2 * M + 1;//tot
a.a[ytot][ytot] = ( 1ll * M * ( M + 1 ) / 2 ) % MOD;
for( int l = 1; l <= M; l ++ )
a.a[l][ytot] = ( -( M + 1 - l ) + MOD ) % MOD;
for( int r = 1; r <= M; r ++ )
a.a[M + r][ytot] = ( -r + MOD ) % MOD;
int ysum = 2 * M + 2;//sum
for( int x = 1; x <= 2 * M + 1; x ++ )
a.a[x][ysum] = a.a[x][ytot];
a.a[2 * M + 2][ysum] = 1;
g.a[1][2 * M + 1] = g.a[1][2 * M + 2] = M % MOD;//tot和sum初始都是M
for( int i = 1; i <= M; i ++ )
g.a[1][i] = ( i - 1 ) % MOD;//pre
a = qpow( a, N - 1 );
g = g * a;
cout << g.a[1][siz];
return 0;
}
\(\textup{Last}\)
敲柿子快敲死了,怕错所以用了 ai 复核了点下标可能写的有点 ai 味(比如那个大括号),见谅。
如果您有所疑惑或我有所错漏,请您在评论区指出或找我,我会一定解答并且修改本题解。
如果您觉得本文写的还不错,那可以留个赞吗?
谢谢你看到这里~

浙公网安备 33010602011771号