D - We Like AGC

はじめに

競技プログラミングAtCoder Beginner Contest 122
D - We Like AGC
言語はC#
何かあればTwitter→@pirorirori_n712まで

問題

問題文

整数 N が与えられます。
次の条件を満たす長さ N の文字列の数を 10~9+7 で割った余りを求めてください。

・A, C, G, T 以外の文字を含まない。
・AGC を部分文字列として含まない。
・隣接する 2 文字の入れ替えを 1 回行うことで上記の条件に違反させることはできない。

注記

文字列 T の部分文字列とは、T の先頭と末尾から 0 文字以上を取り去って得られる文字列です。
例えば、ATCODER の部分文字列には TCO, AT, CODER, ATCODER,  (空文字列) が含まれ、AC は含まれません。

制約

・3≤N≤100

入力

入力は以下の形式で標準入力から与えられる。

N

出力

条件を満たす文字列の数を 10^9+7 で割った余りを出力せよ

解き方

以下を参考にしています。
ABC122 D - We Like AGC - ARMERIA
AtCoder ABC 122 D - We Like AGC (400 点) - けんちょんの競プロ精進記録
提出 #4704271 - AtCoder Beginner Contest 122

解き方の流れ

問題文に書かれている条件のうち
以下の2つ目と3つ目

・AGC を部分文字列として含まない。
・隣接する 2 文字の入れ替えを 1 回行うことで上記の条件に違反させることはできない。

がなければ問題がシンプルになるので
どのような場合に
この条件に引っかかるのかを考えます。

すると以下の文字の並びのとき、
条件にかかることが分かります。

AGC GAC ACG A?GC AG?C

「?」は「何でもいいので何か一文字」を指しています。

上の条件にかかってしまう部分文字列から、
3文字前、2文字前、1文字前、現在の文字を調べ
上の部分文字列と同じになっていたら
そのパターンは数としてカウントしないという方法がとれそうです。

文字列のパターンをカウントしていくには
DPを使用します。
構成要素は以下のようにします。

dp[文字の数][現在の文字から2つ前の文字][現在の文字から1つ前の文字][現在の文字]=「文字の数」でできるパターンの数

そして、
以下のようにループを回しながら
作ることのできる文字列をカウントしていきます。

問題文から文字列を構成するのは
A,G,C,Tの4文字ですが
数字に置き換えるとループの数をそのまま使えるので
A=1,G=2,C=3,T=4と各文字を数字に置き換えています。

また、DPの始点としての文字列が一つ欲しいので
ダミーの文字を一つ追加し、
数字を0と置きました。
始点を全てdummy文字にしています。
(dp[0, 0, 0, 0] = 1;)

参考:AtCoder ABC 122 D - We Like AGC (400 点) - けんちょんの競プロ精進記録

//dummy=0,A=1,G=2,C=3,T=4
var dp = new long[N + 1, 5, 5, 5];
dp[0, 0, 0, 0] = 1;
//文字数
for (int n = 0; n < N; ++n)
{
    //3文字前の文字
    for (int i = 0; i < 5; ++i)
    {
        //2文字前の文字
        for (int j = 0; j < 5; ++j)
        {
            //1文字前の文字
            for (int k = 0; k < 5; ++k)
            {
                //現在の文字
                for (int l = 1; l < 5; ++l)
                {
                    //AGC GAC ACG A?GC AG?C (i,j,k,l)のパターンをはじく
                    if (j == 1 && k == 2 && l == 3//AGC
                    || j == 2 && k == 1 && l == 3//GAC
                    || j == 1 && k == 3 && l == 2//ACG
                    || i == 1 && k == 2 && l == 3//A?GC
                    || i == 1 && j == 2 && l == 3)//AG?C
                    {
                        continue;
                    }
                    dp[n + 1, j, k, l] = (dp[n + 1, j, k, l] + dp[n, i, j, k]) % 1000000007;
                }
            }
        }
    }
}

現在の文字の「l」だけ範囲が
0~5ではなく1~5になっていますが
これは、
実際に数える際には「dummy」の数字は必要ないから
だと思います。

あと、
これはなぜか分からなかったのですが
「l」の範囲を0~5にすると
値がずれます。

dp[0, 0, 0, 0] = 1 は
1のままで変わらないで欲しいのに
i=0,j=0,k=0,l=0のとき
dp[0, 0, 0, 0] =dp[0, 0, 0, 0] +dp[0, 0, 0, 0] で
dp[0, 0, 0, 0] が2になってしまうからかもしれないです。

あとは、
dummyの0を除いた
1~4でループを回し
DPの中に保存されている
N文字のときのパターンを
足せば答えが出ます。

//パターンの個数を出す
var ans = 0L;
//dummyの0を除いた1~4でループ
 for (int i = 1; i < 5; ++i)
 {
    for (int j = 1; j < 5; ++j)
    {
        for (int k = 1; k < 5; ++k)
        {
            //N文字のときのパターンの数が知りたいので文字数はN固定
            ans = (ans + dp[N, i, j, k]) % 1000000007;
        }
    }
}

全体のコード

using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var N = Int32.Parse(Console.ReadLine());
        //dummy=0,A=1,G=2,C=3,T=4
        var dp = new long[N + 1, 5, 5, 5];
        dp[0, 0, 0, 0] = 1;
        //文字数
        for (int n = 0; n < N; ++n)
        {
            //3文字前の文字
            for (int i = 0; i < 5; ++i)
            {
                //2文字前の文字
                for (int j = 0; j < 5; ++j)
                {
                    //1文字前の文字
                    for (int k = 0; k < 5; ++k)
                    {
                        //現在の文字
                        for (int l = 1; l < 5; ++l)
                        {
                            //AGC GAC ACG A?GC AG?C (i,j,k,l)のパターンをはじく
                            if (j == 1 && k == 2 && l == 3//AGC
                            || j == 2 && k == 1 && l == 3//GAC
                            || j == 1 && k == 3 && l == 2//ACG
                            || i == 1 && k == 2 && l == 3//A?GC
                            || i == 1 && j == 2 && l == 3)//AG?C
                            {
                                continue;
                            }
                            dp[n + 1, j, k, l] = (dp[n + 1, j, k, l] + dp[n, i, j, k]) % 1000000007;
                        }
                    }
                }
            }
        }
        //パターンの個数を出す
        var ans = 0L;
        //dummyの0を除いた1~4でループ
        for (int i = 1; i < 5; ++i)
        {
            for (int j = 1; j < 5; ++j)
            {
                for (int k = 1; k < 5; ++k)
                {
                    //N文字のときのパターンの数が知りたいので文字数はN固定
                    ans = (ans + dp[N, i, j, k]) % 1000000007;
                }
            }
        }
        Console.WriteLine(ans);
    }
}