1. 程式人生 > >JZOJ2368 【SDOI2011】黑白棋

JZOJ2368 【SDOI2011】黑白棋

題目

在這裡插入圖片描述

題目大意

在一個1*n的棋盤上,有黑棋和白棋交錯分佈,每次,一個人可以移動自己的 d d 顆旗子。
問先手必勝的方案數。


思考歷程

在一開始,我就有點要放棄的念頭。
因為這題是一道博弈問題。
我是非常不擅長博弈類問題的。
但是其他的題有想不出來,於是只能硬是想了好久。
最終那了個部分分。


正解

這題正解當然跟博弈有一些關係。
首先,對於這題有一個隱藏的限制。
白棋不能向左移,黑棋不能向右移。


這是為什麼?
我們可以感性理解一下(證明什麼的我當然不會):
如果白棋左移,那麼在它右邊的黑棋可以模仿它的操作,也跟著左移。
同理,如果黑棋右移,那麼在它左邊的白棋也可以模仿他的動作,也跟著右移。
那麼我們就可以發現,這樣走是沒有意義的。
然後我們就可以轉換問題的模型:
可以將對應(相鄰)的兩個白棋和黑棋中間的距離當作石子。
那麼就有 K 2 \frac{K}{2}
堆石子,然後每次可以在至多 d d 堆石子,至少 1 1 堆石子中各自拿走一顆石子。
這個問題叫Nimk問題。
這又是個什麼東西啊?
對於Nimk問題,我們有一個神奇的結論:將所有堆石子的個數化成二進位制,每一位統計一下 1
1
的個數,如果個數都是 d + 1 d+1 的倍數,那麼這就是必敗態。
如何證明?
我們可以感性理解一下:怎麼又是感性理解……
先手只能拿走 1 1 d d 堆中的石子,那麼,後手可以故意的模仿,然後將兩人取得的石子的總數固定在 d + 1 d+1 。那麼如果一直這麼下去,後手必定會贏。
為什麼要轉成二進位制呢?
我覺得,其實只要轉成了二進位制,那麼後手就能保證每次能夠取得這麼多的石子,使得兩個人取石子的總數為 d + 1 d+1 。具體怎樣解釋,我覺得我還要好好理解一下,暫且感性感性吧(感性理解真是一個好東西)。

模型轉換成了Nimk問題,讓我們求先手必勝的方案數。
先手必勝的方案數等於總方案數減去先手必敗的方案數。
如何求先手必敗的方案數呢?
當然是DP。
f i , j f_{i,j} 表示在二進位制的前 i i 位中,一共用了 j j 顆石子的先手必敗的方案數。
仔細想想這個狀態沒有任何問題,因為我們只需要保證在每一個二進位制位上都滿足它必敗就好了。這樣設狀態之後轉移就方便多了。
我們再列舉一個 k k (小寫的 k k ,不要混淆),表示一共有 k ( d + 1 ) k*(d+1) 堆石子下一個二進位制位上為 1 1
轉移?
f i + 1 , j + k ( d + 1 ) 2 i f i , j C K 2 k ( d + 1 ) f_{i+1,j+k*(d+1)*2^i}\leftarrow f_{i,j}*C_{\frac{K}{2}}^{k*(d+1)}
注意:為了程式實現方便,在這個方程中,前 i i 位的最高位實際上是 i 1 i-1 位。這樣的DP中,初始化直接是 f 0 , 0 = 1 f_{0,0}=1
在一波DP之後,我們就可以統計答案了。
其實我們可以將其視為,在 n j K n-j-K 個空中插入 K / 2 K/2 個東西。
這個東西可以用組合數來搞一搞,那麼就是 C n j K 2 K 2 C_{n-j-\frac{K}{2}}^{\frac{K}{2}} 種方案,用它來乘上 f M A X B I T , j f_{MAXBIT,j} 就好了。
然後這題就愉快地解決了。
複雜度表示懶得分析。


程式碼

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define mo 1000000007
int n,K,d;
int C[10001][101];
int f[17][10001];
int main(){
	scanf("%d%d%d",&n,&K,&d);
	C[0][0]=1;
	for (int i=1;i<=n;++i){
		C[i][0]=1;
		for (int j=1;j<=i && j<=K;++j)
			C[i][j]=(C[i-1][j]+C[i-1][j-1])%mo;
	}
	f[0][0]=1;
	for (int i=0;i<15;++i)
		for (int j=0;j<=n-K;++j)
			if (f[i][j])
				for (int k=0;k*(d+1)<=K>>1 && j+k*(d+1)*(1<<i)<=n-K;++k)
					(f[i+1][j+k*(d+1)*(1<<i)]+=(long long)f[i][j]*C[K>>1][k*(d+1)]%mo)%=mo;
	int ans=0;
	for (int j=0;j<=n-K;++j)
		ans=(ans+(long long)f[15][j]*C[n-j-(K>>1)][K>>1]%mo)%mo;
	printf("%d\n",(C[n][K]-ans+mo)%mo);
	return 0;
}

我認為這個程式實現不用註釋。


總結

博弈類問題,可真是一個神奇東西啊!
然而,好多的我都不會嚴謹證明。
感性理解就好……
我覺得以後要多做一些博弈類問題。