编程之美 - 一排石头游戏及扩展问题
2016-02-10 11:42
519 查看
问题:一堆石头排成一排,两个人轮流从其中抓取一块或两块石头(两块石头必须是挨着的),谁拿到了最后的石头,谁就是赢家,编写算法保证先抓的人一定能赢。
思路:
假设有三块石头,甲先拿中间的一块,这样无论乙怎么拿甲都会赢。
如果有四块石头,甲先拿中间的两块,这样无论乙怎么拿甲也会赢。
再扩展一下,如果有五块石头,甲先拿中间的一块,如果下面乙拿一块,甲就拿和乙中心对称的一块,这样甲还是会赢。
规律就是如果是奇数块石头,先拿的就拿中间的一块;如果是偶数块先拿的就先拿中间的两块。剩下的只要和对方的基于中心点对称就一定会赢。
程序实现:
测试结果:
---------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------
扩展问题,如果把规则变为抓到最后一个石头的人输呢?
思路:还是从头开始一块,两块的开始分析
1 块: 先抓必输 把这种情况定义为 0 First Lose FL
2 块: 先抓必赢 把这种情况定义为 1 First Win FW
3 块: 先抓必赢 FW
4 块: 1 + 3 先抓 1 块,剩3块,那后抓的人一定会赢 FL
2 + 2 先抓 2 块,剩2块,那后抓的人一定会赢
---------------------------------------------------------------------------------------------------------------------------------------------------
前三种情况很明显,从第 5 块开始需要分解
5 块: A可以选择取一块或取两块
那么取完 1 块后,B可能面临的情况:
1) 4 B 输
2) 1 || 3 B 赢
3) 2 || 2 B 输
那么取完 2 块后,B可能面临的情况:
4) 3 B 赢
5) 1 || 2 B 赢
A可以选择方案 1 或 3 必胜
6 块:
那么取完 1 块后,B可能面临的情况:
1) 5 B 赢
2) 1 || 4 B 赢
3) 2 || 3 B 赢
那么取完 2 块后,B可能面临的情况:
4) 4 B 输
5) 1 || 3 B 赢
6) 2 || 2 B 输
A可以选择方案 4 或 6 必胜
7 块:
那么取完 1 块后,B可能面临的情况:
1) 6 B 赢
2) 1 || 5 B 输
3) 2 || 4 B 赢
4) 3 || 3 B 输
那么取完 2 块后,B可能面临的情况:
5) 5 B 赢
6) 1 || 4 B 赢
7) 2 || 3 B 赢
A可以选择方案 2 或 4 必胜
8 块:
那么取完 1 块后,B可能面临的情况:
1) 7 B 赢
2) 1 || 6 B 赢
3) 2 || 5 B 赢
4) 3 || 4 B 输
那么取完 2 块后,B可能面临的情况:
5) 6 B 赢
6) 1 || 5 B 输
7) 2 || 4 B 赢
8) 3 || 3 B 输
A可以选择方案 4,6,8 必胜
9 块:
那么取完 1 块后,B可能面临的情况:
1) 8 B 赢
2) 1 || 7 B 赢
3) 2 || 6 B 赢
4) 3 || 5 B 赢
5) 4 || 4 B 赢
那么取完 2 块后,B可能面临的情况:
6) 7 B 赢
7) 1 || 6 B 赢
8) 2 || 5 B 赢
9) 3 || 4 B 赢
当 9块石头时A 一定会输
因为当 N = 4 或 9时先取的一定会输,N = 5,6,7,8先取一定会赢
可以看出基于这个规则,先取无法保证一定会赢。
总结一下规律
规律 1:
对于目标数字进行分解,如果发现分解后的一个组合可以让对方必输的,则可以把当前的分解看做是先手必胜的。
如果,发现一个数字的所有分解都是
FW(first win) 的,那当前这个组合是必输的。例如 9,分解后都是对方赢的,那面对 9 时就是必输的了。
规律 2:
1:N ==> 有奇数个1时先取的输,偶数时先取的赢
2:N ==> 有奇数个key时先取的赢,偶数时先取的输
当1 和 2 有组合时
1 || 2 ==> FW
1 || 1 || 2 ==> FW
1 || 2 || 2 ==> FW
1 || 1 || 2 || 2 ==> FL
1 || 2 || 2 || 2 ==> FW
1 || 1 || 2 || 2 || 2 ==> FW
当有奇数个2时,不用考虑 1 的个数,先手都会赢。
当有偶数个2时,如果有奇数个 1,先手会赢。
当有偶数个2时,如果有偶数个 1,后手会赢。
算法描述:
1)扫描当前石头情况,得到当前分解的情况:
例如:
O O O O O : seq = 5
O X O O O : seq = 1 | | 3
2)如果当前分解中有大于 2 的数字就需要继续分解得到子序列,此处需要用到递归
例如:
O O O O O : seq = 5
可以分解为:
X O O O O : sub_seq = 0 || 4
O X O O O : sub_seq = 1 || 3
O O X O O : sub_seq = 2 || 2
4 和 1 || 3需要继续分解
3)当完全分解完全到位后,可根据上面的规律2可以知道它的序列是 FW(First Win) 或 FL(First Lose)
序列中没有大于2的数字
4)通过子序列的 FW和FL属性可以得父序列是 FW(First Win) 或 FL(First Lose)
如果子序列全是 FW,则父序列是 FL
如果子序列中有一个是FL,则父序列是 FW
5)当得到最原始序列的FW(First Win) 或 FL(First Lose)属性后,如果是FW则自己先选,如果是FL的则可以让对手先选
思路:
假设有三块石头,甲先拿中间的一块,这样无论乙怎么拿甲都会赢。
如果有四块石头,甲先拿中间的两块,这样无论乙怎么拿甲也会赢。
再扩展一下,如果有五块石头,甲先拿中间的一块,如果下面乙拿一块,甲就拿和乙中心对称的一块,这样甲还是会赢。
规律就是如果是奇数块石头,先拿的就拿中间的一块;如果是偶数块先拿的就先拿中间的两块。剩下的只要和对方的基于中心点对称就一定会赢。
程序实现:
#include <iostream> using namespace std; bool take_stone(char *stones, int len, int start, int num) { bool bRet = true; if ((start < 0) || ((start+num) > len) || (num < 1 || num > 2)) return false; if (((num == 1) && (stones[start] != 'O')) || ((num == 2) && ((stones[start] != 'O') || (stones[start+1] != 'O')))) return false; if (num == 1) stones[start] = 'x'; else if (num == 2) { stones[start] = 'x'; stones[start+1] = 'x'; } return bRet; } bool scan(char *stones, int len, int start, int num, int round) { int i = 0, mid = 0; bool bRet = false; mid = (len+1)/2; if (round == 0) { if (len%2 == 1) { mid = (len-1)/2; stones[mid] = 'x'; } else { mid = len/2 - 1; stones[mid] = 'x'; stones[mid+1] = 'x'; } } else { mid = len - 1 - start; stones[mid] = 'x'; mid = mid - (num-1); stones[mid] = 'x'; } for (i=0; i < len; i++) { if (stones[i] == 'O') { bRet = true; break; } } if (!bRet) { cout << "PROGRAM WIN!!" << endl; bRet = false; } return bRet; } void print(char *stones, int len) { int i = 0; for (i=0; i < len; i++) { cout << stones[i] << " "; } cout << endl; } void play(char *stones, int len) { int nStart=0, nNum=0; int round = 0; while(scan(stones, len, nStart, nNum, round)) { print(stones, len); cout << "please input the start stone and will take how many(max is 2)" << endl; cin >> nStart >> nNum; while (!take_stone(stones, len, nStart, nNum)) { cout << "please re-input your choose" <<endl; cin >> nStart >> nNum; } round++; } print(stones, len); } void main() { char test[]={'O','O','O','O','O','O','O','O'}; int len = 8; play(test, len); cin >> len; }
测试结果:
---------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------
扩展问题,如果把规则变为抓到最后一个石头的人输呢?
思路:还是从头开始一块,两块的开始分析
1 块: 先抓必输 把这种情况定义为 0 First Lose FL
2 块: 先抓必赢 把这种情况定义为 1 First Win FW
3 块: 先抓必赢 FW
4 块: 1 + 3 先抓 1 块,剩3块,那后抓的人一定会赢 FL
2 + 2 先抓 2 块,剩2块,那后抓的人一定会赢
---------------------------------------------------------------------------------------------------------------------------------------------------
前三种情况很明显,从第 5 块开始需要分解
5 块: A可以选择取一块或取两块
那么取完 1 块后,B可能面临的情况:
1) 4 B 输
2) 1 || 3 B 赢
3) 2 || 2 B 输
那么取完 2 块后,B可能面临的情况:
4) 3 B 赢
5) 1 || 2 B 赢
A可以选择方案 1 或 3 必胜
6 块:
那么取完 1 块后,B可能面临的情况:
1) 5 B 赢
2) 1 || 4 B 赢
3) 2 || 3 B 赢
那么取完 2 块后,B可能面临的情况:
4) 4 B 输
5) 1 || 3 B 赢
6) 2 || 2 B 输
A可以选择方案 4 或 6 必胜
7 块:
那么取完 1 块后,B可能面临的情况:
1) 6 B 赢
2) 1 || 5 B 输
3) 2 || 4 B 赢
4) 3 || 3 B 输
那么取完 2 块后,B可能面临的情况:
5) 5 B 赢
6) 1 || 4 B 赢
7) 2 || 3 B 赢
A可以选择方案 2 或 4 必胜
8 块:
那么取完 1 块后,B可能面临的情况:
1) 7 B 赢
2) 1 || 6 B 赢
3) 2 || 5 B 赢
4) 3 || 4 B 输
那么取完 2 块后,B可能面临的情况:
5) 6 B 赢
6) 1 || 5 B 输
7) 2 || 4 B 赢
8) 3 || 3 B 输
A可以选择方案 4,6,8 必胜
9 块:
那么取完 1 块后,B可能面临的情况:
1) 8 B 赢
2) 1 || 7 B 赢
3) 2 || 6 B 赢
4) 3 || 5 B 赢
5) 4 || 4 B 赢
那么取完 2 块后,B可能面临的情况:
6) 7 B 赢
7) 1 || 6 B 赢
8) 2 || 5 B 赢
9) 3 || 4 B 赢
当 9块石头时A 一定会输
因为当 N = 4 或 9时先取的一定会输,N = 5,6,7,8先取一定会赢
可以看出基于这个规则,先取无法保证一定会赢。
总结一下规律
规律 1:
对于目标数字进行分解,如果发现分解后的一个组合可以让对方必输的,则可以把当前的分解看做是先手必胜的。
如果,发现一个数字的所有分解都是
FW(first win) 的,那当前这个组合是必输的。例如 9,分解后都是对方赢的,那面对 9 时就是必输的了。
规律 2:
1:N ==> 有奇数个1时先取的输,偶数时先取的赢
2:N ==> 有奇数个key时先取的赢,偶数时先取的输
当1 和 2 有组合时
1 || 2 ==> FW
1 || 1 || 2 ==> FW
1 || 2 || 2 ==> FW
1 || 1 || 2 || 2 ==> FL
1 || 2 || 2 || 2 ==> FW
1 || 1 || 2 || 2 || 2 ==> FW
当有奇数个2时,不用考虑 1 的个数,先手都会赢。
当有偶数个2时,如果有奇数个 1,先手会赢。
当有偶数个2时,如果有偶数个 1,后手会赢。
算法描述:
1)扫描当前石头情况,得到当前分解的情况:
例如:
O O O O O : seq = 5
O X O O O : seq = 1 | | 3
2)如果当前分解中有大于 2 的数字就需要继续分解得到子序列,此处需要用到递归
例如:
O O O O O : seq = 5
可以分解为:
X O O O O : sub_seq = 0 || 4
O X O O O : sub_seq = 1 || 3
O O X O O : sub_seq = 2 || 2
4 和 1 || 3需要继续分解
3)当完全分解完全到位后,可根据上面的规律2可以知道它的序列是 FW(First Win) 或 FL(First Lose)
序列中没有大于2的数字
4)通过子序列的 FW和FL属性可以得父序列是 FW(First Win) 或 FL(First Lose)
如果子序列全是 FW,则父序列是 FL
如果子序列中有一个是FL,则父序列是 FW
5)当得到最原始序列的FW(First Win) 或 FL(First Lose)属性后,如果是FW则自己先选,如果是FL的则可以让对手先选
示例程序
相关文章推荐
- Python eval函数
- php字符串处理函数(下)
- Configuring Beans in the Spring IoC Container
- java写库读库相关
- Java设计模式(十九)----备忘录模式
- 使用FileZilla等软件搭建ftp服务器
- u-boot-2016.01之支持yaffs以及制作补丁
- Java 方法参数的值传递和引用传递
- python核心编程-第六章习题答案
- spring(DI) 懒加载的执行顺序、通过构造函数给属性赋值
- 第六章函数
- java中this关键字
- php字符串处理函数(上)
- python 使用requests第三方库自动登陆新浪微博
- java基本语法
- c#委托
- spring三种实例化bean的方式
- C、C++中命名规范
- 给牛牛上编程课——第三次课(字符串的存储和打印、转义字符)
- struts2文件上传下载(含中文编码问题)