点和盒最快的播放器


16

面临的挑战是为经典的纸笔游戏“ 点和盒”编写求解器。您的代码应使用两个整数,m并将其n作为输入,指定板的大小。

从一个空的点网格开始,玩家轮流在两个未连接的相邻点之间添加一条水平或垂直线。一位完成1×1框第四边的玩家将获得一个积分,并转一圈。(通常通过将玩家的识别标记(例如首字母)放在框中来记录积分)。当无法放置更多行时,游戏结束。游戏的赢家是得分最高的玩家。

在此处输入图片说明

您可以假设n = mor n = m - 1m至少为2。

挑战在于solve在一分钟内实现最大的点和盒游戏。一个游戏的规模很简单n*m。您的代码输出应为windraw或者lose应该是假设两个玩家都发挥最佳状态的第一个玩家的结果。

您的代码必须可以使用易于安装和免费的工具在ubuntu上进行编译/运行。请报告您的分数,以及您在1分钟内可以在计算机上解决的最大问题。然后,我将在计算机上测试代码,并按顺序排列条目表。

在抢七的情况下,获胜者将是在一分钟内可以解决的最大尺寸板上最快的代码。


如果输出的代码不仅输赢,而且输出实际分数,那会更好。这样可以进行正确性检查。


2
我们必须使用minimax吗?
qwr

@qwr您能告诉我您还有什么其他选择吗?

等一下,这个游戏中有一个完全基于网格大小的可预测赢家吗?
并非查尔斯(Charles)

@Charles是的,如果两个玩家都发挥最佳状态。

1
@PeterTaylor我想你得到2分,但只有一转。

Answers:


15

C99-0.084秒内的3x3电路板

编辑:我重构了我的代码,并对结果进行了更深入的分析。

进一步编辑:添加了按对称修剪。这有4种算法配置:有或没有对称X,有或没有alpha-beta修剪

最深入的编辑:使用哈希表添加了备忘录,最终实现了不可能:解决3x3面板!

