获胜贪吃蛇与梯子游戏的最少步数

14
给定一款“蛇与梯子”游戏,编写一个函数,返回到达终点所需的最少跳跃次数。你可以假设你掷骰子时,结果总是有利于你。
***
以下是我提供的解决方案,但不确定是否正确。
这个问题类似于在数组中青蛙跳跃。但在此之前,我们需要以这种格式对问题进行建模。
创建一个大小为100的数组,并为每个位置存储6(如果没有蛇或梯子)。如果该位置上有梯子,则存储跳跃次数。如果有蛇,则在该位置存储负跳跃值。
现在,我们需要解决如何以最少的步骤到达结尾。主要问题可以使用动态规划在O(n^2)时间复杂度和O(n)空间复杂度内解决。

尝试使用BFS(广度优先搜索)算法。由于从某一点开始,您最多只会向前移动6个单元格,因此运行时间为O(N * 6)= O(N)。 - Fallen
@Fallen,是的,但在这六个步骤中,我必须选择具有最大梯子跳跃的那一个,并避开蛇的位置。对吗? - AKS
2
我在我的博客上使用模拟来解决这个问题。我的解决方案是用Scheme编写的,还有Haskell和Python的解决方案。除了模拟,评论者还使用转移矩阵和Dijkstra最短路径算法来找到解决方案。 - user448810
5
请在下投票时留下您的评论和原因。如果没有任何评论,就没有下投票的意义。 - AKS
为了实现可扩展性,我肯定会考虑使用Dijkstra最短路径算法,但是考虑到问题的规模,也可以轻松地使用暴力方法解决(6^7并不多)。 - Dennis Jaheruddin
8个回答

6
请看这篇博客文章this,它对Chutes and Ladders进行了完整的数学分析,使用了Monte-Carlo模拟和Markov链。他展示了一种计算获胜的最小步数的方法(基本上是构造一个转移矩阵,并查看需要多少次乘以起始向量才能得到具有非零终止位置的解)。这可能不是最有效的方法,但这篇文章确实值得一读。
以下是使用python和集合的快速解决方案。我从博客文章中获取了跳跃表中的数字。在每个步骤中,它只需计算出从前一个步骤可达的所有位置,并继续这样做,直到最终位置位于可达位置之间。
jumps = {1: 38, 4: 14, 9: 31, 21: 42, 28: 84, 36: 44, 51: 67, 71: 91, 80: 100,
  98: 78, 95: 75, 93: 73, 87: 24, 64: 60, 62: 19, 56: 53, 49: 11, 48: 26, 16: 6}
final_pos = 100
positions = {0} #initial position off the board
nsteps = 0

while final_pos not in positions:
    nsteps += 1
    old_positions = positions
    positions = set()
    for pos in old_positions:
        for dice in range(1, 7):
            new_pos = pos + dice
            positions.add(jumps.get(new_pos, new_pos))

print 'Reached finish in %i steps' % nsteps            

执行时间可以忽略不计,它会输出正确的答案(参见博客),为7。

有可能所有6个连续的位置都向前跳跃到了之前的位置,无法到达最后的位置。 - Fu Jiantao

3
这是一份简单的Python广度优先搜索解决方案:
# the target square and the positions of the snakes and ladders:
top = 100
jump = { 1: 38,  4: 14,  9: 31, 16:  6, 21: 42, 28: 84, 36: 44,
        48: 26, 49: 11, 51: 67, 56: 53, 62: 19, 64: 60, 71: 91,
        80:100, 87: 24, 93: 73, 95: 75, 98: 78}

# start from square 0 (= outside the board) after 0 rolls
open = {0}
path = {0: ()}

while len(open) > 0:
    i = open.pop()
    p = path[i] + (i,)
    for j in xrange(i+1, i+7):
        if j > top: break
        if j in jump: j = jump[j]
        if j not in path or len(path[j]) > len(p):
            open.add(j)
            path[j] = p

for i in path:
    print "Square", i, "can be reached in", len(path[i]), "rolls via", path[i]

棋盘布局(即jump字典)取自Bas Swinckels在他的答案中链接的博客文章。此代码将打印出到达棋盘上每个可到达方格的(其中之一)最短路径,以以下内容结束:
Square 100 can be reached in 7 rolls via (0, 38, 41, 45, 67, 68, 74)

