E - 通勤経路
古い問題ということもあり、解説記事が少なかったので、
記事にします。
問題雑概要
(1,1)から(w,h)までの経路を数えます。w,hはそれぞれ100以下です。
今回の問題では、交差点で「曲がる」という選択が2連続で出来ません。
「曲がった」場合には、次の交差点では一度「直進」する必要があります。
進行方向は常に上か右かの2択です。
方針
DPをします。
遷移を考えます。
通常であれば、以下のようなコードで済みます。
dp[i+1][j]+=dp[i][j]; dp[i][j+1]+=dp[i][j];
しかし今回の場合、進み方に制限がかかっているため無闇に上や右に進めません。
状態を持たせる
過去2回分の進行方向を状態として持たせます。
連続で「直進」している場合には、「直進」「曲がる」の両方を選べますし、
過去2回の進行方向が異なる場合には「曲がった」ということになるので、「直進」にのみ遷移できます。
//2回前の進行方向をlast_last_direction、1回前の進行方向をlast_directionとする //縦方向の移動→0、横方向の移動→1とする //配列のサイズは多めに宣言しておく int w_max=110, h_max=110, last_last_direction=3, last_direction=3; int dp[w_max][h_max][last_last_direction][last_direction];
(これは後で気づきましたが、状態はわざわざ2次元用意する必要はなく、
0:横横
1:横縦
2:縦横
3:縦縦
とかにすれば1次元でも十分機能するはずです。bit探索的な考え方ですね。)
なお、問題文上では、
JOIさんは,通勤時間を短くするため,東または北にのみ向かって移動して通勤している.
というように進行方向を示していますが、この記事では便宜上、東向きを「横方向」、北向きを「縦方向」と呼びます。上下も左右も区別しません。
遷移
現在の位置(i、j)は4つの状態を持ちます。
そのそれぞれの状態について、どこから遷移されてくるかをそれぞれ考えます。
状態は以下のように表せました。
dp[i][j][0][0] dp[i][j][1][0] dp[i][j][0][1] dp[i][j][1][1]
まず縦方向の移動から考えます。
縦は0を表すので、dp[ i ][ j ][ k ][ 0 ]が遷移先となります。(last_directionが縦を表している)
dp[i][j][0][0]+=dp[i-1][j][0][0]+dp[i-1][j][1][0]; dp[i][j][1][0]+=dp[i-1][j][1][1];
- dp[ i ][ j ][ 0 ][ 0 ]は縦縦となるため、遷移元が1つ前の交差点で曲がった場合でも受け入れられます。
しかし、2回前の操作も縦方向の移動である必要があるため、例えばdp[ i-1 ][ j ][ 0 ][ 1 ]は受け入れられません。 - dp[ i ][ j ][ 1 ][ 0 ]は横縦です。つまり「曲がる」を選択したので、その前の操作としては「直進」を連続で選んでいる場合のみ認められます。この時の直進は横移動です。
横方向へ移動する遷移も同様に書けます。
dp[i][j][1][1]+=dp[i][j-1][1][1]+dp[i][j-1][0][1]; dp[i][j][0][1]+=dp[i][j-1][0][0];
初期化を考える
考えます。
初期位置は通常1を置きます。(初期位置への経路は1通りなので)
ですが今回の場合は、1つの位置が4つの状態を持っています。
ややこいので初期位置dp[ 0 ][ 0 ][ last_last_direction ][ last_direction ]は飛ばします。
h\w | 0 | 1 | 2 |
---|---|---|---|
0 | ? | ? | |
1 | ? | ||
2 | ? | ||
3 | ? |
?の部分について考えます。
結論から言うと、?=1です。直進するしかないからです。
問題はこの1をどの状態に置くべきかということです。
端を進むだけなので進行方向は常に同じです。
[ 0 ][ 0 ] あるいは[ 1 ][ 1 ]に入れるのがよいでしょう。(ここらへんでwを高さ、hを横幅としてコーディングしていることに気づく)
for(int i=1;i<w;i++) dp[i][0][0][0]=1; for(int i=1;i<h;i++) dp[0][i][1][1]=1;
横横、縦縦の状態に入れておくことで「直進」「曲がる」どちらでも選べるようになります。
その他の状態は0でよいです。
ここまでを一旦実装します。
#include<bits/stdc++.h> using namespace std; int dp[110][110][3][3]; int ans[110][110]; main() { int w,h; cin>>w>>h; for(int i=1;i<w;i++)dp[i][0][0][0]=1; for(int i=1;i<h;i++)dp[0][i][1][1]=1; for(int i=0;i<w;i++) { for(int j=0;j<h;j++) { if(i==0||j==0)continue; //初期化ずみ if(i>0) { dp[i][j][0][0]+=dp[i-1][j][0][0]+dp[i-1][j][1][0]; dp[i][j][1][0]+=dp[i-1][j][1][1]; } if(j>0) { dp[i][j][0][1]+=dp[i][j-1][0][0]; dp[i][j][1][1]+=dp[i][j-1][1][1]+dp[i][j-1][0][1]; } } } //4つの状態を1つにまとめる for(int a=0;a<2;a++)for(int b=0;b<2;b++)for(int i=0;i<w;i++)for(int j=0;j<h;j++) { ans[i][j]+=dp[i][j][a][b]; } //debug for(int a=0;a<2;a++)for(int b=0;b<2;b++) { printf("[%d][%d]\n",a,b); for(int i=0;i<w;i++) { for(int j=0;j<h;j++) { cout<<dp[i][j][a][b]<<" "; } cout<<endl; } cout<<endl; } for(int i=0;i<w;i++) { for(int j=0;j<h;j++)cout<<ans[i][j]<<" "; cout<<endl; } }
入力例1の3 4
を投げてみます。
3 4 [0][0] 0 0 0 0 1 0 0 0 1 1 1 1 [0][1] 0 0 0 0 0 1 0 0 0 1 1 1 [1][0] 0 0 0 0 0 1 1 1 0 0 1 1 [1][1] 0 1 1 1 0 0 1 1 0 0 1 2 0 1 1 1 1 2 2 2 1 2 4 5
このようになります。5つ目のテーブルの右下が答えです。
なお、今回の問題では、100,000で割った余りを出力するよう求められます。
w=49, h=49くらいでlong longを超えるからです。
const int mod=1e5; dp[i][j][0][0]+=(dp[i-1][j][0][0]+dp[i-1][j][1][0])%mod;
cout<<ans[w-1][h-1]%mod<<endl;
というようにこまめにmodを取ってあげるとよいでしょう。int型で十分動くようになります。
提出コードはこちらです。
解説はここまでで以上になります。
ここまでお読みいただいた方、ありがとうございました。