主要特点:

  • 通过alpha-beta修剪直接实现minimax
  • 很少的内存管理(维护有效移动的dll;树搜索中每个分支的O(1)更新)
  • 通过对称修剪的第二个文件。仍然可以实现每个分支的O(1)更新(技术上为O(S),其中S是对称数。对于方板是7,对于非方板是3)
  • 第三和第四个文件添加备注。您可以控制哈希表的大小(#define HASHTABLE_BITWIDTH)。当此大小大于或等于墙的数量时,它将保证不发生碰撞并且O(1)更新。较小的哈希表将具有更多的冲突,并且会稍微慢一些。
  • 编译-DDEBUG输出

潜在的改进:

  • 修复第一次编辑中修复的小内存泄漏
  • 在第二次编辑中添加了alpha / beta修剪
  • 在第三次编辑中添加了修剪对称性(请注意,对称性不是通过备注处理的,因此仍然是单独的优化。)
  • 备忘录已添加到第四编辑中
  • 当前备忘录在每个墙都使用一个指示符位。3x4电路板有31面墙,因此无论时间如何,此方法都无法处理4x4电路板。改进将是模拟X位整数,其中X至少等于墙的数量。

由于缺乏组织,文件数量已无法控制。所有代码已移至该Github存储库。在备忘录编辑中,我添加了一个makefile和测试脚本。

结果

执行时间的对数图

复杂性说明

暴力破解点和盒子的方法很快就变得复杂起来

考虑具有R行和C列的板。有R*C正方形,R*(C+1)垂直墙和C*(R+1)水平墙。总共是W = 2*R*C + R + C

由于Lembik要求我们使用minimax 解决游戏,因此我们需要遍历游戏树的叶子。现在让我们忽略修剪,因为重要的是数量级。

W第一步有一些选择。对于每一个,下一个玩家都可以玩W-1其余的墙壁,等等。这为我们提供了SS = W * (W-1) * (W-2) * ... * 1或的搜索空间SS = W!。阶乘非常庞大,但这仅仅是开始。SS是搜索空间中叶节点的数量。与我们的分析更相关的是必须做出的决策总数(即树中的分支B)。分支的第一层具有W选项。对于每一个,下一级别都有W-1,等等。

B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!

B = SUM W!/(W-k)!
  k=0..W-1

让我们看一些小表:

Board Size  Walls  Leaves (SS)      Branches (B)
---------------------------------------------------
1x1         04     24               64
1x2         07     5040             13699
2x2         12     479001600        1302061344
2x3         17     355687428096000  966858672404689

这些数字越来越荒谬。至少他们解释了为什么蛮力代码似乎永远挂在2x3板上。2x3板的搜索空间是2x2的742560倍。如果2x2需要20秒才能完成,则保守的推断会预测2x3的执行时间超过100天。显然,我们需要修剪。

修剪分析

我首先使用alpha-beta算法添加了非常简单的修剪功能。基本上,它停止搜索理想的对手是否永远不会给它当前的机会。“嗨,瞧瞧-如果我的对手让我赢得每个平方,我都会赢很多!”,从来没有AI认为。

编辑我还添加了基于对称板的修剪。我不使用记忆化方法,以防万一有一天我添加记忆化并想分开进行分析。相反,它的工作方式是这样的:大多数行在网格上的其他位置具有“对称对”。最多有7种对称性(水平,垂直,180旋转,90旋转,270旋转,对角线和另一个对角线)。所有7个均适用于正方形板,但最后四个不适用于非正方形板。对于每种对称,每面墙都有一个指向其“对”的指针。如果转弯时该板是水平对称的,则每个水平对中只有一个需要演奏。

编辑编辑备忘录!每堵墙都有一个唯一的ID,我可以方便地将其设置为指示器位;第n面墙有ID 1 << n。那么,棋盘的哈希值就是所有游戏墙的OR。这在O(1)时间在每个分支上更新。哈希表的大小在中设置#define。所有测试均以2 ^ 12的大小运行,因为为什么不呢?当墙数多于索引哈希表的位(在这种情况下为12位)时,最低有效位12被屏蔽并用作索引。冲突通过每个哈希表索引处的链接列表进行处理。下表是我对哈希表大小如何影响性能的快速分析。在具有无限RAM的计算机上,我们总是将桌子的大小设置为墙的数量。一个3x4的木板的哈希表长度为2 ^ 31。las,我们没有那么奢侈。

哈希表大小的影响

好,回到修剪状态。通过在树上停止搜索,我们可以节省大量时间,而不必走到树叶上。“修剪因子”是我们必须访问的所有可能分支的一部分。蛮力的修剪因子为1。值越小越好。

分支的对数图

修剪因子的对数图


对于像C这样的快速语言,23s似乎慢得多。您是否强行使用?
qwr 2014年

蛮力,从alpha beta进行少量修剪。我同意23s是可疑的,但是我在代码中看不到任何不一致的理由。换句话说,这是个谜
6

1
输入的格式如问题所指定。两个空格分隔的整数,rows columns指定木板的大小
–rongu

1
@Lembik我认为没有任何事情要做。我已经完成了这个疯狂的项目!
rightu 2014年

1
我认为您的答案值得特别注意。我查了一下,“ 3乘3”是有史以来最大的问题,您的代码几乎是即时的。如果您可以按4乘3或4乘4的方式进行求解,则可以将结果添加到Wiki页面并出名:)

4

Python-29秒内2x2

拼图交叉张贴。没有特别优化,但可能为其他进入者提供有用的起点。

from collections import defaultdict

VERTICAL, HORIZONTAL = 0, 1

#represents a single line segment that can be drawn on the board.
class Line(object):
    def __init__(self, x, y, orientation):
        self.x = x
        self.y = y
        self.orientation = orientation
    def __hash__(self):
        return hash((self.x, self.y, self.orientation))
    def __eq__(self, other):
        if not isinstance(other, Line): return False
        return self.x == other.x and self.y == other.y and self.orientation == other.orientation
    def __repr__(self):
        return "Line({}, {}, {})".format(self.x, self.y, "HORIZONTAL" if self.orientation == HORIZONTAL else "VERTICAL")