如果您想查看完整的输出,请参见ideone.com上的此演示

如果在编写 open.add(j) 之前不编写 if j in jump: j = jump[j],那么就会出错。对吧?我刚看到了这个,担心他们编写的代码可能是错误的,因为他们没有将 jump[j] 放入队列中。请检查一下。 - ajaysinghnegi
如果使用队列而不是变量open进行设置,则无需比较“len(path [j])> len(p):”,因为它现在按深度顺序访问。使用集合的问题在于,由于跳跃,节点可能不会按顺序到达,因此深度较小的节点可能比较大的节点先弹出,这需要比较“len(path [j])> len(p)”,这不再严格符合“BFS”。 - Fu Jiantao

1
我已经用C#实现了这个。你可以在这里查看我的依据。我也会在下面粘贴代码。
在我的实现中,我考虑了以下几点:
  • avoiding loops : This happens when a snake crosses a ladder. For example when end of a ladder is head of a snake and tail of the snake is start of the same ladder. That's a simple example but there might be more complex loops.
  • taking good snakes : I noticed that you don't need to avoid all the snakes. Sometimes you need to take the snake since it helps you to get to the target quicker. I call them "good snakes". For example 3>60 then 61>50 then 51>100 (target). If you take the snake the min dice count will be 3 and without the snake 8.
  • optional and mandatory jumps : When optional jump is set to false the algorithm has to take the jump when gets to one.
  • shortest way awareness : When the algorithm gets to the target it will register the shortest way and during the calculation it will drop searches that are longer that the current found solution. That makes the process a lot faster in complex situations.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    namespace SnakeAndLaddersSolution
    {
      public class SnakeAndLadder
      {
        private struct Jump
       {
         public int Start;
         public int End;
       }
    
       private const int TOP = 100;
       private const int STEP_SIZE = 6;
       private int _minDiceCount = int.MaxValue;
       private readonly List<Jump> _jumps = new List<Jump>();
    
       public bool OptionalJump { get; set; }
    
       public void AddJump(int start, int end)
       {
           _jumps.Add(new Jump { Start = start, End = end });
       }
    
       public int Solve()
       {
           var path = new Stack<int>();
           path.Push(1); //start from square 1
           return FindMinimumDice(path, 0);
       }
    
       private int FindMinimumDice(Stack<int> path, int diceCount)
       {
           if (diceCount >= _minDiceCount)
           {
               //too long. we've already found a shortest path.
               //drop going deeper
               return -1;
           }
           var currentSquare = path.Peek();
           var diceCounts = new List<int>();
           int newDiceCount;
    
           var ignoreNormalJump = false;
    
        for (var i = 0; i <= STEP_SIZE; i++)
        {
            var newSquare = currentSquare + i;
            if (newSquare == TOP)
            {
                //got there
                var totalDiceCount = diceCount + (i == 0 ? 0 : 1);
                _minDiceCount = Math.Min(_minDiceCount, totalDiceCount); //register the shortest path
                return totalDiceCount;
            }
    
    
            if (_jumps.All(j => j.Start != newSquare)) continue; //only process jumps
    
            var jump = _jumps.First(j => j.Start == newSquare);
            if (path.Contains(jump.End))
                continue; //already been here
            path.Push(jump.Start);
            path.Push(jump.End);
            newDiceCount = FindMinimumDice(path, diceCount + (i == 0 ? 0 : 1));
            path.Pop();
            path.Pop();
            if (newDiceCount != -1)
                diceCounts.Add(newDiceCount);
            if (i == 0 && !OptionalJump) //the current squre is a jump that should be taken
            {
                ignoreNormalJump = true;
                break;
            }
        }
        if (!ignoreNormalJump)
        {
            var longestJump = 0;
            for (var i = STEP_SIZE; i > 0; i--)
            {
                if (_jumps.All(j => j.Start != currentSquare + i))
                {
                    longestJump = currentSquare + i;
                    break;
                }
            }
            if (longestJump != 0)
            {
                path.Push(longestJump);
                newDiceCount = FindMinimumDice(path, diceCount + 1);
                path.Pop();
                if (newDiceCount != -1)
                    diceCounts.Add(newDiceCount);
            }
        }
        return !diceCounts.Any() ? -1 : diceCounts.Min();
       }
      }
    
      class Program
      {
    
       static void Main(string[] args)
       {
           var sal = new SnakeAndLadder();
           //set OptionalJump to true if the jump is optional
           //sal.OptionalJump = true;
           sal.AddJump(10,60);
           sal.AddJump(51,100);
           Console.WriteLine(sal.Solve());
           Console.ReadLine();
       }
      }
      }
    

