圈叉棋、套娃圈叉棋、嵌套圈叉棋、九个井字棋

目录

一,圈叉棋(Tic-Tac-Toe)

二,套娃圈叉棋

1,规则

2,解空间分析

1,节点

2,解空间结构

3,复杂度分析

1,总节点数目

2,思路一——关键节点

3,思路二——对称性

4,数据结构

1,颜色

2,数目

5,编码实现

1,有效颜色状态

2,有效数目状态

三,嵌套圈叉棋

四,九个井字棋

五,n阶圈叉棋

力扣 面试题 16.04. 井字游戏


一,圈叉棋(Tic-Tac-Toe)

 圈叉棋是一种游戏,3*3的9个方格子,先下者画圈,后下者画叉,每人可以在任意没有对方棋子的封闭方格里下一次,看谁先连成一行(一列,斜线)3个就判胜。由于圈叉棋规则简单,在任何地方都可以玩,所以是打发时间的有效的休闲游戏。现在几乎全球人都了解圈叉棋。


之前一直不知道为什么圈叉棋竟然可以风靡全球,因为对于2个智商足够高的人来说,结局肯定是平局(即双方均有不败策略)而且平局总共也就只有321种不同的情况。

今天把所有情况都列了出来,发现还是有点意思的。

(按照上面的图来编号,我所说的321种不同的情况是指,通过翻转、旋转之后不能重合的下法。)

(然而,我列出的321种只是一种上界,所有的平局都能通过翻转、旋转之后变成这321种中的一种,但是这321种之间,应该还是有些可以通过翻转、旋转重合的)

首先,第一个回合,不同的平局只有15,21,25,28,51这5种!

其次,可以得出后手的不败策略:

第一步,先手下5就下1,否则(另外的8种情况)就下5。

第二步,比第一步简单地多,但是已经不好总结了,就不总结了。总之已经基本上已经不会输了。

当然了,也可以得出先手的不败策略,不过先手基本上都不会输。。。

所以我总结一下先手和一个还不知道不败策略的人下棋的时候的方法:

如果下2的话,后手不管下哪儿都还可能是平局。

如果下5的话,看后手怎么下,如果下2就干掉他,如果下1的话,很可能就是平局了。

如果下1的话,后手很可能下5,然后就很可能平局,如果后手不下5,当然可以直接干掉他了。

总之,和一个第一次玩圈叉棋的人一样,第一步还是下5合理一些,除非你对后手有一定的了解,那当然另当别论了。

讨论一下圈叉棋的平局到底有多少种情况(仅限双方都不犯错的情况)

第一个回合即前2步,有15,21,25,28,51这5种情况。

然后分别讨论,看前6步有多少种情况,可以枚举出来,有138种情况。

最后,还有3步。

这138种情况中,有77种情况,最后3步只有1种情况,还有61种情况,最后3步有4种情况。

所以一共是77+61*4=321种情况。

最后,附上圈叉棋321种不同的下法

(长度为9的序列是棋子落子的顺序,比如152374689就是先手下1,然后后手下5,然后先手下2。。。。。。)

(?*4就是说,最后3步有4种情况,不是简单地为了简写,而是为了表达出这4种情况之间的共性)

(15开头的有9+4*4=25种)

152374689、152374698、153284697、153286479、156823749、156374*4、156289*4、156287394、156974*4、156923748、156932874、156983742、156982*4

(21开头的有34+30*4=154种)

214593786、214596*4、214635978、214635987、214637*4、214638*4、214658379、214658937、214685973、14695*4、214697*4、21469357、21469375、21469537、215846*4、215864739、215873*4、215893*4、215894736、215896*4、215897463、217598*4、217835*4、217853*4、217845936、217845963、217843*4、217846*4、217863*4、217864*4、217865934、217893456、217893465、217893546、217893564、217895*4、217896*4、219478*4、219536478、219536487、219543786、219546*4、219563784、219578*4、219745368、219746*4、219836*4、219841*4、219845*4、219846357、219846375、219846537、219846573、219863*4、219854736、219857463、219853*4、219856*4、219873456、219873465、219873546、219873564、219875*4、219876*4

(25开头的有11+5*4=31种)

251374689、251374698、254193786、254196*4、254371986、257198*4、257469138、257463*4、257641983、257314689、257314698、257389146、257341986、257398*4、257361984、257364*4

(28开头的有21+19*4=97种)

281359746、281374*4、281395746、284156*4、284165*4、284173*4、284175936、284175963、284176*4、284193*4、284195*4、284196357、284196375、284196537、284196573、284356917、284365719、284371*4、284391*4、284395716、287135964、287143*4、287145936、287145963、287146*4、287153*4、287163*4、287165934、287193456、287193465、287193546、287193564、287195*4、287196*4、287314*4、287341*4、287351*4、287354*4、287356914、287359641

