No.93 ペガサス

No.93 ペガサス - yukicoder

  • 1,2,...,n の順列のうち特定の条件を満たすものを求める問題。
  • 1,2,...,n の順番で挿入しながら順列を作ると考えることで、状態に遷移(方向性)をもたせることができるのかー。
  • 最初 (n,n-2) の隣接フラグをなんで持たせるのかがわからなかった。次のループで必要になるからか。現在のループで次の (n,n-2) フラグが更新できることまで見抜けないと DP の設計ができないなぁ..。
class Pegasus {
public:
    void solve(void) {
            //
            // 縦横にはおけないのと N*N 盤面であることから
            //
            //  +-+-+-+-+
            //  | |o| | |
            //  +-+-+-+-+
            //  |o| | | |
            //  +-+-+-+-+
            //  | | | |o|
            //  +-+-+-+-+
            //  | | |o| |
            //  +-------+
            //  [2,1,4,3]
            //
            // のように 1...n の順列のうち隣り合う数字との差の絶対値が
            // 2 にならないようなものの総数を求めれば良い。
            // 以下隣接する数字の差の絶対値が 2 になる条件を cond と表す。
            //
            // 挿入 DP でやる。
            //  1,2,3,..,N の順に挿入して順列をつくると考える。
            //
            //  1,2,..,n の順列にたいして n+1 を挿入する時
            //   * n-1 の隣に挿入してしまうと cond をみたすものが 1 つできる。
            //     (1,2,... の順で挿入するので片側だけ考えれば良い。)
            //    * ただし n-3,n-1 が隣接するとき間に cond を満たすものの総数は変わらない ((-1) + (+1) = 0)
            //   * それ以外の cond をみたす隣接2数の間に挿入すれば cond を満たすものの総数を減らせる。
            //
            //  n+1 の挿入時に n-3,n-1 が隣接しているかの情報が必要になる。
            //  つまり次の更新時のために n,n-2 が隣り合うかの情報更新が必要。
            //
            // 以上を踏まえると dp は
            // dp[n][k][a][b] := n 番目まで挿入したときに、 cond をみたす場所が k 個あるときの挿入結果の総数
            //                   ただし a==1 のとき (n-3,n-1) が隣接
            //                   ただし b==1 のとき (n-2,n) が隣接
            //
            // となる。
            int N;
            cin>>N;

            vector<vector<vector<vector<ModInt>>>> dp;
            mdv::resize(dp, N+1, N+1, 2, 2, 0);

            dp[1][0][0][0] = 1;
            // O(N^2)
            FOR(n,1,N)
            REP(k,N)
            REP(a,2)
            REP(b,2)
            {
                if (n == 1)
                {
                    // 1 枚しかないときはどこに挿入しても cond を満たさない
                    dp[n+1][k][0][0] += (n+1)*dp[n][k][a][b];
                    continue;
                }

                // (n-2,n)   -> (n-3,n-1)
                // (n-1,n+1) -> (n-2,n)
                if (a==1) // (n-3,n-1) が隣接
                {
                    // n-1 のとなりに挿入するケース
                    // (n-3,n-1) の間に挿入
                    dp[n+1][k][b][1] += dp[n][k][a][b];
                    // (n-3,n-1)じゃないほうの n-1 のとなり
                    dp[n+1][k+1][b][1] += dp[n][k][a][b];

                    // cond 総数を減らすような挿入。 k-1 なのは上の (n-3,n-1) のケースを省くから
                    if (k >= 1)
                    {
                        if (b==1) // (n-2,n) が隣接するケース
                        {   // (n-2,n) の間に挿入
                            dp[n+1][k-1][0][0] += dp[n][k][a][b];
                            // それ以外
                            dp[n+1][k-1][1][0] += ((k-1)-1)*dp[n][k][a][b];
                        }
                        else
                            dp[n+1][k-1][b][0] += (k-1)*dp[n][k][a][b];
                    }

                    // のこり (n+1) なのは 1,...,n の両側を含めるから
                    dp[n+1][k][b][0] += ((n+1)-k-1)*dp[n][k][a][b];
                }
                else
                {
                    // n-1 のとなりに挿入(左右二通り)
                    dp[n+1][k+1][b][1] += 2*dp[n][k][a][b];

                    // k を減らすような挿入
                    if (k >= 1)
                    {
                        if (b==1)
                        {   // (n-2,n) の間に挿入
                            dp[n+1][k-1][0][0] += dp[n][k][a][b];
                            // それ以外
                            dp[n+1][k-1][1][0] += (k-1)*dp[n][k][a][b];
                        }
                        else
                            dp[n+1][k-1][b][0] += k*dp[n][k][a][b];
                    }
                    // のこり
                    dp[n+1][k][b][0] += ((n+1)-k-2)*dp[n][k][a][b];
                }
            }
            cout<<dp[N][0][0][0]<<endl;
    }
};