优化Leaper图算法?

7
在与谷歌进行的45分钟技术面试中,我遇到了一个Leaper Graph问题。 我编写了可以工作的代码,但后来被拒绝了工作机会,因为我缺乏数据结构知识。 我在想我还能做得更好点什么。
问题是这样的:给定一个大小为N的棋盘,并告诉你一件棋子可以水平跳跃i个位置(左或右),并且垂直跳跃j个位置(上或下)(即,类似于国际象棋中的马),那么跳跃者能否到达棋盘上的每个位置?
我编写了以下算法。 它通过递归地查找是否可以到达棋盘上的每个位置来标记访问过的图上的所有点。 如果无法到达,则至少有一个字段为false,函数将返回false。
      static boolean reachable(int i, int j, int n) {
        boolean grid[][] = new boolean[n][n];
        reachableHelper(0, 0, grid, i, j, n - 1);
        for (int x = 0; x < n; x++) {
          for (int y = 0; y < n; y++) {
            if (!grid[x][y]) {
              return false;
            }
          }
        }
        return true;
      }

      static void reachableHelper(int x, int y, boolean[][] grid, int i, int j, int max) {
        if (x > max || y > max || x < 0 || y < 0 || grid[x][y]) {
          return;
        }
        grid[x][y] = true;
        int i2 = i;
        int j2 = j;
        for (int a = 0; a < 2; a++) {
          for (int b = 0; b < 2; b++) {
            reachableHelper(x + i2, y + j2, grid, i, j, max);
            reachableHelper(x + j2, y + i2, grid, i, j, max);
            i2 = -i2;
          }
          j2 = -j2;
        }
      }

现在,有人指出最佳解决方案是实现唐纳德·克努斯的互质实现:http://arxiv.org/pdf/math/9411240v1.pdf。45分钟的技术面试中是否应该能够找到这个答案呢?
除了上述内容,我还能做得更好吗?
编辑: - 我询问了起始位置。我被告知从0,0开始是可以的。
编辑2: 根据反馈,我使用了while循环和队列的方法。当n = 85时,递归方法会导致堆栈溢出。然而,下面的while循环和队列方法可以处理高达n = 30,000的问题。(此后,它将遇到内存超过GB的堆问题)。如果您知道如何进一步优化,请告诉我。
    static boolean isReachableLoop(int i, int j, int n) {
        boolean [][] grid = new boolean [n][n];

        LinkedList<Point> queue = new LinkedList<Point>();
        queue.add(new Point(0,0)); // starting position. 

        int nodesVisited = 0;
        while (queue.size() != 0) {
          Point pos = queue.removeFirst();

          if (pos.x >= 0 &&  pos.y >= 0 && pos.x < n && pos.y < n) {
            if (!grid[pos.x][pos.y]) {
              grid[pos.x][pos.y] = true;
              nodesVisited++;
              int i2 = i;
              int j2 = j;
              for (int a = 0; a < 2; a++) {
                for (int b = 0; b < 2; b++) {
                  queue.add(new Point(pos.x+i2, pos.y+j2));
                  queue.add(new Point(pos.x+j2, pos.y+i2));
                  i2 = -i2;
                }
                j2 = -j2;
              }
            }
          }
        }
        if (nodesVisited == (n * n)) {
          return true;
        } else {
          return false;
        }
      }

1
你是否有一个起始位置?还是必须自己发现最佳位置? - smac89
1
我向面试官询问了这个问题。他告诉我0x0是一个合法的位置。 - Leo Ufimtsev
3
如果你可以从棋盘上的“任何”位置到达所有地方,那么你可以从“每个”位置到达棋盘上的所有地方,因此起始位置并不重要。 - Matt Timmermans
2
但以上基本上已经是一个递归深度优先搜索了,不是吗? - Leo Ufimtsev
1
是的,递归深度优先搜索有时会导致堆栈溢出。 - Saeid
显示剩余4条评论
2个回答

