TopCoder SRM 548 Div1 Medium KingdomAndDice

問題

2個のダイスがあり、ダイスの面には数字が書かれている。
数字は以下の条件を満たす。

  • 1以上X以下
  • 書かれている数は全て互いに異なる
  • ただし、一つ目のダイスには0が何回か書かれていることがある。

いま、このダイスを使って次のようなゲームをする
先手が1番目のダイスを振り、後手が2番目のダイスを振る。
出た目の大きいほうが勝ち。


1番目のダイスの0を、まだ使われていない1からXの数字のどれかに書き換えることで、
このゲームの勝率Pを、できるだけ1/2に近づけたい。
このときの勝率Pの値を求めよ。
1/2に対して同じ近さの勝率Pが2通りあるときは、小さいほうの値を採用する。

制約条件

先手のダイスに書かれている数字は0以上X以下。
後手のダイスに書かれている数字は1以上X以下。
先手のダイスと後手のダイスの面の数は同じ。
0以外の数字は高々1度しか現れない。

方針

動的計画法による。
まずは二番目のダイスに書かれている数字を小さい順にソートする。


小さい順に並べたときのダイスの数をa[i]とすると、
一番目のダイスの0を、各a[i]について、a[i]〜a[i + 1]の中から、一つ選んで割り当てることが問題の操作に相当する。


(ただし、a[-1] = 0, a[n] = X + 1と考える)
dp[i][j][k]を、区間i番目までで、0をj個残して、今までの勝利数がkであるような状態に至れるかどうかという配列とする。
区間ごとに0をどれだけ割り当てられるかは、min(その区間の幅, j)であるため、
この範囲でループを回せばテーブルが更新できる。


計算量はO(n^5)っぽく見えるが、
テーブルの大部分にはアクセスされないので大体O(n^4).

ソースコード

bool dp[60][60][3000];

class KingdomAndDice {
	public:
	double newFairness(vector <int> firstDie, vector <int> secondDie, int X) 
	{
		int n = firstDie.size();
		memset(dp, 0, sizeof(dp));
		
		sort(all(secondDie));
		secondDie.pb(X + 1);
		int num[60] = {}, prev = 0;
		rep(i, n + 1){
			int cnt = secondDie[i] - prev - 1;
			rep(j, n) if(prev < firstDie[j] && firstDie[j] < secondDie[i]) cnt--;
			num[i] = cnt;
			prev = secondDie[i];
		}
		
		int cnt = 0, zero = 0;
		rep(i, n) if(firstDie[i] == 0) zero++;
		rep(i, n) if(firstDie[i]) rep(j, n) if(firstDie[i] > secondDie[j]) cnt++;
		dp[0][zero][cnt] = 1;
		
		int ans = inf;
		rep(i, n + 2) rep(j, zero + 1) rep(k, 3000) if(dp[i][j][k]){
			
			int tmp = k;
			if(abs(2 * tmp - n * n) < abs(2 * ans - n * n) ||
				abs(2 * tmp - n * n) == abs(2 * ans - n * n) && tmp < ans) ans = tmp;
			
			if(i < n + 1) rep(l, min(num[i], j) + 1){
				dp[i + 1][j - l][k + i * l] = 1;
			}
		}
		return ans * 1.0 / n / n;
	}
};