Longest Increasing Path in a Matrix
Bạn nhận được matrix, một lưới gồm các số nguyên có m hàng và n cột, được biểu diễn dưới dạng danh sách các hàng. Một đường đi di chuyển lên, xuống, sang trái hoặc sang phải, không đi chéo và không vòng qua các cạnh, đồng thời mỗi bước phải đến một ô có giá trị lớn hơn nghiêm ngặt. Hãy trả về số ô trên đường đi dài nhất như vậy. Một ô đơn lẻ là đường đi gồm 1 ô.
Hàm
- matrixinteger-2d-array
- lưới giá trị, dưới dạng danh sách các hàng có độ dài bằng nhau
- Trả vềinteger
- số ô trên đường đi tăng nghiêm ngặt dài nhất
Ràng buộc
1 ≤ m, n ≤ 100, trong đóm = matrix.lengthvàn = matrix[i].length- Mỗi hàng có cùng độ dài
n. 0 ≤ matrix[i][j] ≤ 231-1
Ví dụ
- Đầu vào
- matrix = [[9, 8, 3], [2, 7, 4], [1, 6, 5]]
- Đầu ra
- 7
- Giải thích
- Đường đi 3, 4, 5, 6, 7, 8, 9 đi xuống cột bên phải, men theo hàng dưới cùng rồi đi lên cột giữa để đến ô 9: có 7 ô. Từ giá trị nhỏ nhất, 1, những đường đi tốt nhất có 5 ô.
- Đầu vào
- matrix = [[2, 2, 2], [2, 5, 2]]
- Đầu ra
- 2
- Giải thích
- Các giá trị bằng nhau không tạo thành một bước tiến, nên không có đường đi nào men theo các ô số 2. Tốt nhất là đi một bước từ một trong ba ô số 2 quanh ô số 5 sang ô số 5: 2 ô.
- Đầu vào
- matrix = [[4, 4], [4, 4], [4, 4]]
- Đầu ra
- 1
- Giải thích
- Mọi giá trị đều là 4, nên không có bước nào được phép ở bất kỳ đâu. Mỗi ô riêng lẻ tạo thành một đường đi gồm 1 ô, và 1 là đáp án.
Câu hỏi mở rộng
Bạn cũng có thể trả về các ô trên một đường đi dài nhất, cùng với độ dài của đường đi đó không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Đường đi có thể quay lại một ô mà nó đã đi qua không? Hãy quan sát các giá trị thay đổi như thế nào trên đường đi.
Giá trị chỉ tăng, nên một đường đi không bao giờ đi qua cùng một ô hai lần. Đường đi dài nhất bắt đầu từ một ô không phụ thuộc vào cách bạn đến đó. Độ dài của đường đi này bằng 1 cộng với độ dài đường đi dài nhất bắt đầu từ ô láng giềng có giá trị lớn hơn tốt nhất.
Tính số đó một lần cho mỗi ô rồi lưu lại. Bạn có thể điền số bằng cách dùng thuật toán tìm kiếm theo chiều sâu qua các ô lân cận lớn hơn, điều khiển bằng ngăn xếp của riêng bạn, hoặc lần lượt gỡ từng lớp khỏi các đỉnh của lưới và đếm số lớp.
Lời giải
Vẽ một mũi tên từ mỗi ô đến từng ô lân cận có giá trị lớn hơn. Giá trị tăng dọc theo mỗi mũi tên, vì vậy không chuỗi mũi tên nào có thể tạo thành vòng lặp: lưới là một đồ thị có hướng không chu trình, và đáp án là đường đi dài nhất của đồ thị đó. Khi không có chu trình, đường đi dài nhất từ một ô chỉ phụ thuộc vào ô đó, vì vậy bạn chỉ cần tính một lần cho mỗi ô: O(m × n).
Theo dõi mọi đường đi tăng dần
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Bắt đầu một đường đi tại mỗi ô và đi theo mọi nhánh có giá trị lớn hơn cho đến khi không còn nhánh nào; đường đi dài nhất chính là đáp án. Giá trị tăng ở mỗi bước, vì vậy đường đi không bao giờ quay lại một ô và không cần tập hợp các ô đã đi qua.
Đường đi bắt đầu từ một ô được thực hiện lại mỗi khi có đường đi khác đi qua ô đó. Trên lưới 100 × 100, trong đó mỗi giá trị bằng chỉ số hàng cộng với chỉ số cột, mỗi bước sang phải hoặc đi xuống đều làm giá trị tăng lên, và có hơn 10^58 đường đi bắt đầu từ ô trên cùng bên trái.
Thuật toán
- Với mỗi ô, đưa (ô đó, 1) vào ngăn xếp.
- Lấy một mục (ô, độ dài) ra khỏi ngăn xếp và cập nhật đáp án bằng độ dài đó.
- Với mỗi ô lân cận nằm trong lưới và có giá trị lớn hơn hẳn, đưa (ô lân cận, độ dài + 1) vào ngăn xếp.
- Lặp lại cho đến khi ngăn xếp trống, rồi chuyển sang ô bắt đầu tiếp theo.
- Trả về độ dài lớn nhất đã gặp.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
answer = 0
for sr in range(rows):
for sc in range(cols):
# Each entry is one path in progress: the cell it ends on and
# how many cells it has.
stack = [(sr, sc, 1)]
while stack:
r, c, length = stack.pop()
answer = max(answer, length)
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
stack.append((nr, nc, length + 1))
return answerTìm kiếm theo chiều sâu có ghi nhớ bằng ngăn xếp của riêng bạn
Ý tưởng
Gọi best[cell] là số ô trên đường đi tăng dài nhất bắt đầu tại ô đó: 1 + max(best[nb]) với các ô láng giềng lớn hơn nb, hoặc là 1 nếu không có ô nào. Lưu giá trị này là an toàn vì các ô đứng trước cell trên đường đi đều nhỏ hơn và không bao giờ đứng sau nó. Trong ví dụ đầu tiên, 9 có giá trị 1, 8 có giá trị 2, 7 có giá trị 3, 6 và 2 có giá trị 4, 5 và 1 có giá trị 5, 4 có giá trị 6, và 3 có giá trị 7.
Đường đi cũng có thể đi qua cả 10,000 ô, quá sâu để dùng đệ quy, vì vậy hãy tự quản lý ngăn xếp. Ô trên cùng thử hướng tiếp theo rồi đẩy một ô láng giềng lớn hơn chưa hoàn tất vào ngăn xếp. Khi đã thử cả bốn hướng, hãy lấy ô đó ra khỏi ngăn xếp và đặt giá trị best cho nó. Không cần đánh dấu "đang xử lý": mỗi ô trên ngăn xếp đều lớn hơn ô bên dưới.
Thuật toán
- Gán
bestbằng 0 (chưa biết) và đặt bộ đếm hướng bằng 0 cho mỗi ô. - Với mỗi ô có
bestbằng 0, đẩy ô đó vào ngăn xếp. - Ở ô trên cùng, tăng bộ đếm của ô đó và đẩy ô lân cận vào ngăn xếp nếu ô đó nằm trong lưới, có giá trị lớn hơn và chưa hoàn tất.
- Sau khi xét đủ bốn hướng, lấy ô ra khỏi ngăn xếp và đặt
bestbằng 1 cộng với giá trịbestlớn nhất trong số các ô lân cận có giá trị lớn hơn, hoặc bằng 1. - Trả về giá trị
bestlớn nhất.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
# best[r][c]: cells on the longest increasing path that starts at (r, c).
# 0 means not known yet.
best = [[0] * cols for _ in range(rows)]
# step[r][c]: how many of the 4 directions the search has tried from (r, c).
step = [[0] * cols for _ in range(rows)]
answer = 0
for sr in range(rows):
for sc in range(cols):
if best[sr][sc]:
continue
# Our own stack instead of recursion: a path can be thousands of
# cells long, past Python's limit of 1,000 nested calls.
stack = [(sr, sc)]
while stack:
r, c = stack[-1]
d = step[r][c]
if d < 4:
step[r][c] = d + 1
nr, nc = r + dirs[d][0], c + dirs[d][1]
# The stack only climbs, so a larger neighbour is never on it.
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c] and best[nr][nc] == 0:
stack.append((nr, nc))
continue
# Every larger neighbour is finished: build on the best of them.
stack.pop()
length = 1
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
length = max(length, best[nr][nc] + 1)
best[r][c] = length
answer = max(answer, length)
return answerGọt lưới từ các đỉnh của nó
Ý tưởng
Xây dựng cùng một bảng từ các giá trị cao nhất trở xuống, giống như sắp xếp tô pô của Kahn. Một đỉnh, tức ô không có ô láng giềng nào lớn hơn, mở đầu một đường đi gồm 1 ô: lớp 1. Loại bỏ tất cả các đỉnh; những ô mất đi ô láng giềng lớn hơn cuối cùng sẽ thuộc lớp tiếp theo. Lặp lại cho đến khi lưới trống. Một ô được xử lý sau ô láng giềng lớn hơn cuối cùng của nó một vòng, nên lớp của ô bằng best, và số lớp chính là đáp án. Trong ví dụ đầu tiên, các lớp là 9; 8; 7; 2 và 6; 1 và 5; 4; 3.
Đếm số ô láng giềng lớn hơn còn lại của mỗi ô. Khi loại bỏ một ô, giảm số đếm của mỗi ô láng giềng nhỏ hơn nó, và ô nào có số đếm bằng 0 sẽ thuộc lớp tiếp theo. Mỗi ô chỉ bị loại bỏ một lần: O(m × n), không cần ngăn xếp.
Thuật toán
- Với mỗi ô, đếm số ô láng giềng có giá trị lớn hơn.
- Đưa mọi ô có số đếm bằng 0 vào lớp hiện tại.
- Khi lớp hiện tại chưa trống, tăng số lớp lên 1. Với mỗi ô trong lớp đó, giảm số đếm của từng ô láng giềng có giá trị nhỏ hơn nghiêm ngặt, rồi đưa ô láng giềng nào có số đếm giảm xuống 0 vào lớp tiếp theo.
- Đặt lớp tiếp theo làm lớp hiện tại rồi lặp lại.
- Trả về số lớp.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
# higher[r][c]: neighbours of (r, c) with a larger value, not peeled yet.
higher = [[0] * cols for _ in range(rows)]
layer = []
for r in range(rows):
for c in range(cols):
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
higher[r][c] += 1
# A peak: no neighbour is larger, so a path from it has one cell.
if higher[r][c] == 0:
layer.append((r, c))
layers = 0
while layer:
layers += 1
next_layer = []
for r, c in layer:
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] < matrix[r][c]:
higher[nr][nc] -= 1
# Its last larger neighbour is peeled: it is a peak now.
if higher[nr][nc] == 0:
next_layer.append((nr, nc))
layer = next_layer
# Each layer is one step further from a peak; the count is the longest path.
return layers
Lỗi thường gặp và trường hợp biên
- So sánh bằng
>=thay vì>. Khi có hai số 4 liền kề, cách vét cạn sẽ đi tới đi lui mãi, còn cách tìm kiếm có ghi nhớ sẽ đọc độ dài vẫn đang được tính. - Dùng đệ quy trong Python. Các giá trị uốn lượn qua một lưới 100 × 100 tạo thành đường đi gồm 10.000 ô, gấp mười lần giới hạn mặc định 1.000 lần gọi.
- Bỏ qua các ô đã hoàn tất như khi tô màu loang. Độ dài được lưu của chúng chính là thứ ô hiện tại cần.
- Chỉ bắt đầu từ giá trị nhỏ nhất. Trong ví dụ đầu tiên, số 1 cho đường đi gồm 5 ô, nhưng đáp án là 7, bắt đầu từ số 3.
- Bắt đầu độ dài từ 0. Một lưới có các giá trị bằng nhau có đáp án là 1.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán đường đi tăng dài nhất trong ma trận là bao nhiêu?
Độ phức tạp thời gian và bộ nhớ là O(m × n), dùng tìm kiếm theo chiều sâu có ghi nhớ hoặc phương pháp bóc tách: mỗi ô được xử lý một lần và kiểm tra 4 ô lân cận. Thử mọi đường đi từ mọi ô sẽ có độ phức tạp theo cấp số nhân.
Longest Increasing Path in a Matrix là bài toán quy hoạch động hay bài toán đồ thị?
Cả hai. Đây là đường đi dài nhất trong đồ thị có hướng không chu trình, tức là quy hoạch động (dynamic programming) theo thứ tự tô pô. Sắp xếp các ô theo giá trị từ lớn nhất đến nhỏ nhất sẽ tạo ra một thứ tự hợp lệ khác, với chi phí O(m × n × log(m × n)) cho việc sắp xếp.
Điều này khác gì so với dãy con tăng dài nhất?
Dãy con có thể bỏ qua các phần tử nhưng vẫn giữ nguyên thứ tự của chúng; ở đây, đường đi phải bước sang một ô liền kề theo một trong bốn hướng. Trong cả hai trường hợp, một dãy tăng nghiêm ngặt không thể quay ngược lại chính nó.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def longestIncreasingPath(matrix):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
matrix = [[9, 8, 3], [2, 7, 4], [1, 6, 5]]
Đầu ra mong đợi
7