3
我会问很多这样的面试问题。我不认为你在面试中需要想出互质方法,但如果你每次递归调用都传递了所有这些参数而没有使用对象,我会扣分,因为这将使用O(n^2)堆栈空间。
我会问你关于这个问题,并期望你提出使用堆上的堆栈或队列的BFS或DFS。如果你失败了,我可能会抱怨“缺乏数据结构知识”。
我还会问一些问题,以确保你在分配2D数组时知道自己在做什么。
如果你真的很好,我会问你是否可以利用问题的对称性来减少搜索空间。实际上,你只需要搜索一个J * J大小的网格(假设J> = i)。
重要的是要记住,面试官不仅看你的答案,还看你解决问题的方式和你脑中可以运用于解决问题的工具。
编辑:再考虑一下,到达互质方法的途中有许多渐进步骤,你也可能想到。没人会期望那样,但那会很令人印象深刻!

1
我明白了。谢谢您的回复。 - Leo Ufimtsev
我添加了一个while循环+队列的方法(请参见编辑2)。您是否批准这样的代码? - Leo Ufimtsev
1
它没有任何严重的问题。对于BFS,ArrayDeque<Point>更有效率,对于DFS,ArrayList<Point>更有效率,如果在将点放入队列之前检查grid[][],则可以节省80%的内存。 - Matt Timmermans
ArrayList<Point> 为什么会更有效率?ArrayList 是动态数组,添加和删除元素需要大量移动,而 LinkedList 则是由单独的元素拼接而成的。关于 grid[][] 检查的问题,你提出了一个很好的观点,谢谢。 - Leo Ufimtsev
1
就内存而言,在64位机器上,ArrayList每个条目的成本将在8到16字节之间,不包括Point对象本身。这是1到2个引用的成本,因为支持数组将在一半满和完全满之间。 LinkedList每个条目至少需要32个字节,因为每个节点都需要一个对象头、下一个和前一个链接以及Point引用。 - Matt Timmermans
1
时间方面,将元素添加到ArrayList中也会显着更快。包括所有的移动操作,向数组列表中添加元素平均需要设置2个引用,因为所有这些移动操作平均只会复制每个元素1次(如何工作是另一个常见的面试问题)。向LinkedList中添加元素意味着分配一个新对象并修复4个引用。缓存局部性在链表中也要差得多。哦,注意ArrayList是用于DFS的,这意味着您从末尾添加和删除元素。对于BFS,您将使用ArrayDeque,因为您需要从前面删除元素。 - Matt Timmermans

2

对不起,我感觉我漏掉了什么。

如果你只能通过 i 上下移动或 j 左右移动,那么一个位置 (x,y) 可以从起始位置 (a,b) 到达,当且仅当存在整数 m 和 n 满足以下条件:

a + m*i = x

b + n*j = y

也就是说,当 n > 1 时,在一个正方形棋盘上所有情况都不成立。

如果你的意思更像是象棋中的马走法,你可以通过 i 上下移动或 j 左右移动,或者通过 j 上下移动或 i 左右移动,你可以使用同样的技巧。只需要解决两个方程:

a + m * i + n * j = x

b + o * i + p * j = y

如果没有整数 m、n、o 和 p 满足这些方程,则无法到达该点。


后者更像是一个可以跳到8个不同位置的骑士。问题不在于达到x,y,而在于棋盘上的每个位置是否都可以到达。此外,在我的情况下,我必须编写特定的代码而不是编写数学方程?(你有什么想法?) - Leo Ufimtsev
如果您可以检查一个点是否可以到达,那么您只需要循环遍历每个点并每次进行检查。还有一些更或多或少明显的优化可以添加,例如使用问题的对称性来减少搜索空间。 - Leherenn
板子的有限宽度对你的整数m、n(以及o和p)又加了一个条件。在第二种(“骑士式”)情况下,还有m=p和n=o的限制。 - j_random_hacker

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