Jump Game
Bạn đang ở chỉ số 0 của mảng nums. Từ chỉ số i, bạn có thể nhảy về phía trước từ 1 đến nums[i] bước, vì vậy nums[i] là khoảng nhảy xa nhất từ đó và 0 có nghĩa là bạn không thể di chuyển. Trả về true nếu có một chuỗi các bước nhảy đưa bạn đến chỉ số cuối cùng, nếu không thì trả về false.
Hàm
- numsinteger-array
- cú nhảy xa nhất bạn có thể thực hiện từ mỗi chỉ số
- Trả vềboolean
- đúng nếu bạn có thể đến được chỉ số cuối cùng khi bắt đầu từ chỉ số 0, nếu không thì sai
Ràng buộc
1 ≤ nums.length ≤ 1040 ≤ nums[i] ≤ 105- Một bước nhảy có thể ngắn hơn
nums[i], vì vậy một bước nhảy dài không bao giờ buộc bạn vượt quá chỉ số cuối cùng.
Ví dụ
- Đầu vào
- nums = [2, 0, 3, 1, 0, 2]
- Đầu ra
- true
- Giải thích
- Từ chỉ số 0, bạn có thể đến chỉ số 1 hoặc 2. Chỉ số 1 chứa 0 và là ngõ cụt, nhưng chỉ số 2 chứa 3 và dẫn đến chỉ số 5, chỉ số cuối cùng.
- Đầu vào
- nums = [1, 3, 0, 0, 0, 2]
- Đầu ra
- false
- Giải thích
- Chỉ số 0 chỉ có thể chuyển đến chỉ số 1, còn chỉ số 1 có thể đi xa nhất đến chỉ số 4. Các chỉ số 2, 3 và 4 đều chứa giá trị 0, nên không có gì vượt qua chỉ số 4 để đến chỉ số 5.
- Đầu vào
- nums = [0]
- Đầu ra
- true
- Giải thích
- Mảng có một phần tử, vì vậy bạn bắt đầu ở chỉ số cuối cùng và không cần nhảy chút nào.
Câu hỏi mở rộng
Đếm số chuỗi nhảy khác nhau để đến chỉ số cuối cùng, lấy modulo 10^9+7, vẫn trong thời gian O(n).
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
0chỉ khiến bạn mắc kẹt khi không có phần tử nào đứng trước nó có thể nhảy qua nó. Bạn cần biết điều gì về các chỉ số đứng trước nó để xác định?Nếu bạn có thể đến chỉ số
i, bạn cũng có thể đến mọi chỉ số từiđếni+nums[i], vì bạn có thể nhảy những bước ngắn hơn. Vì vậy, các chỉ số có thể đến luôn tạo thành một khối liên tục bắt đầu từ chỉ số 0.Đi từ trái sang phải và giữ
farthest, điểm cuối bên phải của khối đó. Nếu chỉ số hiện tại vượt quáfarthest, bạn không thể đến được chỉ số đó. Nếu không, hãy cập nhậtfarthestthànhi+nums[i]nếu giá trị này lớn hơn. Nếu bạn đi hết mảng, bạn có thể đến được chỉ số cuối cùng.
Lời giải
Thử từng lộ trình một sẽ mất thời gian tăng theo hàm mũ. Thay vào đó, hãy xem các chỉ số: những chỉ số bạn có thể đến luôn tạo thành một khối liền mạch bắt đầu từ chỉ số 0. Đầu bên phải của khối đó chứa mọi thứ bạn cần, và chỉ cần duyệt một lượt là có thể quyết định đáp án.
Thử mọi cú nhảy
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Bắt đầu từ chỉ số 0, lần lượt thử mọi vị trí có thể tiếp đất, rồi lặp lại thao tác đó từ mỗi vị trí. Nếu bất kỳ nhánh nào đến được chỉ số cuối cùng, đáp án là true. Cách tìm kiếm này chính xác vì nó thử mọi đường đi.
Cách này không ghi nhớ chỉ số nào đã khám phá, nên khi đáp án là false, nó phải loại trừ mọi đường đi. Trong [4, 3, 2, 1, 0, 5], mọi chỉ số trước số 0 đều có thể đến số 0, tạo ra 8 đường đi đến đó. Với 30 chỉ số như vậy, sẽ có hơn 500 triệu đường đi.
Thuật toán
- Viết một hàm hỗ trợ
reach(i)để trả lời câu hỏi: bạn có thể đi từ chỉ sốiđến chỉ số cuối cùng không? - Nếu
ilà chỉ số cuối cùng, trả vềtrue. - Nếu không, hãy thử mọi vị trí tiếp đất
nexttừi+1đếnmin(i+nums[i], n-1), và trả vềtruengay khireach(next)trả về giá trị đó. - Nếu không có vị trí tiếp đất nào phù hợp, trả về
false. - Đáp án là
reach(0).
def canJump(nums):
last = len(nums) - 1
def reach(i):
# Can you get from index i to the last index?
if i == last:
return True
for nxt in range(i + 1, min(i + nums[i], last) + 1):
if reach(nxt):
return True
return False
return reach(0)Hãy nhớ những chỉ số nào có thể kết thúc
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Gọi một chỉ số là tốt nếu bạn có thể đi đến cuối từ đó. Chỉ số cuối cùng là tốt, và chỉ số i là tốt nếu có một chỉ số nào đó từ i+1 đến i+nums[i] là tốt. Mỗi chỉ số chỉ phụ thuộc vào các chỉ số ở bên phải nó, vì vậy hãy điền bảng từ phải sang trái và xác định mỗi chỉ số một lần. Trong ví dụ đầu tiên, chỉ số 2 đi đến được chỉ số 5, nên nó là tốt, và chỉ số 0 cũng vậy vì đi đến được chỉ số 2.
Việc xác định một chỉ số vẫn có thể phải quét n ô. Trong [9998, 9997, …, 1, 0, 7], mọi chỉ số đều đi đến được số 0 nhưng không đi xa hơn, nên mỗi chỉ số phải quét toàn bộ phạm vi của nó: khoảng 5 × 10^7 lượt kiểm tra với 10^4 phần tử.
Thuật toán
- Tạo một mảng boolean (boolean array) có độ dài
nvà đặtgood[n-1]thành true. - Duyệt
itừn-2xuống 0. - Duyệt
jtừi+1đếnmin(i+nums[i], n-1). Nếu có bất kỳgood[j]nào là true, hãy đặtgood[i]thành true rồi dừng duyệt. - Trả về
good[0].
def canJump(nums):
n = len(nums)
good = [False] * n # good[i]: from i you can reach the last index
good[n - 1] = True
for i in range(n - 2, -1, -1):
for j in range(i + 1, min(i + nums[i], n - 1) + 1):
if good[j]:
good[i] = True
break
return good[0]Theo dõi chỉ số xa nhất có thể đến được
Ý tưởng
Từ một chỉ số có thể đến được i, bạn có thể đi đến bất kỳ vị trí nào từ i+1 đến i+nums[i], không có khoảng trống. Đoạn đó bắt đầu bên trong khối đã đến được, nên khối mở rộng mà không có khoảng trống và luôn là [0, farthest].
Duyệt i từ trái sang phải. Khi i ≤ farthest, chỉ số i có thể đến được và mở rộng farthest thành max(farthest, i+nums[i]). Nếu i vượt quá farthest, không có chỉ số nào có thể đến được nhảy đến i, và khối không thể vượt qua khoảng trống đó, nên không thể đến được chỉ số cuối cùng. Chỉ giữ lại điểm cuối bên phải là đủ: bạn không quyết định nhảy đến vị trí nào, và mọi vị trí có thể đến ngắn hơn đều nằm bên trong khối.
Trong ví dụ thứ hai, farthest lần lượt là 0, 1, 4 rồi giữ nguyên ở 4 qua ba số 0. Chỉ số 5 nằm ngoài phạm vi đó, nên kết quả là false.
Thuật toán
- Đặt
farthest = 0. - Duyệt từng chỉ số
itừ trái sang phải: nếui > farthest, trả vềfalse. - Nếu không, đặt
farthest = max(farthest, i+nums[i]). - Nếu vòng lặp kết thúc, mọi chỉ số đều có thể đến được, nên trả về
true.
def canJump(nums):
farthest = 0 # every index up to farthest can be reached
for i, jump in enumerate(nums):
if i > farthest:
return False # nothing reachable jumps to i
farthest = max(farthest, i + jump)
return True
Lỗi thường gặp và trường hợp biên
- Luôn nhảy đúng
nums[i]bước. Với[2, 5, 0, 0], cú nhảy hết cỡ từ chỉ số 0 sẽ đáp xuống ô 0, còn cú nhảy 1 bước đến chỉ số 1 sẽ chạm đích. - Trả về
falsengay khi bạn thấy số 0.[2, 0, 1]nhảy qua số 0 và đáp án làtrue. - Cập nhật
farthesttrước khi kiểm trai > farthest. Chỉ số không thể tới được không được làm giãn khối. - Coi mảng có một phần tử là thất bại. Bạn đã đứng ở chỉ số cuối cùng, ngay cả khi phần tử đó là 0.
- Dùng đệ quy với mảng dài. Một đường đi có thể dài 10,000 cú nhảy, khiến ngăn xếp lời gọi bị tràn trong một số ngôn ngữ; Python mặc định dừng ở 1,000 lần gọi lồng nhau.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Jump Game là gì?
Thuật toán tìm phạm vi xa nhất duyệt qua mỗi chỉ số một lần, nên chạy trong O(n) thời gian và dùng thêm O(1) không gian. Cách dùng bảng có độ phức tạp O(n²) trong trường hợp xấu nhất, còn thử mọi lộ trình thì có độ phức tạp theo hàm mũ.
Jump Game có phải là một bài toán quy hoạch động không?
Có thể làm như sau: đánh dấu từng chỉ số là tốt nếu một trong các vị trí có thể đến từ đó là tốt, xét từ phải sang trái, trong O(n²). Chỉ cần quan tâm đến chỉ số tốt ngoài cùng bên trái tìm được cho đến lúc này, vì chỉ số nào đến được một chỉ số tốt ở bên phải thì cũng đến được chỉ số đó. Chuyển goal thành i mỗi khi i+nums[i] ≥ goal.
Làm thế nào để tìm số lần nhảy ít nhất?
Áp dụng ý tưởng vươn xa nhất theo từng lớp. Theo dõi điểm cuối của khối bạn có thể đến được với số lần nhảy hiện tại và chỉ số xa nhất mà lần nhảy tiếp theo có thể đến. Khi i vượt qua điểm cuối của khối hiện tại, hãy tăng số lần nhảy thêm một và mở rộng khối đến chỉ số xa nhất đó. Vẫn chỉ cần một lượt duyệt O(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 canJump(nums):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
nums = [2, 0, 3, 1, 0, 2]
Đầu ra mong đợi
true