0

这是用Python编程语言编写的贪吃蛇和梯子游戏代码。

简单的代码可以赢得贪吃蛇和梯子游戏。

**************** 贪吃蛇和梯子游戏代码 ***********************

import random
import curses

s = curses.initscr()
curses.curs_set(0)
sh, sw = s.getmaxyx()
w = curses.newwin(sh, sw, 0, 0)
w.keypad(1)
w.timeout(100)

snk_x = sw/4
snk_y = sh/2
snake = [
    [snk_y, snk_x],
    [snk_y, snk_x-1],
    [snk_y, snk_x-2]
]

food = [sh/2, sw/2]
w.addch(food[0], food[1], curses.ACS_PI)

key = curses.KEY_RIGHT

while True:
    next_key = w.getch()
    key = key if next_key == -1 else next_key

    if snake[0][0] in [0, sh] or snake[0][1] in [0, sw] or snake[0] in snake[1:]:
        curses.endwin()
        quit()

    new_head = [snake[0][0], snake[0][1]]

    if key == curses.KEY_DOWN:
        new_head[0] += 1
    if key == curses.KEY_UP:
        new_head[0] -= 1
    if key == curses.KEY_LEFT:
        new_head[1] -= 1
    if key == curses.KEY_RIGHT:
        new_head[1] += 1

    snake.insert(0, new_head)

    if snake[0] == food:
        food = None
        while food is None:
            nf = [
                random.randint(1, sh-1),
                random.randint(1, sw-1)
            ]
            food =nf if nf not in snake else None
         w.addch(food[0], food[1], curses.ACS_PI)
    else:
        tail = snake.pop()
        w.addch(tail[0], tail[1], ' ')

     w.addch(snake[0][0], snake[0][1], curses.ACS_CKBOARD)

0

广度优先搜索(BFS)或动态规划解决方案将使用O(N)时间和O(N)空间。

初始化:保持一个辅助数组来保存梯子和蛇。假设从第x个单元格到第y个单元格有一条梯子。因此,auxi [x] = y。如果从单元格x到y有蛇,x>y,则保持auxi [x] = -1。如果当前单元格没有梯子或蛇,则保持auxi [x] = x;

动态规划解决方案:

res[top]=0;
for(int i  = top-1; i>=0; i--) {

    res[i] = INF;
    for(int j=1; j<=6; j++){

        if(i-j<0)break;

        if(auxi[i+j]>-1)     // if i+jth cell is start of a snake, we'll always skip it
        res[i]=min( res[i] , res[auxi[i+j]]+1 );
    }

}

由于假设蛇在x单元格开始,结束在第yth单元格,我们将始终跳过蛇开始的单元格。


我认为你的代码肯定有问题,它在初始化之前访问了 res 的元素。 - Ilmari Karonen
@IlmariKaronen:在哪里?请注意,我在内部循环之前初始化 res[i] = INF; - Fallen
2
是的,但 auxi[i-j] 可能会小于(通常是)i。 (我认为这可能是一个错误,应该是 auxi[i+j],因为您正在从 top 向后循环到 0,但即使您修复了它,每当有蛇时仍然会引用未初始化的元素。) - Ilmari Karonen
res是什么,表示从某个位置到达顶部需要的步数?你的算法是否考虑到解决方案可能同时上爬梯子和下滑蛇?不确定min()是否正确考虑了这一点,因为你似乎在查找可能会改变值的棋盘位置... - Bas Swinckels
1
我没有学过计算机科学,但我知道DP是什么。你得到了负评,因为你的算法要么非常难懂,要么就是有bug。正如我和@IlmariKaronen所指出的那样,如果有蛇,你可能正在访问res未初始化的部分,并且你没有在评论中回复这个批评。如果只有梯子,你的解决方案可能会起作用。但是,如果有一个从1到80的梯子,另一个从50到100的梯子和一个从81到49的蛇,它能找到解决方案吗?当i=80时,它还不知道49离终点只有一步之遥。你测试过自己的代码吗? - Bas Swinckels