(51开头的2+3*4=14种)

512846*4、512864739、513746*4、516473289、519328*4

二,套娃圈叉棋

1,规则

在圈叉棋的基础上,加上套娃的规则:

每人有9个棋子,3大3中3小,较大的可以把较小的套住。

接下来我将分析这个博弈游戏的胜负规律。

2,解空间分析

1,节点

节点包含的信息有:

  • 9个格子的状态,每个格子有7种状态(空白、双方的大中小棋子)
  • 轮到谁下
  • 双方手里剩下的棋子数量

双方手里剩下的棋子数量可以推算出轮到谁下。

2,解空间结构

这是一个无环的有向图,开局作为起点,到任意节点都有唯一路径存在。

如果去掉方向,那么得到的无向图就有圈了。

3,复杂度分析

1,总节点数目

9个格子的状态有7^9=40353607种,双方手里剩下的棋子数量有(4*4*4)^ 2 = 4096种情况,一共有40353607*4096=1653亿种情况。

考虑到双方手里剩下的大棋子的数量其实也可以推算出来,那么就只有40353607*16*16=103亿种情况。

数目还是有点大,动态规划的备忘录存103亿种状态,需要的内存太大。

2,思路一——关键节点

所有局面最终都会走向三种局面:

  • 胜负已分,但是还有空格。
  • 所有棋子都用完了,平局,但是还有空格。
  • 没有空格。

其中,前两种局面应该是很少的,关键是第三种,没有空格的局面。

没有空格的局面,方手里剩下的小棋子就没用了,所有只有6^9 * 4 * 4 = 1.6亿种情况。

如果每种情况用4字节来表示,那么一共需要不到1G的内存。

我们可以先把所有的关键状态的胜负全部求出来,然后以此为基础再求其他状态的胜负。

3,思路二——对称性

还是从所有状态着手,但是利用对称性进行去重,降低数据量。

4,数据结构

1,颜色

用1个整数表示棋盘上的9个格子的棋子颜色

int color = 0; //18位,低18位是9个颜色,0空1先手2后手
inline int GetColor(int color, int n)//n是0-8
{
	return (color >> (n + n)) & 3;
}
inline int SetColor(int color, int n, int c)//n是0-8,c是0-2
{
	return color ^ (color & (3 << (n + n))) ^ (c << (n + n));
}

2,数目

用1个整数表示棋盘上的9个格子的棋子大小、先后手手里的棋子数、先手还是后手会赢。

int num = 0;//6位,低2位、接下来2位、接下来2位分别是手里小中大棋子的数目,0-3
int nums = 0;//32位,低18位是9个数目,0-3,接下来6位是先手num1,接下来6位是后手num2
			//接下来2位是轮到先手下时,先手的胜负ret,0平1胜2负
int GetNums(int nums)
{
	return nums & 262143;//2^18-1=262143
}
int GetNum1(int nums)
{
	return (nums >> 18) & 63;//2^6-1
}
int GetNum2(int nums)
{
	return (nums >> 24) & 63;
}
int GetRet(int nums)
{
	return (nums >> 30) & 3;//2^2-1
}

5,编码实现

1,有效颜色状态

颜色的组合有3^9=19683种,但是其中一些是无效状态。

如何判断哪些状态是有可能达到的呢?

有两种方法,第一种思路是用搜索算法去枚举所有可能的情况,

第二种思路是只看是否同时存在双方的三子连线。

如果双方都存在三子连线,那肯定是无效状态,如果双方都不存在,那一定是有效状态,如果只有一方存在,那可能有效也可能无效,但是把其中的无效状态视为有效状态不会影响DFS算法的正确性

所以我们认为,只要不是双方都有三子连线,那就是有效的颜色状态。

2,有效数目状态

在某个颜色状态条件下,哪些数目状态是有效的呢?

首先,任意一个格子的颜色和数目一定同时为0或者同时不为0。

其次,双方大中小的棋子都需要满足数量上限。

三,嵌套圈叉棋

taptap小游戏(嵌套井字棋)

1.双方,回合制。每次落子的范围应是某个特定的小九宫格。如果这个特定的小九宫格无位置了或是开局第一手,则落子的范围是整个大九宫格。

2.大九宫格由九个小九宫格组成。每次落子的这一个特定的小九宫格,相对于大九宫格的位置;正对应着对方上一次落子的格子,相对小九宫格的位置。

3.占领小格:每个小九宫格满足井字棋规则。一旦某次落子后,出现某个小九宫格三子一线,这个小九宫格被成线方占领,变成一个大棋子。

4.胜利:大九宫格满足井字棋规则。一旦某次落子后,某三个占领的小九宫格均三子一线(同色),则达成者胜利。

四,九个井字棋

