1. 程式人生 > 實用技巧 >超級跳馬 —— 矩陣快速冪優化DP

超級跳馬 —— 矩陣快速冪優化DP

P3990 [SHOI2013]超級跳馬

題目描述

  • 現有一個 n 行 m 列的棋盤,一隻馬欲從棋盤的左上角跳到右下角。每一步它向右跳奇數列,且跳到本行或相鄰行。跳越期間,馬不能離開棋盤。例如,當 n = 3, m = 10 時,下圖是一種可行的跳法。

  • 試求跳法種數mod 30011。

輸入格式

  • 僅有一行,包含兩個正整數 n, m,表示棋盤的規模。

輸出格式

  • 僅有一行,包含一個整數,即跳法種數 mod 30011。

樣例輸入

3 5

樣例輸出

10

資料範圍與提示

  • 對於10%的資料,\(1 ≤ n ≤ 10,2 ≤ m ≤ 10\)
  • 對於50%的資料,\(1 ≤ n ≤ 10,2 ≤ m ≤ 10^5\)
  • 對於80%的資料,\(1 ≤ n ≤ 10,2 ≤ m ≤ 10^9\)
  • 對於100%的資料,\(1 ≤ n ≤ 502 ≤ m ≤ 10^9\)

Solve

  • 這題題意很清楚了,DP也很容易想到,\(f_{i,j}\) 表示到達第i列第j行的方案數,將起點方案數設為 1,每個點的方案數由可以過來的點轉移,一看資料範圍,n 就 50,m 最大 1e9 ,這幾乎就可以想到是矩陣快速冪優化了,我們可以一步一步分析。

  • 首先不難得出一個 \(O(n^3)\) 的寫法。(20pts)

    f[1][1] = 1;
    for (int i = 2; i <= m; ++i)
        for (int j = 1; j <= n; ++j)
            for (int k = i - 1; k >= 1; k -= 2)
                f[i][j] = (f[i][j] + f[k][j-1] + f[k][j] + f[k][j+1]) % M;
    
  • 注意到這只是求前面的和,限制條件也只有只能從奇數行轉移,這樣讓 \(f_{i,j}\) 記錄上奇數行的和就能優化到 \(O(n^2\)。(50pts)

    • 這裡的f陣列要同時記錄字首和,所以最終答案是類似區間求和的形式:\(f_{m,n} - f_{m-2,n}\)

    • 為了方便最後矩陣的寫法,又因為他是由倒數第二行最後兩列轉移過來,就直接輸出 \(f_{m-1,n}+f_{m-1,n-1}\)

      f[1][1] = 1;
      for (int i = 2; i < m; ++i)
          for (int j = 1; j <= n; ++j)
              f[i][j] = (f[i-1][j] + f[i-1][j-1] + f[i-1][j+1] + f[i-2][j]) % M;
      printf("%d\n", (f[m-1][n] + f[m-1][n-1]) % M);
      
  • 發現第 i 列只由第 i-1 列和 i-2 列轉移過來的,而且列數又那麼大,考慮矩陣快速冪優化:

    • 可以類比斐波那契數列的寫法,將 \(f_{i.k}\)\(f_{i-1,k}(k\in {1,...,n})\)的狀態壓成一個 \(1\times 2n\) 的矩陣,乘上一個轉移矩陣,得到 \(f_{i+1.k}\)\(f_{i,k}(k\in {1,...,n})\)的狀態,轉移矩陣根據上面第二個解法的轉移方程構造,還是舉 n=4 的例子

\[\begin{bmatrix} f_{i,1} & f_{i,2} &f_{i,3} &f_{i,4} &f_{i-1,1} &f_{i-1,2} &f_{i-1,3} &f_{i-1,4} \end{bmatrix} \ast \begin{bmatrix} 1 & 1 & 0& 0 & 1& 0 &0 & 0\\ 1& 1& 1 & 0& 0 &1 & 0 &0 \\ 0 & 1 & 1& 1& 0 & 0 & 1 & 0\\ 0 & 0 & 1 & 1 &0 & 0 & 0 &1 \\ 1 & 0 &0 & 0 & 0 &0 & 0 &0 \\ 0& 1 & 0& 0& 0& 0& 0& 0\\ 0& 0& 1& 0& 0 & 0&0&0 \\ 0& 0 & 0& 1 & 0 & 0 & 0 & 0 \end{bmatrix} = \begin{bmatrix} f_{i+1,1} & f_{i+1,2} &f_{i+1,3} &f_{i+1,4} &f_{i,1} &f_{i,2} &f_{i,3} &f_{i,4} \end{bmatrix}\]

這個式子有億點長啊

轉移矩陣:

\[\begin{bmatrix} 1 & 1 & 0& 0 & 1& 0 &0 & 0\\ 1& 1& 1 & 0& 0 &1 & 0 &0 \\ 0 & 1 & 1& 1& 0 & 0 & 1 & 0\\ 0 & 0 & 1 & 1 &0 & 0 & 0 &1 \\ 1 & 0 &0 & 0 & 0 &0 & 0 &0 \\ 0& 1 & 0& 0& 0& 0& 0& 0\\ 0& 0& 1& 0& 0 & 0&0&0 \\ 0& 0 & 0& 1 & 0 & 0 & 0 & 0 \end{bmatrix}\]

初始矩陣

\[\begin{bmatrix} 1 & 1 & 0 & 0 & 1 & 0 & 0 & 0 \end{bmatrix}\]

  • 需要注意的幾點:
    1. 因為矩陣無法處理 \(m\le 2\) 的情況,所以需要特判。只有 \(f_{1,1},f_{1,2},f_{2,2}\)值為1,其他情況都是0.
    2. 此解法只處理了 \(n>1\) 的情況,對於 \(n=1\) 的情況打表發現可以轉換成斐波那契數列,而且此時轉移矩陣與求斐波那契數列的轉移矩陣也很想,也特判一下就好了。
  • 接下來就是實現,具體看程式碼。

Code

#include <cstdio>
#include <cstring>
using namespace std;
const int N = 105, M = 30011;

int n, n2, m;
struct Matrix {
    int a[N][N];
    Matrix () {
        memset(a, 0, sizeof(a));
    }
    int *operator [] (const int &i) {
        return a[i];
    }//過載方括號運算子,寫起來方便
    Matrix operator * (const Matrix &b) {
        Matrix c;
        for (int i = 1; i <= n2; ++i)
            for (int j = 1; j <= n2; ++j)
                for (int k = 1; k <= n2; ++k)
                    (c.a[i][j] += a[i][k] * b.a[k][j] % M) %= M;
        return c;
    }//過載乘號
    void Print() {
        for (int i = 1; i <= n2; ++i, puts(""))
            for (int j = 1; j <= n2; ++j)
                printf("%d ", a[i][j]);
        puts("");
    }//除錯輸出用
}a, b, c;

Matrix Pow(Matrix a, int k) {
    Matrix ans = a; k--;
    for (; k; k >>= 1, a = a * a)
        if (k & 1) ans = ans * a;
    return ans;
}//快速冪

int main() {
    scanf("%d%d", &n, &m);
    if (m <= 2) {
        if (n <= 2 && m <= n) puts("1");
        else puts("0");
        return 0;
    }//特判情況1
    n2 = n << 1;
    for (int i = 1; i <= n; ++i) {//構造轉移矩陣
        a[i][i-1] = a[i][i] = a[i][i+n] = a[i+n][i] = 1;
        if (i != n) a[i][i+1] = 1;
    }
    b = Pow(a, m - 2);
    if (n == 1) return printf("%d\n", b[1][1]), 0;//特判情況2
    int s1 = (b[1][n2-1] + b[2][n2-1] + b[n+1][n2-1]) % M;
    int s2 = (b[1][n2] + b[2][n2] + b[n+1][n2]) % M;
    //根據初始矩陣和快速冪後的轉移矩陣求得目標矩陣的有用值
    printf("%d\n", (s1 + s2) % M);
    //s1是f[m-1][n-1],s2是f[m-1][n],加起來就是答案
    return 0;
}