class State(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.whose_turn = 0
        self.scores = {0:0, 1:0}
        self.lines = set()
    def copy(self):
        ret = State(self.width, self.height)
        ret.whose_turn = self.whose_turn
        ret.scores = self.scores.copy()
        ret.lines = self.lines.copy()
        return ret
    #iterate through all lines that can be placed on a blank board.
    def iter_all_lines(self):
        #horizontal lines
        for x in range(self.width):
            for y in range(self.height+1):
                yield Line(x, y, HORIZONTAL)
        #vertical lines
        for x in range(self.width+1):
            for y in range(self.height):
                yield Line(x, y, VERTICAL)
    #iterate through all lines that can be placed on this board, 
    #that haven't already been placed.
    def iter_available_lines(self):
        for line in self.iter_all_lines():
            if line not in self.lines:
                yield line

    #returns the number of points that would be earned by a player placing the line.
    def value(self, line):
        assert line not in self.lines
        all_placed = lambda seq: all(l in self.lines for l in seq)
        if line.orientation == HORIZONTAL:
            #lines composing the box above the line
            lines_above = [
                Line(line.x,   line.y+1, HORIZONTAL), #top
                Line(line.x,   line.y,   VERTICAL),   #left
                Line(line.x+1, line.y,   VERTICAL),   #right
            ]
            #lines composing the box below the line
            lines_below = [
                Line(line.x,   line.y-1, HORIZONTAL), #bottom
                Line(line.x,   line.y-1, VERTICAL),   #left
                Line(line.x+1, line.y-1, VERTICAL),   #right
            ]
            return all_placed(lines_above) + all_placed(lines_below)
        else:
            #lines composing the box to the left of the line
            lines_left = [
                Line(line.x-1, line.y+1, HORIZONTAL), #top
                Line(line.x-1, line.y,   HORIZONTAL), #bottom
                Line(line.x-1, line.y,   VERTICAL),   #left
            ]
            #lines composing the box to the right of the line
            lines_right = [
                Line(line.x,   line.y+1, HORIZONTAL), #top
                Line(line.x,   line.y,   HORIZONTAL), #bottom
                Line(line.x+1, line.y,   VERTICAL),   #right
            ]
            return all_placed(lines_left) + all_placed(lines_right)

    def is_game_over(self):
        #the game is over when no more moves can be made.
        return len(list(self.iter_available_lines())) == 0

    #iterates through all possible moves the current player could make.
    #Because scoring a point lets a player go again, a move can consist of a collection of multiple lines.
    def possible_moves(self):
        for line in self.iter_available_lines():
            if self.value(line) > 0:
                #this line would give us an extra turn.
                #so we create a hypothetical future state with this line already placed, and see what other moves can be made.
                future = self.copy()
                future.lines.add(line)
                if future.is_game_over(): 
                    yield [line]
                else:
                    for future_move in future.possible_moves():
                        yield [line] + future_move
            else:
                yield [line]

    def make_move(self, move):
        for line in move:
            self.scores[self.whose_turn] += self.value(line)
            self.lines.add(line)
        self.whose_turn = 1 - self.whose_turn

    def tuple(self):
        return (tuple(self.lines), tuple(self.scores.items()), self.whose_turn)
    def __hash__(self):
        return hash(self.tuple())
    def __eq__(self, other):
        if not isinstance(other, State): return False
        return self.tuple() == other.tuple()

#function decorator which memorizes previously calculated values.
def memoized(fn):
    answers = {}
    def mem_fn(*args):
        if args not in answers:
            answers[args] = fn(*args)
        return answers[args]
    return mem_fn

#finds the best possible move for the current player.
#returns a (move, value) tuple.
@memoized
def get_best_move(state):
    cur_player = state.whose_turn
    next_player = 1 - state.whose_turn
    if state.is_game_over():
        return (None, state.scores[cur_player] - state.scores[next_player])
    best_move = None
    best_score = float("inf")
    #choose the move that gives our opponent the lowest score
    for move in state.possible_moves():
        future = state.copy()
        future.make_move(move)
        _, score = get_best_move(future)
        if score < best_score:
            best_move = move
            best_score = score
    return [best_move, -best_score]

n = 2
m = 2
s = State(n,m)
best_move, relative_value = get_best_move(s)
if relative_value > 0:
    print("win")
elif relative_value == 0:
    print("draw")
else:
    print("lose")

使用pypy最多可以加快18秒。

2

Javascript-1x2电路板,持续20ms

此处提供在线演示(警告- 如果大于1x2且具有完整的搜索深度,将非常慢):https : //dl.dropboxusercontent.com/u/141246873/minimax/index.html

是为原始的获胜标准(高尔夫代码)开发的,而不是针对速度的。

已在Windows 7的google chrome v35中测试。

//first row is a horizontal edges and second is vertical
var gameEdges = [
    [false, false],
    [false, false, false],
    [false, false]
]

//track all possible moves and score outcome
var moves = []

function minimax(edges, isPlayersTurn, prevScore, depth) {

    if (depth <= 0) {
        return [prevScore, 0, 0];
    }
    else {

        var pointValue = 1;
        if (!isPlayersTurn)
            pointValue = -1;

        var moves = [];

        //get all possible moves and scores
        for (var i in edges) {
            for (var j in edges[i]) {
                //if edge is available then its a possible move
                if (!edges[i][j]) {

                    //if it would result in game over, add it to the scores array, otherwise, try the next move
                    //clone the array
                    var newEdges = [];
                    for (var k in edges)
                        newEdges.push(edges[k].slice(0));
                    //update state
                    newEdges[i][j] = true;
                    //if closing this edge would result in a complete square, get another move and get a point
                    //square could be formed above, below, right or left and could get two squares at the same time

                    var currentScore = prevScore;
                    //vertical edge
                    if (i % 2 !== 0) {//i === 1
                        if (newEdges[i] && newEdges[i][j - 1] && newEdges[i - 1] && newEdges[i - 1][j - 1] && newEdges[parseInt(i) + 1] && newEdges[parseInt(i) + 1][j - 1])
                            currentScore += pointValue;
                        if (newEdges[i] && newEdges[i][parseInt(j) + 1] && newEdges[i - 1] && newEdges[i - 1][j] && newEdges[parseInt(i) + 1] && newEdges[parseInt(i) + 1][j])
                            currentScore += pointValue;
                    } else {//horizontal
                        if (newEdges[i - 2] && newEdges[i - 2][j] && newEdges[i - 1][j] && newEdges[i - 1][parseInt(j) + 1])
                            currentScore += pointValue;
                        if (newEdges[parseInt(i) + 2] && newEdges[parseInt(i) + 2][j] && newEdges[parseInt(i) + 1][j] && newEdges[parseInt(i) + 1][parseInt(j) + 1])
                            currentScore += pointValue;
                    }

                    //leaf case - if all edges are taken then there are no more moves to evaluate
                    if (newEdges.every(function (arr) { return arr.every(Boolean) })) {
                        moves.push([currentScore, i, j]);
                        console.log("reached end case with possible score of " + currentScore);
                    }
                    else {
                        if ((isPlayersTurn && currentScore > prevScore) || (!isPlayersTurn && currentScore < prevScore)) {
                            //gained a point so get another turn
                            var newMove = minimax(newEdges, isPlayersTurn, currentScore, depth - 1);

                            moves.push([newMove[0], i, j]);
                        } else {
                            //didnt gain a point - opponents turn
                            var newMove = minimax(newEdges, !isPlayersTurn, currentScore, depth - 1);

                            moves.push([newMove[0], i, j]);
                        }
                    }



                }


            }

        }//end for each move

        var bestMove = moves[0];
        if (isPlayersTurn) {
            for (var i in moves) {
                if (moves[i][0] > bestMove[0])
                    bestMove = moves[i];
            }
        }
        else {
            for (var i in moves) {
                if (moves[i][0] < bestMove[0])
                    bestMove = moves[i];
            }
        }
        return bestMove;
    }
}

var player1Turn = true;
var squares = [[0,0],[0,0]]//change to "A" or "B" if square won by any of the players
var lastMove = null;

function output(text) {
    document.getElementById("content").innerHTML += text;
}

function clear() {
    document.getElementById("content").innerHTML = "";
}

function render() {
    var width = 3;
    if (document.getElementById('txtWidth').value)
        width = parseInt(document.getElementById('txtWidth').value);
    if (width < 2)
        width = 2;

    clear();
    //need to highlight the last move taken and show who has won each square
    for (var i in gameEdges) {
        for (var j in gameEdges[i]) {
            if (i % 2 === 0) {
                if(j === "0")
                    output("*");
                if (gameEdges[i][j] && lastMove[1] == i && lastMove[2] == j)
                    output(" <b>-</b> ");
                else if (gameEdges[i][j])
                    output(" - ");
                else
                    output("&nbsp;&nbsp;&nbsp;");
                output("*");
            }
            else {
                if (gameEdges[i][j] && lastMove[1] == i && lastMove[2] == j)
                    output("<b>|</b>");
                else if (gameEdges[i][j])
                    output("|");
                else
                    output("&nbsp;");

                if (j <= width - 2) {
                    if (squares[Math.floor(i / 2)][j] === 0)
                        output("&nbsp;&nbsp;&nbsp;&nbsp;");
                    else
                        output("&nbsp;" + squares[Math.floor(i / 2)][j] + "&nbsp;");
                }
            }
        }
        output("<br />");

    }
}

function nextMove(playFullGame) {
    var startTime = new Date().getTime();
    if (!gameEdges.every(function (arr) { return arr.every(Boolean) })) {

        var depth = 100;
        if (document.getElementById('txtDepth').value)
            depth = parseInt(document.getElementById('txtDepth').value);

        if (depth < 1)
            depth = 1;

        var move = minimax(gameEdges, true, 0, depth);
        gameEdges[move[1]][move[2]] = true;
        lastMove = move;

        //if a square was taken, need to update squares and whose turn it is

        var i = move[1];
        var j = move[2];
        var wonSquare = false;
        if (i % 2 !== 0) {//i === 1
            if (gameEdges[i] && gameEdges[i][j - 1] && gameEdges[i - 1] && gameEdges[i - 1][j - 1] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j - 1]) {
                squares[Math.floor(i / 2)][j - 1] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
            if (gameEdges[i] && gameEdges[i][parseInt(j) + 1] && gameEdges[i - 1] && gameEdges[i - 1][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j]) {
                squares[Math.floor(i / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
        } else {//horizontal
            if (gameEdges[i - 2] && gameEdges[i - 2][j] && gameEdges[i - 1] && gameEdges[i - 1][j] && gameEdges[i - 1] && gameEdges[i - 1][parseInt(j) + 1]) {
                squares[Math.floor((i - 1) / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
            if (gameEdges[i + 2] && gameEdges[parseInt(i) + 2][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][parseInt(j) + 1]) {
                squares[Math.floor(i / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
        }

        //didnt win a square so its the next players turn
        if (!wonSquare)
            player1Turn = !player1Turn;

        render();

        if (playFullGame) {
            nextMove(playFullGame);
        }
    }

    var endTime = new Date().getTime();
    var executionTime = endTime - startTime;
    document.getElementById("executionTime").innerHTML = 'Execution time: ' + executionTime;
}

function initGame() {

    var width = 3;
    var height = 2;

    if (document.getElementById('txtWidth').value)
        width = document.getElementById('txtWidth').value;
    if (document.getElementById('txtHeight').value)
        height = document.getElementById('txtHeight').value;

    if (width < 2)
        width = 2;
    if (height < 2)
        height = 2;

    var depth = 100;
    if (document.getElementById('txtDepth').value)
        depth = parseInt(document.getElementById('txtDepth').value);

    if (depth < 1)
        depth = 1;

    if (width > 2 && height > 2 && !document.getElementById('txtDepth').value)
        alert("Warning. Your system may become unresponsive. A smaller grid or search depth is highly recommended.");

    gameEdges = [];
    for (var i = 0; i < height; i++) {
        if (i == 0) {
            gameEdges.push([]);
            for (var j = 0; j < (width - 1) ; j++) {
                gameEdges[i].push(false);
            }
        }
        else {
            gameEdges.push([]);
            for (var j = 0; j < width; j++) {
                gameEdges[(i * 2) - 1].push(false);
            }
            gameEdges.push([]);
            for (var j = 0; j < (width - 1) ; j++) {
                gameEdges[i*2].push(false);
            }
        }
    }

    player1Turn = true;

    squares = [];
    for (var i = 0; i < (height - 1) ; i++) {
        squares.push([]);
        for (var j = 0; j < (width - 1); j++) {
            squares[i].push(0);
        }
    }

    lastMove = null;

    render();
}

document.addEventListener('DOMContentLoaded', initGame, false);

该演示真的很棒!3 x 3真的很有趣,因为获胜者会随着您增加搜索深度而来回变化。我可以检查一下,您的minimax会在转弯中途停止吗?我的意思是,如果有人得到一个方块,它会一直延伸到回合结束吗?

2x2是3乘3的点。确定代码可以在20ms内完全解决吗?

“如果有人得到一个方块,它会一直延伸到回合结束吗?” -如果玩家得到一个方块,它仍然会移至下一回合,但下一回合是针对同一位玩家的,即他们会获得一个额外的回合以完成一个方块。“ 2x2是3点乘3点”-糟糕。在那种情况下,我的分数是1x1。
rdans 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.