読者です 読者をやめる 読者になる 読者になる

torus711 のアレ

主に競技プログラミングの問題について書きます

AtCoder Beginner Contest #003, D : AtCoder社の冬

AtCoder 動的計画法

概要

 R \times C の盤面上に 'D', 'L', '-' が配置されている。このとき、全ての '-' 以外の文字を内部に含む最小の矩形領域の大きさは X \times Y となった。盤面上には 'D' が D 個、'L' が L 個ある。盤面の配置として valid なものの数を MOD で求めよ。

解法

 ※全てを DP でゴリ押ししていてあまり賢くないです。(強引な)DP 解に興味のある方向けです。

 X \times Y の矩形領域内部での配置の数が分かれば、可能な平行移動の数である ( R - X + 1 ) * ( C - Y + 1 ) を掛けることで全体の答えが分かります。つまり、矩形領域内部に関する小問題が解ければ全体が解けます。

小問題・部分点まで

 次に矩形領域内部についてですが、都合がいいのでまず部分点問題について述べます。部分点問題では、領域内のグリッドの数と D + L が一致しています。つまり、内部に '-' は存在しません。全ての行を横一列に配置すると考えても並び替えても不都合は生じないので、小問題は「 X \times Y 個の 'D', 'L' を一列に並べるパターンの数を求める」と言い換えられます。これならば、次のような DP によって解くことができます。

  • dp[ i 個並べた ][ 'D' を j 個並べた ] := パターンの数

'L' の数は i - j と計算できるので明示的に保持する必要はありません。状態の更新は 'D' を置くか 'L' を置くかなので、

  • dp[ i + 1 ][ j + 1 ] を dp[ i ][ j ] で更新
  • dp[ i + 1 ][ j ] を dp[ i ][ j ] で更新

の二つです。計算が終わった後、dp[ D + L ][ D ] に答えが求まっています。

小問題・満点まで

 以上の部分点問題の解法は、'-' が存在しない場合のものでした。ですが、矩形領域内部の「 'D' または 'L' が配置されるグリッド」の配置が何通りあるかを求められれば、更に前述の解法で得られる結果を掛けることで満点問題での小問題を解くことができます。
 この問題の場合は矩形領域の上下左右の端となる行・列にそれぞれ一つ以上配置する必要があるので、その制約の達成条件も保持する必要があります。上の行から順に、左から決定していくと考えると次のような DP を考えることができます。

  • dp[ i 個決まった ][ 'D' または 'L' が置かれるグリッドの数 ][ それぞれの端の達成状態 ] := パターンの数

三つ目の次元の値は、一番上の行に置いたか・一番左の列に置いたか・一番下の行に置いたか・一番右の列に置いたか を整数の 1 〜 4 bit 目( 1-indexed)に割り当ててエンコードした値です。i 個目のグリッドに 'D' または 'L' を置くことにするとき、それぞれの bit は次のように更新されます

  • 一番上の行に置いたか : i < Y で OR をとる
  • 一番左の列に置いたか : i % Y == 0 で OR をとる
  • 一番下の行に置いたか : ( X * Y - Y <= i ) で OR をとる
  • 一番右の列に置いたか : ( i % Y == Y - 1 ) で OR をとる

dp[ i ][ j ][ k ] からの更新は 'D' または 'L' を置くことにする場合の新しい k を nk とすると、

  • dp[ i + 1 ][ j ][ k ] を更新
  • dp[ i + 1 ][ j + 1 ][ nk ] を更新

の二つです。計算が終了した後、dp[ X * Y ][ D + L ][ ( 1 << 4 ) - 1 ] に答えが求まっています。
 最後に、この値に 'L', 'D' を一列に並べるパターンの数( = 'D' または 'L' が置かれるグリッドに対して 'D', 'L' を割り当てるパターンの数)を掛けて、更に矩形領域の平行移動の数を掛ければ全体の答えが求まります。

コード

typedef long long LL;
typedef vector<int> VI;
typedef vector<VI> VVI;

#define REP( i, m, n ) for ( int i = (int)( m ); i < (int)( n ); ++i )

const int MOD = 1000000007;

int dp[ 30 * 30 + 1 ][ 30 * 30 + 2 ][ 1 << 4 ];

int solve2( const int N, const int A, const int B )
{
	VVI dp( N + 1, VI( A + 2, 0 ) );
	dp[0][0] = 1;

	REP( i, 0, N )
	{
		REP( a, 0, A + 1 )
		{
			const int b = i - a;

			if ( a < A )
			{
				( dp[ i + 1 ][ a + 1 ] += dp[i][a] ) %= MOD;
			}
			if ( b < B )
			{
				( dp[ i + 1 ][a] += dp[i][a] ) %= MOD;
			}
		}
	}

	return dp[N][A];
}

int main()
{
	cin.tie( 0 );
	ios::sync_with_stdio( false );

	int R, C, X, Y, D, L;
	cin >> R >> C >> X >> Y >> D >> L;

	dp[0][0][0] = 1;
	//dp[ i 箇所決定 ][ 何かを置いた数 ][ 上下左右のフラグ ] := 場合の数

	REP( i, 0, X * Y )
	{
		REP( j, 0, X * Y + 1 )
		{
			REP( k, 0, 1 << 4 )
			{
				int nk = k;
				nk |= i < Y;
				nk |= ( i % Y == 0 ) << 1;
				nk |= ( X * Y - Y <= i ) << 2;
				nk |= ( i % Y == Y - 1 ) << 3;

				{ // 何も置かない
					( dp[ i + 1 ][j][k] += dp[i][j][k] ) %= MOD;
				}
				{ // 何かを置く
					( dp[ i + 1 ][ j + 1 ][ nk ] += dp[i][j][k] ) %= MOD;
				}
			}
		}
	}


	LL res = (LL)dp[ X * Y ][ D + L ][ ( 1 << 4 ) - 1 ] * solve2( D + L, D, L );
	( res *= R - X + 1 ) %= MOD;
	( res *= C - Y + 1 ) %= MOD;

	cout << res << endl;

	return 0;
}