1. 程式人生 > >第七屆藍橋杯取球博弈詳解

第七屆藍橋杯取球博弈詳解

                                                                                        取球博弈
兩個人玩取球的遊戲。
一共有N個球,每人輪流取球,每次可取集合{n1,n2,n3}中的任何一個數目。
如果無法繼續取球,則遊戲結束。
此時,持有奇數個球的一方獲勝。
如果兩人都是奇數,則為平局。

假設雙方都採用最聰明的取法,
第一個取球的人一定能贏嗎?
試程式設計解決這個問題。

輸入格式:
第一行3個正整數n1 n2 n3,空格分開,表示每次可取的數目 (0<n1,n2,n3<100)
第二行5個正整數x1 x2 ... x5,空格分開,表示5局的初始球數(0<xi<1000)

輸出格式:
一行5個字元,空格分開。分別表示每局先取球的人能否獲勝。
能獲勝則輸出+,
次之,如有辦法逼平對手,輸出0,
無論如何都會輸,則輸出-
例如,輸入:
1 2 3
1 2 3 4 5
程式應該輸出:
+ 0 + 0 -
再例如,輸入:
1 4 5
10 11 12 13 15
程式應該輸出:
0 - 0 + +
再例如,輸入:
2 3 5
7 8 9 10 11
程式應該輸出:

+ 0 0 0 0

思路:看了下資料量,需要進行動態規劃,否則只能過小資料。基本思路如下:

定義dp陣列,存放當前輪到的玩家,A玩家手裡球數,B玩家手裡球數,這樣一個儲存陣列

A為0,B為1

假如現在能摸的數量為1,2,3,球總數為5

如果輪到先手A摸球,那麼當前局面應該是(0,0,0,5),第二和第三個引數是當前A和B手裡的球數,因為剛開局兩人手裡一個球也沒有

A試探摸1,2,3,看看摸完後的局面,比如試探摸1個球,那麼應該跳轉到這個局面(1,1,0,5),1表示輪到B玩家了,A已經摸過了,因為A摸了一個球,所以第二個引數是1

最後如果不能摸了,則對當前局面進行判斷,A%2,B%2 分別判斷A和B手上球的奇偶性。

程式碼:

import java.util.Scanner;

public class 取球遊戲 {
	static int ns[] = new int[3];//可取數目
	static int xs[] = new int[5];//初始的球數
	static char dp[][][] = new char[2][1000][1000];//動態規劃儲存陣列,儲存的是玩家方,A玩家手上球數,B玩家手上球數這個局面的結果
	
	//求陣列的最小值
	public static int min(int a[]) {
		int min=9999;
		for (int i : a) {
			if (min>i) {
				min=i;
			}
		}
		return min;
	}
	 //玩家名,A玩家手裡球數,B玩家手裡球數,開局球數
	static char dfs(int p,int Aown,int Bown,int totoalNum){
		int min = min(ns);//可取球數的最小值
		int rest = totoalNum - Aown - Bown;//當前局中場上剩餘球數,總球數-A擁有球數-B擁有的球數
		if (dp[p][Aown][Bown] != '\u0000') {//java中char型別如果沒有初始化就是\u0000 即一個空格
			return dp[p][Aown][Bown];//動態規劃,如果已經計算過(初始化過),直接返回即可
		}
		if (rest < min) {//如果最小能取2個,而場上只有1個,則雙方都不能再去,開始判輸贏
			if (Aown % 2 == 1 && Bown % 2 == 0) {//如果A奇B偶,則贏,注意+表示的是A贏這個局面
				dp[p][Aown][Bown] = '+';  
			}
			else if (Aown % 2 == 0 && Bown % 2 == 1) {//A輸局面
				dp[p][Aown][Bown] = '-';
			}else {
				dp[p][Aown][Bown] = '0';
			}
			return dp[p][Aown][Bown];
		}
		
		if (p==0) {//如果當前輪到先手A取球
			boolean iseq = false;//判斷是否有和的可能
			for (int i = 0; i < ns.length; i++) {//遍歷可取的球數
				if (rest >= ns[i]) { //如果可以取,如果最小能取2個,而場上只有1個,就不能取,ns[i] 是當前在嘗試的取球數
					if (dfs(1, Aown + ns[i], Bown,totoalNum)=='+') {//對接下來的取球進行遍歷,只要找到一個取球方法使得A贏,那麼A就能贏
						return dp[0][Aown][Bown] = '+';
					}else if (dfs(1, Aown + ns[i], Bown,totoalNum) == '0') {//如果找到能和的局面,則標記能和
						iseq = true;
					}
				}
			}
			//for迴圈遍歷結束,走到這裡已經肯定不能贏
			if (iseq) //嘗試和局,看是否標記能和
				dp[0][Aown][Bown] = '0';
			else 
				dp[0][Aown][Bown] = '-';
			
		}
		if (p==1) {//如果輪到後手B取球
			boolean iseq = false;
			for (int i = 0; i < ns.length; i++) {
				if (rest >= ns[i]) { //如果可以取,如果最小能取2個,而場上只有1個,就不能取,ns[i] 是當前在嘗試的取球數
					if (dfs(0, Aown , Bown+ ns[i],totoalNum)=='-') {//只要在A的儲存數組裡找到一個會輸的局面,那A就會輸,因為採用最聰明取法
						return dp[1][Aown][Bown] = '-';
					}else if (dfs(0, Aown , Bown+ ns[i],totoalNum) == '0') {//看是否能和
						iseq = true;
					}
				}
			}
			//走到這裡已經不能贏,看是否能和
			if (iseq) 
				dp[1][Aown][Bown] = '0';
			else 
				dp[1][Aown][Bown] = '+';
			
		}
		return dp[p][Aown][Bown];
		
	}
	
	
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		for (int i = 0; i < ns.length; i++) {
			ns[i] = scanner.nextInt();
		}
		for (int i = 0; i < xs.length; i++) {
			xs[i] = scanner.nextInt();
		}
		int k;
		for (int i = 0; i < xs.length; i++) {
			dp = new char[2][1000][1000];//每次新開一局要重新初始化dp陣列
			k = dfs(0, 0, 0,xs[i]);
			System.out.print((char)k + " ");
		}
	}
}

總結:+,-是具有絕對性的,+代表A贏,-代表A輸。

if (dfs(1, Aown + ns[i], Bown,totoalNum)=='+') {

return dp[0][Aown][Bown] = '+';

上面這段A在尋找有沒有一種能讓A(自己)贏的局面

if (dfs(0, Aown , Bown+ ns[i],totoalNum)=='-') {

return dp[1][Aown][Bown] = '-';

這段B在尋找下一個跳轉局面裡有沒有一種能讓A輸的局面,即B(自己)贏的局面

A,B兩人都尋找能讓對方輸掉或自己贏的局,找到一個就採用這種取法,找不到就看看有沒有能和的局面,再找不到就是輸了

為什麼每開一局要重新初始化dp陣列,之前的資料不能重複用嗎?

不行,因為同一個dp[0][1][0] 在開局球數不同的情況下意義也不同。

如果不清除,那麼在當前開局球數為5,A摸了1個的情況下,會呼叫之前存著的開局球數為1,A已經摸了一個的情況

後者顯然是必贏局,與前者結局不同