0

这个程序模拟实际场景...请告诉我它是否符合期望...

import java.util.HashMap;
import java.util.Map;

public class SnakeLadder {

    private Map<Integer,Integer> snakeLadderMapping=new HashMap<Integer,Integer>();


    private int winPosition=100;
    private int currentPosition=1;

    public SnakeLadder(){
        snakeLadderMapping.put(9, 19);
        snakeLadderMapping.put(17, 5);
        snakeLadderMapping.put(12, 40);
        snakeLadderMapping.put(24, 60);
        snakeLadderMapping.put(68, 89);
        snakeLadderMapping.put(50, 12);
        snakeLadderMapping.put(84, 98);
        snakeLadderMapping.put(75, 24);
        snakeLadderMapping.put(72, 16);
    }

    public int startGame(){
        int count=0;
        while(currentPosition!=winPosition){
            count++;
            getNextPosition(rollDice());
        }
        System.out.println("Game Won!!!!!!");
        return count;
    }

    public int rollDice(){
        return 1+ (int)(Math.random()*5);
    }

    public void getNextPosition(int diceValue){
        int temp=currentPosition+diceValue;
        if(snakeLadderMapping.containsKey(temp)){
            currentPosition=snakeLadderMapping.get(temp);
        }else{
            if(temp<=winPosition){
                currentPosition=temp;
            }
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        SnakeLadder l=new SnakeLadder();
        System.out.println("No of Steps to win:"+l.startGame());

    }

}

0
  1. 将每个格子视为有向图中的一个顶点。 2.从第1个格子可以到达第2、3、4、5、6、7个格子,因此顶点1将有指向顶点2、顶点3……顶点7的有向边。类似地,对于其余的格子也是如此。
  2. 对于蛇-从蛇头顶点连接有向边到蛇尾顶点。
  3. 对于梯子-从梯子底部顶点连接有向边到梯子顶部顶点。
    1. 现在问题被简化为最短路径问题。因此,通过广度优先搜索(使用队列)我们可以解决这个问题。

在此处查看完整的代码解决方案 - https://algorithms.tutorialhorizon.com/snake-and-ladder-problem/


0

使用C#编写o(n)的解决方案。

构建一个帮助矩阵来遍历每一步,查看到达目标的最小路径并将其加1。

const int BoardSize = 100;
const int MaxStep = 6;
static void Main()
{

    // - means a ledder ending at the pos
    // + means a snake (taking you back n steps)
    int[] arr = new int[] {     
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,      8,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1, 
                            -1,     -1,     -1,     -1,     -1,     -1,     -1,     -71,    -1,     -1      };

    Console.WriteLine("Steps needed: " + solve(arr));
}

public static int solve(int[] inArr)
{
    int[] arr = new int[BoardSize];

    arr[0] = 1;

    for (int i = 0; i < BoardSize; ++i)
    {
        // steps here is the minimum of all the positions in all the previos 6 cells +1
        if (i < MaxStep)
            arr[i] = 1;
        else
        {
            int extraOption = int.MaxValue;
            if (inArr[i] < -1 || inArr[i] > 0)
                extraOption = arr[i + inArr[i]];
            else if (inArr[i] > 0)
                extraOption = arr[i + inArr[i]];
            arr[i] = min(arr[i - 1], arr[i - 2], arr[i - 3], arr[i - 4], arr[i - 5], arr[i - 6], extraOption) + 1;
        }
    }

    for (int i = 0; i < BoardSize; ++i)
    {
        Console.Write(arr[i] + "\t");
        if ((i + 1) % 10 == 0)
            Console.WriteLine("");
    }

    return arr[arr.Length-1];
}

public static int min(int a, int b, int c, int d, int e, int f, int g)
{
    int ab = Math.Min(a,b);
    int cd = Math.Min(c,d);
    int ef = Math.Min(e,f);
    return Math.Min(Math.Min(ab, cd), Math.Min(ef,g));
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接