Featured image of post 【Leetcode】python - [2267] Check if There Is a Valid Parentheses String Path 個人解法筆記 | 292nd LeetCode Weekly Contest (內含:Memoization 記憶化搜索筆記)

【Leetcode】python - [2267] Check if There Is a Valid Parentheses String Path 個人解法筆記 | 292nd LeetCode Weekly Contest (內含:Memoization 記憶化搜索筆記)

整理 LeetCode #2267 — DP, linked list, DFS。

題目出處

2267. Check if There Is a Valid Parentheses String Path

難度

hard

題目分類

Array, Dynamic Programming, Matrix

2026-09-29 二刷

個人範例程式碼 - 二刷 (2026/09/29)

class Solution:
    def hasValidPath(self, grid: list[list[str]]) -> bool:
        h, w = len(grid), len(grid[0])
        table = [[set() for _ in range(w)] for _ in range(h)]
        # use set to prevent duplicate

        # early return impossible cases:
        if (h + w - 1) % 2 == 1 or grid[0][0] == ')' or grid[-1][-1] == '(':
            return False

        for i in range(h):
            for j in range(w):
                cur_possible_paths = set()
                delta = 1 if grid[i][j] == '(' else -1 # ( + 1, ) - 1

                if i == 0 and j == 0: # init case
                    cur_possible_paths.add(delta)

                # path from up and has valid path
                if i > 0:
                    for value in table[i-1][j]:
                        if value + delta >= 0:
                            cur_possible_paths.add(value + delta)

                # path from left and has valid path
                if j > 0:
                    for value in table[i][j-1]:
                        if value + delta >= 0:
                            cur_possible_paths.add(value + delta)

                table[i][j] = cur_possible_paths

        # Does final block contain 0?
        return True if 0 in table[-1][-1] else False

算法說明

