1. 程式人生 > >a simple stone game--k倍動態規劃減法遊戲

a simple stone game--k倍動態規劃減法遊戲

要求b[j],表示a[0]……a[i]組成,那麼顯然是要用到a[i]的,不然不就成了b[i-1],既然用了a[i],但是又要使相鄰的倍數在K以上。則找到最大的j,使a[j]*k<a[i]那麼滿足條件,便是a[0]……a[j]能組成的最大的數,加上a[i],那麼後者表示當前項不能和之前項組合,那麼最大的數就只能是本身

附上我當時搜的大神的資料,看到這個我才搞懂的k倍動態規劃的原理

思路:

博弈論中的 K倍動態減法遊戲,難度較大,參看了好多資料才懵懂!

此題可以看作 Fibonacci 博弈的擴充套件,建議沒弄懂 Fibonacci博弈的先學那個,個人整理 http://blog.csdn.net/tbl_123/article/details/24033245 ;

而說擴充套件體現在數列不再是Fib數列,是根據 k 的值自行構造的,其它換湯不換藥,具體構造方法如下:

這兒方便說明白,首先根據k的值分情況討論:

1) 當 k = 1 時,必敗態為 n = 2 ^ i, 因為我們把數按二進位制分解後,拿掉二進位制的最後一個1,那麼對方必然不能拿走倒數第二位的1,因為他不能拿的比你多。你只要按照這個策略對方一直都不可能拿完。所以你就會贏。而當分解的二進位制中只有一個1時,因為第一次先手不能全部取完,所以後手一定有辦法取到最後一個1,所以必敗!

舉個例子,當 n = 6 = (110)時:

第一輪:先手第一次取最右邊的1,即2個,此時還剩4(100)個,後手能取1或2個;

第二輪:假如上輪後手取的兩個,先手再取兩個直接贏了

假如後手取了一個,那麼還剩三個,自己只能去1個,以後也只能取一個,所以必勝!

2) 當 k = 2 時,赤裸裸的Fibonacci博弈了,具體這兒不多說,自己再上述部落格已寫的很明白了。其實n經拆解後也可以表示成二進位制的形式,用 k = 1時的方法來理解,比如 n = 11 = 7 + 3 + 1,可表示成 10101;

3) 當 k 取任意非零正值時,重點來了:

猶如Fibonacci博弈,我們首先要求一個數列,將n分解成數列中一些項的和,然後就可以按Fibonacci博弈的解決方法來完成,也可以按二進位制的方法來理解,每次取掉最後一個1 還是符合上面的條件。

我們用a陣列表示要被求的數列,b陣列中的b[i]儲存 a[0...i] 組合能夠構造的最大數字。這兒有點難理解,所謂構造就是指n分解為Fib數相加的逆過程。舉例說明,當k = 2 時,a[N]={1, 2, 3, 5, 8, 13, 21, 33....} (Fibonacci陣列);那麼b[3] 即 1、2、 3 能夠構造的最大數字,答案是4,有點匪夷所思?或許你會問為什麼不是5、6或者其它的什麼,其實是這樣的 ,4 能分解成 1+3 是沒有爭議的,但5能分解成2+3嗎? 不能,因為5本身也是Fibonacci數;6雖然能分解,但不是分解成1+2+3,而是分解成1+5。

經過上述,我們知道b[i] 是 a[0...i] 能夠構造出的最大數字,那麼a[i +1] = b[i]+1;因為a陣列(Fib陣列)所存的數字都是不可構造的(取到它本身就是必敗態),顯然a[0...i]構造的最大數字 + 1 即為下一個不可構造的數字了(a[i + 1])。

然後關於b[i]的計算,既然是a[0...i]構造最大數字,那麼 a[i]是一定要選用的(這兒需要一定的推理,a[i]構造數字時,相鄰的j個是不能同時用的,就像上述的2、3不能構造出5一樣,推理請自己完成),那麼要選用的下一項只能遞減尋找,直到找到 a[t] 滿足 a[t] * K < a[i] ,而b[t]就是a[0...t]所能構造的最大數字,再加上a[i], 即為a[0...i]能構造的最大數字,於是b[i] = b[t] + a[i]。

求的數列後,之後的工作就簡單了,跟Fibonacci博弈一樣一樣的,如果n=數列中的數,則必敗,否則必勝;必勝時還要求輸出第一步取法,其實就是分解的數列中最小的一個,將見程式碼。

大神的思路就是明白,再次膜拜,其實懂了斐波那契博弈後完全就會理解k倍動態規劃的原理。附上大神程式碼一份:
#include 
#define N 20000005

int a[N], b[N];			// a 為數列, b 儲存 a[0...i] 能構造出的最大的數

int main()
{
    int n, k;
    int loop = 0, casei = 1;
	scanf("%d",&loop);
	while(loop --){
        scanf("%d%d",&n,&k);
        a[0] = b[0] = 1;
        int i = 0, j = 0;

        while(n > a[i]){			// 構建數列
            i ++;
            a[i] = b[i - 1] + 1;
            while(a[j + 1] * k < a[i])
                j ++;
            if(k * a[j] < a[i])
                b[i] = b[j] + a[i];
            else
				b[i] = a[i];
        }

        printf("Case %d: ", casei ++);
        if(n == a[i])
			printf("lose\n");
        else{
            int ans;
            while(n){
                if(n >= a[i]){		// 構成 n 的最小的數列中的數字,即為第一次要取的數字
                    n -= a[i];
                    ans = a[i];
                }
                i --;
            }
            printf("%d\n",ans);
        }
    }
    return 0;
}