The solution of「CF2204G Grid Path」

· · 题解

\textup{CF2204G Grid Path}

\textup{Luogu} | \textup{Codeforces} | 矩阵优化 dp。

基础的矩阵快速幂可以看这个。

:::info[笑点] :::

\textup{\textup{Description}}

n \times m 的网格中,可以在网格内向左、右、下移动任意次,可随时停止。

问路径上格子的集合的种数。

\textup{\textup{Solution}}

> 注意到只能向下走,这意味着不能回头去上一行。 > > 且向左向右的时候每一行中踩过的格子是连续的,也就是一个区间。 > > 从而令 $dp_{i, l, r}$ 表示到了第 $i$ 行,恰好在该行走过 $[l,r]$ 这几个格子的方案数。 > > 转移即枚举上一行的子区间,此时是 $O(m^4 \cdot n )$ 的,考虑优化。 $\textup{\textup{Step2}}$:用什么优化?怎么优化? > 通过~~开头的标签~~暴力的转移方程我们发现,新的一行的状态只和前一行有关,也许可以用矩阵优化? > > 直接对上面的暴力做矩阵快速幂,具体这个可能和后面没关系,而且比较繁琐,就不展开了。 > > 只需要知道是 $O(m ^ 6 \cdot \log n)$ 的就可以了,因为区间数的总数是 $O(m^2)$ 的,而以这个矩阵做一个快速幂就这样了。 $\textup{\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{\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}$$ > > :::success[推导] > 考虑从定义入手: > $$ > \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{\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}]

详细推一波转移矩阵,比较长就折起来了。

:::success[矩阵转移] 为了方便,我们令 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
+ 系数为 $\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$。 于是我们转移完了。 ::: 这样就可以做到 $O( ( 2m ) ^ 3 \cdot n)$ 了,常数稍微卡卡就能过,反正也是对的。

\textup{\textup{Code}}

回收开头笑点的伏笔,似乎会卡常,所以开 int128 然后最后统一取模会快很多,快速幂也可以像我代码这样写会快一点。

\textup{Rec.}

#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 味(比如那个大括号),见谅。

审核管理员辛苦了,如果您有所疑惑或我有所错漏,请您在评论区指出或找我,我会一定解答并且修改本题解。

如果您觉得本文写的还不错,那可以留个赞吗?

谢谢你看到这里~