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}]\)

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

矩阵转移 为了方便,我们令 $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\)

于是我们转移完了。

这样就可以做到 $O( ( 2m ) ^ 3 \cdot n)$ 了,常数稍微卡卡就能过,反正也是对的。

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

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

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

谢谢你看到这里~

posted @ 2026-07-17 10:32  Fαll  阅读(13)  评论(0)    收藏  举报