与迷宫有关的游戏

时间:2019-05-08 09:31:50

标签: algorithm dynamic game-theory

A和B玩游戏如下:

  

对于由矩阵表示的迷宫,其大小为 n × n ,则单元格包含字符“”。表示可移动区域,单元格包含字符“#”。字符表示无法踩到的障碍物。从单元格( n n )开始,每个人轮流选择要移动的下一个位置。新位置必须在当前单元格的左侧或上方(作为国际象棋中的车子移动)。当前位置和新位置之间没有障碍。如果找不到新职位,该人将失去。 A是第一个走的人。确定谁是赢家。

例如:

  • n n = 2且迷宫:
 . .
 . .

结果为B,因为最初在坐标(2,2)处,A将向左或向上移动,因此将有坐标(1,2)或(2,1)。然后B将移动到坐标(1,1)。 A不能再移动了,所以他输了,B赢了。

  • 使用 n = 5并迷宫:
. . # . .
# . . . #
. . # . .
# . . . .
. . . . .

类似地解释,我们有A将成为赢家。

这是我的尝试:我尝试使用递归编程来确定谁是赢家,但是当 n 很大时,它花费的时间太长,为此我尝试构建动态编程。问题。

编辑:

  

所以,这是我们如何确定谁是这场比赛的胜利者的主要问题。假设最初在坐标( n n )处有一块石头。 A和B轮流玩游戏,如下所示:A将选择一个新位置来移动该石头(我们可以想象该石头像国际象棋中的车子一样),这个新位置必须在当前单元格的左侧或上方,之后,B也选择一个新位置来移动此石头。直到无法移动这块石头的人成为失败者。

请注意:字符“。”代表可移动土地,而字符“#”代表障碍!

发布此问题的目的是,我想尝试动态编程或递归以确定谁是这场比赛的赢家。

2 个答案:

答案 0 :(得分:2)

我们可以将矩阵中的坐标分类为获胜(即,在正确比赛中获胜的一方获胜)或失败(即在正确比赛中获胜的一方获胜)

对应于可移动土地的坐标(r,c)

  • 如果没有法律动议就会失败;
  • 如果所有合法举动都转到获胜坐标,则失败;
  • 如果至少有一项法律举措导致输掉坐标,则获胜。

请注意,根据第一个规则,(1,1)输了,因此,根据最后一个规则,可以将石头移至(1,1)的任何人都将获胜。

第二个矩阵的分类是:

L W # L W
# L W W #
L W # W L
# W L W W
L W W W W

由于坐标的值仅取决于左右的值,因此我们可以按从上到下,从左到右的顺序计算值。您实际上不需要递归或动态编程。像

for r in 1...n
  for c in 1...n
    look left and up until the edge of the board or the first # sign
    if there are any L values, then mark (r,c) as winning
    otherwise mark (r,c) as losing

这种幼稚的方法每个坐标花费O(n)时间,因此总时间为O(n 3 )。可以通过在扫描矩阵时保留一些布尔标志来将其改进为O(n 2 ):

  • 一个行标志,指示我们是否可以从当前位置向左移动到丢失位置。值L会将标志设置为true,而#号会将其重置为false。
  • N 列标志,其中 f i 表示我们是否可以将 i 列向上移动到失去位置。每个标志都如上所述更改值。

答案 1 :(得分:2)

好吧,您只需申请Sprague-Grundy theorem即可找出获胜者。

因此计算粗略数字将是这样的:

  1. 将损失肯定的单元格标记为0(不能向上或向左移动的单元格)
0 . # 0 .
# . . . #
0 . # . .
# . . . .
0 . . . .
  1. 遍历剩余的单元格,并为每个未知单元格(标记为.)一举找到所有可到达的单元格
  2. 现在MEX个单元格将是未知单元格的值
  3. 填充所有单元格后,我们将得到以下内容:
0 1 # 0 1
# 0 1 2 #
0 2 # 1 0
# 3 0 4 1
0 4 1 3 2
  1. 因此,如果起始单元(n,n)不为0,则玩家将获胜

示例代码(C ++),O(n ^ 3):

#include <bits/stdc++.h>
using namespace std;

int main()
{
    vector<string>A = {
                "..#..",
                "#...#",
                "..#..",
                "#....",
                "....."};

    int n = A.size();
    int Grundy[n][n]={};

    for(int i=0; i<n; i++)
    for(int j=0; j<n; j++)
        if(A[i][j]!='#')
        {
            int can[2*n+1]={};

            int left = j-1;
            while(left>=0 && A[i][left]!='#')
            {
                can[Grundy[i][left]]++;
                left--;
            }

            int up = i-1;
            while(up>=0 && A[up][j]!='#')
            {
                can[Grundy[up][j]]++;
                up--;
            }

            while(can[Grundy[i][j]])Grundy[i][j]++;
        }

    cout<<(Grundy[n-1][n-1] ? "Player 1 wins\n" : "Player 2 wins\n");
}

这将产生O(n ^ 3)解,尽管我们仍然可以按以下方式优化为O(n ^ 2):

  1. 因为我们一次只玩一个游戏板,所以我们真的不需要所有那些脏乱的数字,这就足以知道当前单元格中是否有可用的脏乱数字0
  2. 让我们称这些值为0的丢失单元格,这样,当且仅当我们可以移动到某个丢失的单元格时,我们才能从(i,j)单元中获胜。
  3. 如果我们为此使用for / while循环,则搜索可到达的单元格仍将导致O(n ^ 3)
  4. 要获得O(n ^ 2),我们需要对行和列使用前缀数组。
  5. 所以win[i][j]-存储是否可以从(i,j)单元中获胜
  6. loseRow[i][j]-存储第i行中是否有可以从像元(i,j)到达的丢失像元
  7. loseCol[i][j]-存储是否可以从单元格(i,j)到达列j中是否有丢失的单元格

示例代码(C ++),O(n ^ 2):

#include <bits/stdc++.h>
using namespace std;

int main()
{
    vector<string>A = {
                "..#..",
                "#...#",
                "..#..",
                "#....",
                "....."};

    int n = A.size();
    int win[n][n]={};
    int loseRow[n][n]={};
    int loseCol[n][n]={};

    for(int i=0; i<n; i++)
    for(int j=0; j<n; j++)
        if(A[i][j]!='#')
        {
            if(j-1>=0 && A[i][j-1]!='#')
                {
                    win[i][j]|=loseRow[i][j-1];
                    loseRow[i][j]=loseRow[i][j-1];
                }

            if(i-1>=0 && A[i-1][j]!='#')
                {
                    win[i][j]|=loseCol[i-1][j];
                    loseCol[i][j]=loseCol[i-1][j];
                }

            loseRow[i][j]|=!win[i][j];
            loseCol[i][j]|=!win[i][j];
        }

    cout<<(win[n-1][n-1] ? "Player 1 wins\n" : "Player 2 wins\n");
}