在线play

9个井字棋组成的井字棋游戏。

规则:每一步都可以在81个格子中任意一个格子落子,除非当一个井字有结果(赢或输或者被填满),那么这个井字就不能再下了。游戏最后以输赢的个数比较决定输赢或者平局。

理论上,双方都有不败策略,要想赢,只能找对方的漏洞了。

AI漏洞示例:

我是先手蓝色圈圈,到这一步,我已经有必胜策略了。

五,n阶圈叉棋

力扣 面试题 16.04. 井字游戏

设计一个算法,判断玩家是否赢了井字游戏。输入是一个 N x N 的数组棋盘,由字符" ","X"和"O"组成,其中字符" "代表一个空位。

以下是井字游戏的规则:

  • 玩家轮流将字符放入空位(" ")中。
  • 第一个玩家总是放字符"O",且第二个玩家总是放字符"X"。
  • "X"和"O"只允许放置在空位中,不允许对已放有字符的位置进行填充。
  • 当有N个相同(且非空)的字符填充任何行、列或对角线时,游戏结束,对应该字符的玩家获胜。
  • 当所有位置非空时,也算为游戏结束。
  • 如果游戏结束,玩家不允许再放置字符。

如果游戏存在获胜者,就返回该游戏的获胜者使用的字符("X"或"O");如果游戏以平局结束,则返回 "Draw";如果仍会有行动(游戏未结束),则返回 "Pending"。

示例 1:

输入: board = ["O X"," XO","X O"]
输出: "X"

示例 2:

输入: board = ["OOX","XXO","OXO"]
输出: "Draw"
解释: 没有玩家获胜且不存在空位

示例 3:

输入: board = ["OOX","XXO","OX "]
输出: "Pending"
解释: 没有玩家获胜且仍存在空位

提示:

  • 1 <= board.length == board[i].length <= 100
  • 输入一定遵循井字棋规则
class Solution {
public:
	string tictactoe(vector<string>& board) {
		if (sheng(board, 'X'))return "X";
		if (sheng(board, 'O'))return "O";
		int n = board.size();
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++)if (board[i][j] == ' ')return "Pending";
		}
		return "Draw";
	}
	bool sheng(vector<string>& board, char c) {
		int n = board.size();
		for (int i = 0; i < n; i++) {
			bool flag = true;
			for (int j = 0; j < n; j++)if (board[i][j] != c)flag = false;
			if (flag)return true;
		}
		for (int j = 0; j < n; j++) {
			bool flag = true;
			for (int i = 0; i < n; i++)if (board[i][j] != c)flag = false;
			if (flag)return true;
		}
		bool flag = true;
		for (int i = 0; i < n; i++)if (board[i][i] != c)flag = false;
		if (flag)return true;
		flag = true;
		for (int i = 0; i < n; i++)if (board[i][n-1-i] != c)flag = false;
		if (flag)return true;
		return false;
	}
};

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
无限套娃字典的键值是一个递归的数据结构,可以使用Java中的Map来实现。以下是一个示例代码,使用Map来表示一个无限套娃字典: ``` import java.util.HashMap; import java.util.Map; public class NestedDictionary { private Map<String, Object> dictionary = new HashMap<>(); public void setValue(String key, Object value) { String[] keys = key.split("\\."); Map<String, Object> currentDict = dictionary; for (int i = 0; i < keys.length - 1; i++) { String currentKey = keys[i]; if (!currentDict.containsKey(currentKey)) { currentDict.put(currentKey, new HashMap<String, Object>()); } currentDict = (Map<String, Object>) currentDict.get(currentKey); } currentDict.put(keys[keys.length - 1], value); } public Object getValue(String key) { String[] keys = key.split("\\."); Map<String, Object> currentDict = dictionary; for (int i = 0; i < keys.length - 1; i++) { String currentKey = keys[i]; if (!currentDict.containsKey(currentKey)) { return null; } currentDict = (Map<String, Object>) currentDict.get(currentKey); } return currentDict.get(keys[keys.length - 1]); } } ``` 这个示例代码中的`NestedDictionary`类实现了一个无限套娃字典。`setValue`方法用于在字典中设置一个键值对,`getValue`方法用于获取一个键对应的值。键可以是一个或多个用`.`分隔的字符串。 例如,可以使用以下代码来测试这个类: ``` NestedDictionary dict = new NestedDictionary(); dict.setValue("a.b.c", "value1"); dict.setValue("a.b.d", "value2"); dict.setValue("a.c.e.f", "value3"); System.out.println(dict.getValue("a.b.c")); // 输出"value1" System.out.println(dict.getValue("a.b.d")); // 输出"value2" System.out.println(dict.getValue("a.c.e.f")); // 输出"value3" System.out.println(dict.getValue("a.b")); // 输出null ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值