题目
高僧斗法
古时丧葬活动中经常请高僧做法事。仪式结束后,有时会有“高僧斗法”的趣味节目,以舒缓压抑的气氛。
节目大略步骤为:先用粮食(一般是稻米)在地上“画”出若干级台阶(表示N级浮屠)。又有若干小和尚随机地“站”在某个台阶上。最高一级台阶必须站人,其它任意。(如图1所示)
两位参加游戏的法师分别指挥某个小和尚向上走任意多级的台阶,但会被站在高级台阶上的小和尚阻挡,不能越过。两个小和尚也不能站在同一台阶,也不能向低级台阶移动。
两法师轮流发出指令,最后所有小和尚必然会都挤在高段台阶,再也不能向上移动。轮到哪个法师指挥时无法继续移动,则游戏结束,该法师认输。
对于已知的台阶数和小和尚的分布位置,请你计算先发指令的法师该如何决策才能保证胜出。
输入数据为一行用空格分开的N个整数,表示小和尚的位置。台阶序号从1算起,所以最后一个小和尚的位置即是台阶的总数。(N<100,
台阶总数<1000)输出为一行用空格分开的两个整数: A B, 表示把A位置的小和尚移动到B位置。若有多个解,输出A值较小的解,若无解则输出-1。
例如:
用户输入:
1 5 9
则程序输出:
1 4
再如:
用户输入:
1 5 8 10
则程序输出:
1 3
资源约定:
峰值内存消耗 < 64M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
普通博弈解法
对于一个特定局面,假设小和尚站的位置为 1 5 8 10
,对于每个小和尚,尝试他当前能走的每一种步数,递归地判断新局面是否为负
,如果为负
可立即返回胜
,如果当前局面每个小和尚都尝试了每种走法仍得不出胜
的结果,那个对于当前法师,问题无解。
package org.lanqiao.algo.lanqiaobei.game;
import java.util.Scanner;
public class 高僧斗法 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String[] ss = scanner.nextLine().split(" ");
int[] x = new int[ss.length];
//将小和尚的位置作为数组元素的值
for (int i = 0; i < x.length; i++)
x[i] = Integer.parseInt(ss[i]);
if (!f(x)) {
System.out.println(-1);
} else {
System.out.println(a + " " + b);
}
}
static int a;
static int b;
private static boolean f(int[] x) {
for (int i = 0; i < x.length - 1; i++) {
//i位置向后尝试每一个可走的位置
for (int j = x[i] + 1; j < x[i + 1]; j++) {
int old = x[i];//记录i位置的原始值
x[i] = j; //该表i位置的值
try {
//生成新的局面,如果这个局面判定对方输,那我们就赢了
if (!f(x)) {
a = old;
b = j;
return true;
}
} finally {
x[i] = old;// 恢复i位置的值,进行下一次尝试
}
}
}
//for循环无法进行,或者整个for循环走完都没有一次变动能造成对方输,就返回false
return false;
}
}
这种解法能应对1 5 8 10
这样的输入,即个数少,间隔也小的,如果输入为1 30 40 99
,在规定时间内是无法完成计算的。
尼姆堆解法
关于尼姆游戏,请移步如何在取硬币游戏中必胜?(有关尼姆博弈) - 知乎先了解下定义。
高僧斗法问题,怎么转换成类似取硬币这样的尼姆定式呢?
我们可以把两个小和尚作为一个组合生成一堆(间隔为堆数量),对于输入1 5 8 10
,转换为尼姆堆N={3,1}。如果是奇数个小和尚,如1 5 8 10 13
,我们在最高阶假想一个小和尚,这样第三堆数目为0,此时尼姆堆N={3,1,0}。
对于这样的尼姆堆,我方作为先手,堆元素全部异或起来结果为0,那我们处于先手必败局面,如果全部异或起来不为0,那我们可以改变尼姆堆某一堆的大小来促成异或值为0,这样把必败局面留给对方。
这样做效率能大大提升,因为只需要计算初始形态,不用考虑后面怎么走,因为一旦形成P-position
,对方无论怎么走我都可以恢复为P-position
。
这里有一个比较难想通的问题是,为什么两两组合呢,组合之间的间隔不考虑吗?
如果我方走5号小和尚,无论怎么走对方都可以跟随,这样我们既定的尼姆堆没有任何的改变,也就是你造成了一个P-position
对手又回你一个P-position
。如果我们通过2、8号和尚改变了尼姆堆,形成全异或为0 的P-position
局面,此时无论对手走什么我们可以跟随或改变另外一个尼姆堆来再给对方一个P-position
。
好了,上代码吧;比较烧脑:
static boolean f1(int[] x) {
int[] N = new int[x.length / 2];// 堆的大小
for (int i = 0; i < N.length; i++) {
N[i] = x[2 * i + 1] - x[2 * i]-1;// 计算每个堆的数字
}
int k = N[0];
for (int i = 1; i < N.length; i++) {
k ^= N[i]; //连续做异或
}
// 现在异或不为0
if (k != 0) {
// 找到ni,其x位(k最高位)为1
String kBinary = Integer.toBinaryString(k);
for (int i = 0; i < N.length; i++) {
//当前堆数字的二进制字符串
String NiBinary = Integer.toBinaryString(N[i]);
try {
//右对齐后和k最高位在同一列的二进制为1
if (NiBinary.charAt(NiBinary.length() - kBinary.length()) == '1') {
a = x[2*i]; // 首动位置找到了
//现在Ni 变为Ni' 使得尼姆堆的全体数字异或为0 只需把k所有为1的位对应的Ni上的位做0~1变换
int Nii = N[i];
for (int j = 0; j < kBinary.length(); j++) {
if (kBinary.charAt(j)=='1')
Nii^=(1<<(kBinary.length()-j-1));
}
// N[i]变为Nii肯定是缩小了,缩小的数就是a应该前进的数
b = a+N[i]-Nii;
break;
}
} catch (Exception e) {
// 位数不够
}
}
return true;
}
return false;
}
改进之后即便输入1 30 40 99
,也能很快计算出答案。