五子棋(二)极大极小搜索 + 剪枝
2016-05-15 20:44
295 查看
五子棋(一)
如果在落子的时候往后推算几步,可以大概估算一下复杂度。如果往后推算 4 步走法,即给自己和对方各推算两步,每步计算 50 个可落子点;那总共需要计算 50^4 = 6,250,000 个结点;这是一颗非常巨大的博弈树;程序的实际情况是每秒大约算 10000 个结点(有点慢~),那将需要 600 多秒才可确定下一步落子,这样的结果是不可接受的。还有一点就是,推算的层数最好是偶数,因为如果是奇数的话,最后一步并没有考虑到对方落子,防御力会降低。
在这颗博弈树中用极大极小的方法去计算计算机的下一步落子,可以使计算机为 MAX 层,玩家为 MIN 层。我们可以使用 alpha-beta 剪枝来实现搜索的优化。(博弈基础)
在源代码的 computer.c -> alpha_beta()
具体的实现细节:
之前说统计棋形的时候把二维转成一维再进行计算,这种做法在以上的搜索中表现得并不是很好;每预测落一子都要把该点横竖撇捺 4 个方向上的棋子转成一维,而且每次达到搜索深度都要将整个局面 70 多路棋子转换为一维;在效率上造成很大影响。因此,需要一种更优秀的方案:
可以用一个全局数组 points[MAPSIZE][MAPSIZE][2] 表示整个搜索过程中可落子点的得分情况;可令 points[y][x][0] 表示在点 (y, x) 处下黑棋时,黑棋可得到的分数;points[y][x][1] 表示在点 (y, x) 处下白棋时,白棋可得到的分数。拿前面的例子来说就是:
攻击力:冲四 + 2 * 连活二 = 300 + 2 * 100 = 500
防御力:连活三 = 450
该点价值:500 + 450 = 950
设该点为 (y, x) (第 y 行,第 x 列)
point[y][x][0] = 500
point[y][x][1] =
450
然后,不把二维转换成一维,那怎样统计棋形?如下图所选点为 (y, x),并假设在该点落黑棋
可以用 4 个数组 lchess[2],rchess[2],lempty[2],rempty[2] 来记录在一个方向上的情况,初始值为 0;
无论哪个方向,碰到对手棋都要立刻停止
比如横向:
lchess[0] 表示 (y, x) 左边的且与 (y, x) 相连的连续同类棋数目,这里 lchess[0] = 0;
lempty[0] 表示从 (y, x) 左边第一个空点数起的连续空点数目,这里 lempty[0] = 1;
lchess[1] 表示 (y, x) 左边的且与 (y, x) 至少隔一个空点的连续同类棋数目,这里 lchess[1] = 1;
lempty[1] 表示在 lchess[1] 个同类棋左边的连续空点数目,这里 lempty[1] = 4;
右边做类似处理得:rchess[0] = 1, rempty[0] = 2, rchess[1] = 0(到墙了), rempty[1] = 0(到墙了)
这样就可以根据这些值判断棋型了;这个例子横向棋型为 xxoxooxx ,是活三
1)lchess[0]+1+rchess[0] = 2,加 1 表示 (y, x) 自身;
2)往左一个空点,即 lempty[0] = 1;
3)再往左一个同类棋,即 lchess[1] = 1;
4)整个 oxoo 两边至少各有一个空点(xoxoox ),即 lempty[1] >= 1 && rempty[0] >= 1;
竖向类似,可设下为左,上为右,得:
lchess[0] = 0, lempty[0] = 1, lchess[1] = 0, lempty[1] = 0
rchess[0] = 3, rempty[0] = 0(碰到对手棋,后面类似), rchess[1] = 0, rempty[1] = 0
类似分析可得冲四棋型
判断完横竖撇捺 4 个方向上的棋型就可以打分了。
points[][][2] 的计算在源代码的 computer.c -> cal_point() -> get_points() -> cal_chess()
这样,在搜索过程中在某点落子时,发生改变的 points[][][] 里的值就在该点横竖撇捺 4 个方向上的值,仅改变这 4 个方向上的可落子点的值就可以了,回退时再改回来。
还有一点重要的优化是:在选择落子的时候,不必所有的可行点都去尝试一下,比如大部分落子是在棋盘中心,你没理由在棋盘的某个角落里下子;这些点就是可以排除的。这样,在计算所有可落子点的得分后,对得分由高往低排序,取前面少许点来试探就可以了,得分太低的不需要理会,因为就算考虑它,在搜索的时候仍然会被排除掉;吃力不讨好,还影响了搜索速度。当然也不能只取几个~,取四五十个就差不多了,具体还是需要根据测试确定。
另一个重要的优化是:有些棋型的出现是可以直接判断胜负的,不需要再往下搜索;前面也讲过。
如果落一子
可以成五,则必胜;
可以成活四,双冲四,冲四活三这样的的绝杀棋,且对手不能一步成五,则必胜;
可以成双活三,且对手不能一步成五,也不能一步成绝杀棋,则必胜
以上优化都加入后
伪代码为:
做到以上所说,进行 6 层搜索,每层搜索 20 个结点,可以把 20^6 = 64,000,000 个结点缩到 100000 以内,可以在几秒内落子;而且随着局势的明朗,会更快找到解答(其实就是绝杀棋的作用)
源代码的 fun.c -> go_chose 中的 computer_go() 与 test() 分别表示多层搜索与单层贪心搜索。
地址:https://github.com/QYPan/gobang2
文件:gobang22
把界面改成纯 ASCII 字符了~
下一篇将加入置换表,进一步提高性能。
如果在落子的时候往后推算几步,可以大概估算一下复杂度。如果往后推算 4 步走法,即给自己和对方各推算两步,每步计算 50 个可落子点;那总共需要计算 50^4 = 6,250,000 个结点;这是一颗非常巨大的博弈树;程序的实际情况是每秒大约算 10000 个结点(有点慢~),那将需要 600 多秒才可确定下一步落子,这样的结果是不可接受的。还有一点就是,推算的层数最好是偶数,因为如果是奇数的话,最后一步并没有考虑到对方落子,防御力会降低。
在这颗博弈树中用极大极小的方法去计算计算机的下一步落子,可以使计算机为 MAX 层,玩家为 MIN 层。我们可以使用 alpha-beta 剪枝来实现搜索的优化。(博弈基础)
在源代码的 computer.c -> alpha_beta()
// 初始值 player = 0, depth = 0, alpha = -inf, beta = inf alpha_beta(player, depth, alpha, beta) begin if 成五 then 返回结果(inf or -inf) endif if depth >= depth_limit then 返回估值 endif if(!player) then // 计算机 if !depth then // 在搜索入口层 // 取落子坐标 y = p[0].y x = p[0].x endif n = cal_points(p[]) // 计算所有可落子点的得分并记录在数组 p[] for i = 0; i < n; i++ set_in(p[i].y, p[i].x) // 落子 val = alpha_beta(player^1, depth+1, alpha, beta) if val > alpha then alpha = val if !depth then // 更新结果 y = sp[i].y x = sp[i].x endif endif if alpha >= beta then // 剪枝,等号不可去掉 return alpha endif set_out(p[i].y, p[i].x) // 回退 end return alpha else if(player) // 计算机 n = cal_points(p[]) // 计算所有可落子点的得分并记录在数组 p[] for i = 0; i < n; i++ set_in(p[i].y, p[i].x) // 落子 val = alpha_beta(player^1, depth+1, alpha, beta) if val < beta then beta = val endif if alpha >= beta then // 剪枝,等号不可去掉 return beta endif set_out(p[i].y, p[i].x) // 回退 end return beta endif end
具体的实现细节:
之前说统计棋形的时候把二维转成一维再进行计算,这种做法在以上的搜索中表现得并不是很好;每预测落一子都要把该点横竖撇捺 4 个方向上的棋子转成一维,而且每次达到搜索深度都要将整个局面 70 多路棋子转换为一维;在效率上造成很大影响。因此,需要一种更优秀的方案:
可以用一个全局数组 points[MAPSIZE][MAPSIZE][2] 表示整个搜索过程中可落子点的得分情况;可令 points[y][x][0] 表示在点 (y, x) 处下黑棋时,黑棋可得到的分数;points[y][x][1] 表示在点 (y, x) 处下白棋时,白棋可得到的分数。拿前面的例子来说就是:
攻击力:冲四 + 2 * 连活二 = 300 + 2 * 100 = 500
防御力:连活三 = 450
该点价值:500 + 450 = 950
设该点为 (y, x) (第 y 行,第 x 列)
point[y][x][0] = 500
point[y][x][1] =
450
然后,不把二维转换成一维,那怎样统计棋形?如下图所选点为 (y, x),并假设在该点落黑棋
可以用 4 个数组 lchess[2],rchess[2],lempty[2],rempty[2] 来记录在一个方向上的情况,初始值为 0;
无论哪个方向,碰到对手棋都要立刻停止
比如横向:
lchess[0] 表示 (y, x) 左边的且与 (y, x) 相连的连续同类棋数目,这里 lchess[0] = 0;
lempty[0] 表示从 (y, x) 左边第一个空点数起的连续空点数目,这里 lempty[0] = 1;
lchess[1] 表示 (y, x) 左边的且与 (y, x) 至少隔一个空点的连续同类棋数目,这里 lchess[1] = 1;
lempty[1] 表示在 lchess[1] 个同类棋左边的连续空点数目,这里 lempty[1] = 4;
右边做类似处理得:rchess[0] = 1, rempty[0] = 2, rchess[1] = 0(到墙了), rempty[1] = 0(到墙了)
这样就可以根据这些值判断棋型了;这个例子横向棋型为 xxoxooxx ,是活三
1)lchess[0]+1+rchess[0] = 2,加 1 表示 (y, x) 自身;
2)往左一个空点,即 lempty[0] = 1;
3)再往左一个同类棋,即 lchess[1] = 1;
4)整个 oxoo 两边至少各有一个空点(xoxoox ),即 lempty[1] >= 1 && rempty[0] >= 1;
竖向类似,可设下为左,上为右,得:
lchess[0] = 0, lempty[0] = 1, lchess[1] = 0, lempty[1] = 0
rchess[0] = 3, rempty[0] = 0(碰到对手棋,后面类似), rchess[1] = 0, rempty[1] = 0
类似分析可得冲四棋型
判断完横竖撇捺 4 个方向上的棋型就可以打分了。
points[][][2] 的计算在源代码的 computer.c -> cal_point() -> get_points() -> cal_chess()
这样,在搜索过程中在某点落子时,发生改变的 points[][][] 里的值就在该点横竖撇捺 4 个方向上的值,仅改变这 4 个方向上的可落子点的值就可以了,回退时再改回来。
还有一点重要的优化是:在选择落子的时候,不必所有的可行点都去尝试一下,比如大部分落子是在棋盘中心,你没理由在棋盘的某个角落里下子;这些点就是可以排除的。这样,在计算所有可落子点的得分后,对得分由高往低排序,取前面少许点来试探就可以了,得分太低的不需要理会,因为就算考虑它,在搜索的时候仍然会被排除掉;吃力不讨好,还影响了搜索速度。当然也不能只取几个~,取四五十个就差不多了,具体还是需要根据测试确定。
另一个重要的优化是:有些棋型的出现是可以直接判断胜负的,不需要再往下搜索;前面也讲过。
如果落一子
可以成五,则必胜;
可以成活四,双冲四,冲四活三这样的的绝杀棋,且对手不能一步成五,则必胜;
可以成双活三,且对手不能一步成五,也不能一步成绝杀棋,则必胜
以上优化都加入后
伪代码为:
cal_points() // 计算所有可落子点的得分 // 初始值 player = 0, depth = 0, alpha = -inf, beta = inf alpha_beta(player, depth, alpha, beta) begin if 成五 then 返回结果(inf or -inf) endif if depth >= depth_limit then 返回估值 endif if(!player) then // 计算机 if !depth then // 在搜索入口层 // 取落子坐标 y = p[0].y x = p[0].x endif n = set_order(p[]) //对所有可落子点排序并记录在数组 p[] for i = 0; i < n && i < num_limit; i++ // 最多计算 num_limit 个点 if kill() then // 绝杀棋判断 if !depth then // 更新结果 y = sp[i].y x = sp[i].x endif alpha = inf; break; endif set_in(p[i].y, p[i].x) // 落子 val = alpha_beta(player^1, depth+1, alpha, beta) if val > alpha then alpha = val if !depth then // 更新结果 y = sp[i].y x = sp[i].x endif endif if alpha >= beta then // 剪枝,等号不可去掉 return alpha endif set_out(p[i].y, p[i].x) // 回退 end return alpha else if(player) // 计算机 n = set_order(p[]) //对所有可落子点排序并记录在数组 p[] for i = 0; i < n && i < num_limit; i++ // 最多计算 num_limit 个点 if kill() then // 绝杀棋判断 beta = -inf; break; endif set_in(p[i].y, p[i].x) // 落子 val = alpha_beta(player^1, depth+1, alpha, beta) if val < beta then beta = val endif if alpha >= beta then // 剪枝,等号不可去掉 return beta endif set_out(p[i].y, p[i].x) // 回退 end return beta endif end
做到以上所说,进行 6 层搜索,每层搜索 20 个结点,可以把 20^6 = 64,000,000 个结点缩到 100000 以内,可以在几秒内落子;而且随着局势的明朗,会更快找到解答(其实就是绝杀棋的作用)
源代码的 fun.c -> go_chose 中的 computer_go() 与 test() 分别表示多层搜索与单层贪心搜索。
地址:https://github.com/QYPan/gobang2
文件:gobang22
把界面改成纯 ASCII 字符了~
下一篇将加入置换表,进一步提高性能。
相关文章推荐
- 团队项目
- 中国计算机学会推荐国际学术会议和期刊目录
- java/android 设计模式学习笔记(2)---观察者模式
- 认识代理服务器
- CadenceIC5141安装总结
- 数据结构-平衡二叉树(AVL Tree)
- 利用Graphviz画出图
- IDEA下JNI开发快速生成头文件方法
- 跟着郝斌学数据结构(06)——队列(链式队列)
- manacher算法
- glog使用与功能修改
- 用Xstream解析XML
- LintCode : Unique Paths
- Azure ServiceBus 架构简介
- 引用与指针
- HDU 1498 50 years, 50 colors(最小顶点覆盖)
- Fragment事务及Fragment实现选项卡功能
- 顺序表应用6:有序顺序表查询
- 从头认识多线程-2.19 synchronized同步方法的无限等待与解决方法
- 深度学习模型之各种caffe版本(Linux和windows)的网址