棋串的数据结构
Monte Carlo局面评估+UCT博弈树搜索是现代计算机围棋的主流。这种方法对算法的效率有很高要求,因此须要在Mone Carlo模拟过程中保存棋串(直线相连的同色棋子集合)信息,提高落子效率。
棋串须要满足以下需求:
- 频繁查询某棋子所在的“棋串”。
- 频繁创建棋串。
- 频繁查询棋串的“气”。
- 频繁合并两棋串。
- 移除棋串,并枚举其中的棋子。
因此用链表实现的并查集是很合适的数据结构:
struct Node { PointIndex next_, list_head_; }; struct List { PointIndex tail_, len_; AirSet air_set_; }; Node nodes_[FOO_SQUARE(BOARD_LEN)]; List lists_[FOO_SQUARE(BOARD_LEN)];
创建只有一颗棋子的棋串:
template <BoardLen BOARD_LEN> void ChainSet<BOARD_LEN>::CreateList(PointIndex node_i, const ChainSet<BOARD_LEN>::AirSet &air_set) { nodes_[node_i].list_head_ = node_i; lists_[node_i].tail_ = node_i; lists_[node_i].len_ = 1; lists_[node_i].air_set_ = air_set; }
查询棋子所在棋串:
PointIndex GetListHead(PointIndex node_i) const { return nodes_[node_i].list_head_; }
合并两棋串:
template <BoardLen BOARD_LEN> PointIndex ChainSet<BOARD_LEN>::MergeLists(PointIndex list_a, PointIndex list_b) { FOO_ASSERT(list_a != list_b); if (lists_[list_a].len_ > lists_[list_b].len_) swap(list_a, list_b); for (int i=list_a; ; i=nodes_[i].next_) { nodes_[i].list_head_ = list_b; if (i == lists_[list_a].tail_) break; } nodes_[lists_[list_b].tail_].next_ = list_a; lists_[list_b].tail_ = lists_[list_a].tail_; lists_[list_b].len_ += lists_[list_a].len_; lists_[list_b].air_set_ |= lists_[list_a].air_set_; return list_b; }
移除棋串:
template <BoardLen BOARD_LEN> void ChainSet<BOARD_LEN>::RemoveList(PointIndex head) { for (int i=head; ; i=nodes_[i].next_) { nodes_[i].list_head_ = ChainSet<BOARD_LEN>::NONE_LIST; if (i == lists_[head].tail_) break; } }
枚举棋子:
template <BoardLen BOARD_LEN> PntIndxVector ChainSet<BOARD_LEN>::GetPiecesOfAChain(PointIndex list_i) const { const List *pl = lists_ + list_i; PntIndxVector v(pl->len_); int vi = 0; for (int i=list_i; ; i=nodes_[i].next_) { v[vi++] = i; if (i == pl->tail_) break; } return v; }
棋串的“气”可以按位存储,这样既省内存,又增效率。比如,“气”的数量,就是2进制中1的数量;合并两棋串时也须要合并它们的“气”,则作位或运算。这个数据结构不必自己实现:
typedef std::bitset<FOO_SQUARE(BOARD_LEN)> AirSet;
当然还须要写一些高层接口供落子函数调用。
贴张落子函数的test case:
代码:https://github.com/chncwang/FooGo