1. 程式人生 > >【LeetCode】935. Knight Dialer 解題報告(Python)

【LeetCode】935. Knight Dialer 解題報告(Python)

作者: 負雪明燭
id: fuxuemingzhu
個人部落格: http://fuxuemingzhu.cn/


目錄

題目地址:https://leetcode.com/problems/knight-dialer/description/

題目描述

A chess knight can move as indicated in the chess diagram below:

此處輸入圖片的描述 . 此處輸入圖片的描述

This time, we place our chess knight on any numbered key of a phone pad (indicated above), and the knight makes N-1 hops. Each hop must be from one key to another numbered key.

Each time it lands on a key (including the initial placement of the knight), it presses the number of that key, pressing N

digits total.

How many distinct numbers can you dial in this manner?

Since the answer may be large, output the answer modulo 10^9 + 7.

Example 1:

Input: 1
Output: 10

Example 2:

Input: 2
Output: 20

Example 3:

Input: 3
Output: 46

Note:

  1. 1 <= N <= 5000

題目大意

馬的初始位置可以在撥號按鍵的任意位置,現在要讓它走N - 1步,問這個馬能產生出多少種不同的撥號號碼?

解題方法

動態規劃TLE

本週周賽第二題,卡了我好久啊!好氣!

這個題本身肯定是動態規劃題目,設定dp陣列為當前步以每個按鍵結尾的狀態數。所以我使用了一個4×3的二維陣列,需要注意的是左下角和右下角的位置不可能到達,設定它的數值為0.狀態轉移方程很好求得,那就是把上一步可能存在的位置狀態累加在一起就成了當前位置的狀態數。

問題是會超時啊!甚至可能會超過記憶體限制!

先上一份很容易想到的,但是會超時TLE的程式碼:

時間複雜度是O(N),空間複雜度O(N).

class Solution:
    def knightDialer(self, N):
        """
        :type N: int
        :rtype: int
        """
        self.ans = dict()
        self.ans[0] = 10
        board = [[1] * 3 for _ in range(4)]
        board[3][0] = board[3][3] = 0
        pre_dict = {(i, j) : self.prevMove(i, j) for i in range(4) for j in range(3)}
        for n in range(1, N):
            new_board = copy.deepcopy(board)
            for i in range(4):
                for j in range(3):
                    cur_move = 0
                    for x, y in pre_dict[(i, j)]:
                        cur_move = (cur_move + board[x][y]) % (10 ** 9 + 7)
                    new_board[i][j] = cur_move
            board = new_board
        return sum([board[i][j] for i in range(4) for j in range(3)]) % (10 ** 9 + 7)
        
    def prevMove(self, i, j):
        if (i, j) == (3, 0) or (i, j) == (3, 2):
            return []
        directions = [(-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1)]
        res = []
        for d in directions:
            x, y = i + d[0], j + d[1]
            if 0 <= x < 4 and 0 <= y < 3 and (x, y) != (3, 0) and (x, y) != (3, 2):
                res.append((x, y))
        return res

在比賽的時候剩下的一個小時都在優化這個題,個人感覺這個題卡時間卡的有點太嚴了,上面這個做法應該是標準做法吧,通過不了,需要一些奇技淫巧才能通過。

空間換時間,利用對稱性

這是我在比賽最後的時間通過的程式碼,把所有狀態給初始化了,這樣好處是可以不用在迴圈中不停地copy原來的棋盤狀態了,同時利用了對稱性,只需要求出4個位置(1,2,4,0)的狀態,其餘狀態可以直接利用對稱性得到。

還有一個優化的地方在於在每次的過程中進行取模!雖然取模運算是耗時的運算,但是數字很大的時候,大整數既佔空間又佔時間,所以取模!

經過上面的優化勉強通過了,真是不容易,我覺得這個題非常不友好,因為同樣的Java程式碼可以不做任何優化就通過了。這個題在N很大的時候還會告訴我記憶體超了……簡直了。。

時間複雜度是O(N),空間複雜度O(N).總時間1500ms。