概念簡單,但有點多細節要處理的題目。

  1. 首先記憶表是一開始就要建的,不然沒有方法能查上方或左方的情況(暴力解時間會爆)
    2.1. 狀態可以進行簡化,由於我們重點就是要看 ( 數量有沒有比 )多,所以等於我們只需要紀錄 ( 還可以使用的數量是否 >= 0,也就是紀錄還可以配對的左括號數量。
    2.2. 因此,每走到新的一格時,可以透過 +1, -1 來代表括弧的消耗
  2. 我們需要使用 set() 來紀錄所有的狀態,因為路徑可能來自各種不同的走法,就算是同一格也會有多種可能性,用 set 可以有效解決「不同路徑但同樣結果」的狀態重複問題。
    (對於結果來說,路徑不重要,合法才重要,紀錄還可以用的數字即可)
  3. 最後一步是要檢查 0 是否有在 table[-1][-1] 當中,有存在表示真的找得到路徑。

在此解法裡面,我們只需要判斷有沒有可能,實際路徑可能存在多種可能性。
如果需要明確路徑,那就要再加上更多的記憶了,這解法只紀錄格子的狀態,是無法直接回推所有可能路徑的。

Time Complexity

O(mn(m+n))

  1. mn 是掃矩陣的大小
  2. (m+n) 是因為到該任一位置時,到 (i,j) 的路徑長固定為 i+j+1,最大為 h+w-1 (因為限定只能往右或往下),也是我們最大能查表的搜尋數,簡化為 m+n

Space Complexity

O(mn(m+n))

  1. mn 是矩陣的大小
  2. (m+n) 是因為到該任一位置時,路徑總長固定為當下的 h+w-1 (因為限定只能往右或往下),簡化為 m+n

Boundary conditions

可以先判出一些不可能的情況

  1. h+w-1 (路徑總長固定值),如果除 2 有餘數,則括弧不可能閉合
  2. 起點 grid[0][0] == ‘)’,出現則無法開始
  3. 終點 grid[-1][-1] == ‘(’,出現則無法結束

這些可以快速 early return

2022-05-10 一刷

個人範例程式碼 - 純 DFS (會 TLE)

DIRECTIONS = [
    (1,0),
    (0,1)
]
class Solution:
    def hasValidPath(self, grid: List[List[str]]) -> bool:
        if not grid:
            return False

        return self.dfs(grid, 0, 0, set(), [])

    def dfs(self, grid, i, j, visited, queue):
        # print(i, j, visited, queue)
        # end of recursion
        if i <mark> len(grid)-1 and j </mark> len(grid[0])-1:
            if grid[i][j] <mark> ")" and queue and queue[-1] </mark> "(":
                return len(queue) == 1 # queue is empty
            else:
                return False # not ")" or queue[-1] not "(" 

        # define and split
        ans = False
        visited.add((i, j))
        for d_x, d_y in DIRECTIONS:
            if ans == True: # Find case, early return
                continue
            if self.is_valid(grid, i+d_x, j+d_y) and (i+d_x, j+d_y) not in visited:
                if grid[i][j] == "(":
                    queue.append("(") 
                    ans |= self.dfs(grid, i+d_x, j+d_y, visited, queue)
                    queue.pop(-1) # backtracking 
                else: # ")"
                    if not queue:
                        # return False, wrong: need remove
                        continue
                    else:
                        queue.pop(-1) # pop ")"
                        ans |= self.dfs(grid, i+d_x, j+d_y, visited, queue)
                        queue.append(")") # backtracking

        visited.remove((i, j))
        return ans

    def is_valid(self, grid, i, j):
        m, n = len(grid), len(grid[0])
        return 0 <= i < m and 0 <= j < n

算法說明

矩陣搜索,很直覺想到的就是使用 dfs 去搜尋,
不過速度上會不夠,我們需要優化一下

個人範例程式碼 - DFS + Memoization

class Solution:
    def hasValidPath(self, grid: List[List[str]]) -> bool:
        self.memo = {}
        return self.dfs(grid, 0, 0, 0)

    def dfs(self, grid, i, j, cnt):
        # end of recursion
        if i <mark> len(grid)-1 and j </mark> len(grid[0])-1 and grid[i][j] == ")":
            return cnt == 1

        if not self.is_valid(grid, i, j):
            return False
        if cnt < 0:
            return False

        # define
        if grid[i][j] == "(":
            cnt += 1
        else:
            cnt -= 1

        # split
        if (i, j, cnt) in self.memo:
            return self.memo[(i, j, cnt)]


        ans = self.dfs(grid, i+1, j, cnt) or self.dfs(grid, i, j+1, cnt)
        self.memo[(i, j, cnt)] = ans
        return ans


    def is_valid(self, grid, i, j):
        m, n = len(grid), len(grid[0])
        return 0 <= i < m and 0 <= j < n

最近在練習程式碼本身就可以自解釋的 Coding style,可以嘗試直接閱讀程式碼理解

算法說明

這個是在閱讀別人的解答後,自己重寫的解法,用 cnt 優化了 “()” 的 queue 判斷式,這個想法非常的漂亮。
另外這邊我也才注意到原題目要求的搜索方向固定只有「右」和「下」,
因此多方向的寫法其實也可以簡化得更漂亮。

判定條件

結束條件:

  • 「i = len(grid)-1」 and 「j = len(grid[0])-1」判定結束位置在右下角
  • grid[i][j] = “)",結束必須為 “)”
  • return cnt = 1 (我們也必須要知道的是,剩下個括弧數剛好等於 1,等於只剩「”("」)

終止條件:

  • 當 cnt < 0 ,表示只有 “)",retrun False
  • 當不在範圍內時,return False

Memoization 筆記 (記憶化搜索)

Memoization 記憶化搜索是解決這個 TLE 的關鍵,
簡單來說就是我們把「重複性」且「已經解過的問題」,做解答的筆記。

  • 如果說,DFS 是從起點往終點搜尋
  • Memoization,就是類似從終點往回看的過程紀錄 (類似 DP 概念)

以下面的圖片來說明,可能路線可能有非常多種,
但我們可以知道只要到「特定座標」,且「特定 cnt」,
就是一個重複性的題目,我們可以先把結果記錄下來。

下面看圖應該會更明顯,我們用兩種不同的路線都抵達了 (2, 2),
而我們只需要計算第一次,之後就直接從 memo 找結果,不用再計算了。

【Leetcode】python - [2267] Check if There Is a Valid Parentheses String Path 個人解法筆記 | 292nd LeetCode Weekly Contest (內含:Memoization 記憶化搜索筆記)

你可能會想問,這樣我們怎麼省時間的?

上面我們是用 True 為舉例比較好懂,
但他更可以替我們省下大量的 False 的時間,
因為我們也能夠以同樣概念的做 (2, 2, 0) = False (我隨便舉例的,理論上不可能)
這樣我們第二次碰到 (2, 2, 0) 這題目時 (從不同路線抵達此處,也呈現同樣狀態),
就可以不用再往下重搜了,直接回傳 False。

input handling

同 dfs 結束條件,一起在 dfs 內部處理

Boundary conditions

當上述終止條件或結束條件觸發時,return。

Reference

Licensed under CC BY-NC-SA 4.0
最後更新 Sep 29, 2026
使用 Hugo 建立
主題 Stack 由 Jimmy 設計