NIM(3)两堆石头的游戏
1. 问题描述
假设有两堆石头,有两个玩家会根据如下的规则轮流取石头:每人每次可以从两堆石头中各取出数量相等的石头,或者仅从一堆石头中取出任意数量的石头;最后把剩下的石头一次拿光的人获胜。请问在哪些局面(依据两堆石头中的石头个数)下,先取石头的玩家有必胜的策略。
2. 解法
类似构造质数的筛选方法,这里我们利用找到的必输局面(后取的玩家有必胜策略)来筛去掉能通过一次操作达该必输局面的其它必胜局面(先取的玩家有必胜策略)。最后选出的局面都是必输局面。
构造必胜策略:
如果一开始的局面就是必输局面,那么可能先取的玩家没有必胜策略(当然如果后取的玩家不太聪明,先取的玩家依然有可能能赢)。如果一开始的局面不是必输局面,那么先取的玩家一定有必胜策略,且必胜策略就是保证每次都将当前非必输局面转变为必输局面(后取的玩家必输)。
先取者有必胜策略的局面为“安全局面”,而先取者无必胜策略的局面为“不安全局面”。通过筛选可得如下不安全局面表:
不安全局面表
N | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ... |
an | 1 | 3 | 4 | 6 | 8 | 9 | 11 | 12 | 14 | 16 | ... |
bn | 2 | 5 | 7 | 10 | 13 | 15 | 18 | 20 | 23 | 26 | ... |
有了通项公式,我们就能更加简明地实现函数bool nim(n,m),这个函数的时间复杂度为O(min(m,n))。代码如下:
1 package chapter1youxizhileNIM3; 2 /** 3 * 两堆石头的游戏 4 * @author DELL 5 * 6 */ 7 public class NIM3 { 8 /** 9 * 计算玩家是否能赢得游戏10 * @param n 其中一堆石头的数量11 * @param m 另一堆石头的数量12 * @return 输赢状态13 */14 public static boolean nim(int n, int m){15 double a,b;16 int temp;17 a = (1+Math.sqrt(5.0))/2;18 b = (3+Math.sqrt(5.0))/2;19 if(m == n)20 return true;21 if(m < n){ //保证m>n22 temp = m;23 m = n;24 n = temp;25 }26 int i = n;27 while(i>=1){ //由于序号i值一定小于等于min(m,n)所以遍历查找28 if((int)Math.floor(a*i)==n&&(int)Math.floor(b*i)==m) 29 return false;30 i--;31 }32 return true;33 }34 public static void main(String[] args) {35 if(nim(5,3))36 System.out.println("(5,3)有必胜策略!");37 else38 System.out.println("(5,3)没有必胜策略!");39 40 if(nim(3,6))41 System.out.println("(3,6)有必胜策略!");42 else43 System.out.println("(3,6)没有必胜策略!");44 }45 46 }
程序运行结果如下:
(5,3)没有必胜策略!(3,6)有必胜策略!
3. 扩展问题
1. 现在我们已经给出了一个判断先取者是否能够最终赢得游戏的判断函数,但是,游戏的乐趣在于过程,大家能不能根据本题的思路给出一个赢得游戏的必胜策略呢?即根据当前石头的个数,给出当前玩家下一步要怎么取石头才能必胜。
程序如下:
1 package chapter1youxizhileNIM3; 2 /** 3 * 两堆石头的游戏 4 * @author DELL 5 * 6 */ 7 public class NIM3 { 8 /** 9 * 计算玩家是否能赢得游戏10 * @param n 其中一堆石头的数量11 * @param m 另一堆石头的数量12 * @return 输赢状态13 */14 public static boolean nim(int n, int m){15 double a,b;16 int temp;17 a = (1+Math.sqrt(5.0))/2;18 b = (3+Math.sqrt(5.0))/2;19 if(m == n)20 return true;21 if(m < n){ //保证m>n22 temp = m;23 m = n;24 n = temp;25 }26 int i = n;27 while(i>=1){ //由于序号i值一定小于等于min(m,n)所以遍历查找28 if((int)Math.floor(a*i)==n&&(int)Math.floor(b*i)==m)29 return false;30 i--;31 }32 return true;33 }34 35 public static void result(int n,int m){36 int min,max; //存储m,n中的最小值和最大值37 int i;38 if(m<=n){39 min = m;40 max = n;41 }else{42 min = n;43 max = m;44 }45 if(m==n)46 System.out.println("("+n+", "+m+")取后的状态为(0, 0)");47 if(nim(n,m)){48 for(i=1;i<=min;i++){49 if(!nim(n-i,m-i)){50 System.out.println("("+n+", "+m+")取后的状态为("+(n-i)+", "+(m-i)+")");51 return;52 }53 if(!nim(n-i,m)){54 System.out.println("("+n+", "+m+")取后的状态为("+(n-i)+", "+m+")");55 return;56 }57 if(!nim(n,m-i)){58 System.out.println("("+n+", "+m+")取后的状态为("+n+", "+(m-i)+")");59 return;60 }61 }62 for(i=min+1;i<=max;i++){63 if(n>=m){64 if(!nim(n-i,m)){65 System.out.println("("+n+", "+m+")取后的状态为("+(n-i)+", "+m+")");66 return;67 }68 }else{69 if(!nim(n,m-i)){70 System.out.println("("+n+", "+m+")取后的状态为("+n+", "+(m-i)+")");71 return;72 }73 }74 }75 }else{76 System.out.println("无论怎么取都没有必胜的策略!");77 }78 }79 public static void main(String[] args) {80 result(3,6);81 result(6,6);82 }83 84 }
程序运行结果如下:
(3, 6)取后的状态为(3, 5)(6, 6)取后的状态为(0, 0)
2. 取石头的游戏已经不少了,但是我们还有一种游戏要请大家思考,我们姑且叫它NIM(4):
两个玩家,只有一堆石头,两人依次拿石头,最后拿光者为赢家。取石头的规则是:
(a)第一个玩家不能拿光所有的石头。
(b) 第一次拿石头之后,每人每次最多只能拿掉对方前一次所拿石头的两倍。
那么,这个游戏有没有必胜的算法?(提示:好像和Fibonacci数列有关。)
经分析可知,当这一堆石头的数量为Fibonacci值的时候,先拿者没有必胜策略。
判断的算法如下:
1 package chapter1youxizhileNIM3; 2 /** 3 * 扩展问题 4 * 两个玩家,只有一堆石头,两人依次拿石头,最后拿光者为赢家。取石头的规则是: 5 6 (a)第一个玩家不能拿光所有的石头。 7 8 (b) 第一次拿石头之后,每人每次最多只能拿掉对方前一次所拿石头的两倍。 9 10 那么,这个游戏有没有必胜的算法?(提示:好像和Fibonacci数列有关。)11 * @author DELL12 *13 */14 public class NIM4 {15 /**16 * 计算玩家是否能赢得游戏17 * @param n 给定的一堆石头的数量18 * @return 输赢状态19 */20 public static boolean nim(int n){21 int f1 = 1, f2 = 1; //Fibonacci数列的前两个值22 int f3;23 if(n==1)24 return false;25 do{26 f3 = f1+f2;27 f1 = f2;28 f2 = f3;29 }while(f3
程序运行结果如下:
(3)没有必胜策略!(4)有必胜策略!