class Solution:
    def knightDialer(self, N):
        """
        :type N: int
        :rtype: int
        """
        self.ans = dict()
        self.ans[0] = 10
        board = [[[1] * 3 for _ in range(4)] for _ in range(N)]
        board[0][3][0] = board[0][3][2] = 0
        pre_dict = {(i, j) : self.prevMove(i, j) for i in range(4) for j in range(3)}
        for n in range(1, N):
            for i in range(2):
                cur_move = 0
                for x, y in pre_dict[(i, 0)]:
                    cur_move += board[n - 1][x][y]
                board[n][i][0] = cur_move % (10 ** 9 + 7)
            cur_move = 0
            for x, y in pre_dict[(0, 1)]:
                cur_move += board[n - 1][x][y]
            board[n][0][1] = cur_move % (10 ** 9 + 7)
            cur_move = 0
            for x, y in pre_dict[(3, 1)]:
                cur_move += board[n - 1][x][y]
            board[n][3][1] = cur_move % (10 ** 9 + 7)
            board[n][4][0] = board[n][0][0]
            board[n][0][2] = board[n][0][0]
            board[n][5][1] = 0
            board[n][6][2] = board[n][7][0]
            board[n][8][1] = board[n][0][1]
            board[n][9][2] = board[n][0][2]
            board[n][3][0] = board[n][3][2] = 0
        return (board[N - 1][0][0] * 4 + board[N - 1][0][1] * 2 + board[N - 1][10][0] * 2 + board[N - 1][3][1] + board[N - 1][11][1]) % (10 ** 9 + 7)
        
    def prevMove(self, i, j):
        if (i, j) == (3, 0) or (i, j) == (3, 2):
            return []
        directions = [(-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1)]
        res = []
        for d in directions:
            x, y = i + d[0], j + d[1]
            if 0 <= x < 4 and 0 <= y < 3 and (x, y) != (3, 0) and (x, y) != (3, 2):
                res.append((x, y))
        return res

優化空間複雜度

上面的做法我一直在想著優化時間複雜度,事實上,每個狀態只和之前的狀態有關,所以很容易想到優化空間複雜度。

使用10個變數,分別儲存每個位置能取到的狀態數,然後人為的把每個狀態能通過其他的狀態得到的程式碼給寫出來就行了。

程式碼如下,真的很簡潔,為什麼我沒有想到優化空間!!優化之後時間降到了264 ms,這個告訴我們,優化空間同樣可以大規模地降低時間,如果DP問題超時的話,優先考慮空間!

時間複雜度是O(N),空間複雜度O(1).時間264 ms.

class Solution:
    def knightDialer(self, N):
        """
        :type N: int
        :rtype: int
        """
        if N == 1: return 10
        x1 = x2 = x3 = x4 = x5 = x6 = x7 = x8 = x9 = x0 = 1
        MOD = 10 ** 9 + 7
        for i in range(N - 1):
            x1, x2, x3, x4, x5, x6, x7, x8, x9, x0 = (x6 + x8) % MOD,\
                (x7 + x9) % MOD, (x4 + x8) % MOD, (x3 + x9 + x0) % MOD, 0, (x1 + x7 + x0) % MOD,\
                (x2 + x6) % MOD, (x1 + x3) % MOD, (x2 + x4) % MOD, (x4 + x6) % MOD
        return (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x0) % MOD

如果在上面的解法上再利用好對稱性的話,可以把時間再次降低到160 ms。

時間複雜度是O(N),空間複雜度O(1).時間160 ms。

class Solution:
    def knightDialer(self, N):
        """
        :type N: int
        :rtype: int
        """
        if N == 1: return 10
        x1 = x2 = x3 = x4 = x5 = x6 = x7 = x8 = x9 = x0 = 1
        MOD = 10 ** 9 + 7
        for i in range(N - 1):
            x1, x2, x4, x0 = (x6 + x8) % MOD, (x7 + x9) % MOD, (x3 + x9 + x0) % MOD, (x4 + x6) % MOD
            x3, x5, x6, x7, x8, x9 = x1, 0, x4, x1, x2, x1
        return (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x0) % MOD

相似題目

688. Knight Probability in Chessboard

參考資料

https://leetcode.com/problems/knight-dialer/discuss/189252/O(logN)

日期

2018 年 11 月 4 日 —— 下雨的週日