Split Array Largest Sum
Bạn nhận được một mảng nums gồm các số nguyên không âm và một số nguyên k. Chia nums thành đúng k phần, mỗi phần là một dãy không rỗng gồm các giá trị liền kề và giữ nguyên thứ tự. Mỗi phần có một tổng, chi phí của cách chia là tổng lớn nhất trong các phần đó.
Trả về chi phí nhỏ nhất mà một cách chia thành k phần có thể đạt được.
Hàm
- numsinteger-array
- các giá trị không âm, theo thứ tự
- kinteger
- số phần liên tiếp cần chia chúng thành
- Trả vềinteger
- giá trị nhỏ nhất có thể của tổng phần lớn nhất
Ràng buộc
1 ≤ nums.length ≤ 50000 ≤ nums[i] ≤ 1051 ≤ k ≤ nums.length- Mỗi phần chứa ít nhất một giá trị. Một phần có tất cả giá trị bằng 0 sẽ có tổng bằng 0, điều đó được phép.
Ví dụ
- Đầu vào
- nums = [6, 2, 9, 4, 7, 3]k = 3
- Đầu ra
- 13
- Giải thích
- Cách chia
[6, 2],[9, 4],[7, 3]có chi phí là 13. Không cách chia nào có chi phí là 12: xếp các phần từ trái sang phải, mỗi phần không vượt quá 12, ta được[6, 2],[9],[4, 7],[3], tổng cộng bốn phần.
- Đầu vào
- nums = [8, 1, 1, 1, 5]k = 2
- Đầu ra
- 8
- Giải thích
- Số 8 nằm ở một phần nào đó, nên không phép chia nào có thể tốn ít hơn 8.
[8]và[1, 1, 1, 5]đều có tổng bằng 8.
- Đầu vào
- nums = [3, 0, 4]k = 3
- Đầu ra
- 4
- Giải thích
- Ba giá trị và ba phần tạo thành mỗi phần một giá trị, với tổng lần lượt là 3, 0 và 4. Một phần có tổng bằng 0 vẫn hợp lệ.
Câu hỏi mở rộng
Với tổng tiền tố, một phép kiểm tra tham lam có thể tìm điểm kết thúc của mỗi phần bằng tìm kiếm nhị phân. Phương pháp này chạy nhanh đến mức nào khi k nhỏ?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Giả sử có người hứa rằng phần lớn nhất có thể có tổng tối đa là
c. Bạn có thể nhanh chóng xác định xemkphần có đủ không?Điền các phần từ trái sang phải và chỉ đóng một phần khi giá trị tiếp theo khiến tổng vượt quá
c. Cách này dùng ít phần nhất, vàccàng lớn thì số phần cần dùng không bao giờ nhiều hơn.Tìm kiếm nhị phân giá trị
ctrong khoảng từ giá trị lớn nhất đến tổng các giá trị. Nếu số lượng theo cách tham lam không vượt quák, đáp án làchoặc nhỏ hơn; nếu không, đáp án lớn hơn.
Lời giải
Tìm trực tiếp cách chia tốt nhất tốn O(k·n²) ngay cả khi dùng quy hoạch động. Hãy đổi cách đặt câu hỏi: liệu k phần có thể nằm trong một giới hạn cho trước không? Một lượt tham lam sẽ trả lời câu hỏi đó. Khi giới hạn tăng, câu trả lời sẽ chuyển từ không sang có đúng một lần, và tìm kiếm nhị phân sẽ tìm ra điểm chuyển đổi.
Lập trình động trên các tiền tố
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Viết best[p][j] để biểu thị chi phí nhỏ nhất khi chia j giá trị đầu tiên thành p phần. Nếu phần cuối là nums[i..j-1], chi phí là giá trị lớn hơn giữa best[p-1][i] và tổng của phần đó, còn cách chia tốt nhất cho i giá trị đầu tiên không phụ thuộc vào bất kỳ giá trị nào ở bên phải nó. Vì vậy, best[p][j] = min over i of max(best[p-1][i], prefix[j] - prefix[i]), với i từ p-1 đến j-1 để mỗi phần đều chứa một giá trị. Hàng p chỉ đọc hàng p-1, nên chỉ cần hai hàng.
Thuật toán chạy tối đa k·n²/2 bước: khoảng 1.8 × 10^10 bước với n = 5000 và k = 2500.
Thuật toán
- Tạo
prefix, trong đóprefix[j]là tổng củajgiá trị đầu tiên, và đặtbest[j] = prefix[j]cho một phần. - Với mỗi số phần
ptừ 2 đếnk, và mỗi điểm kết thúcjtừpđếnn, lấy giá trị nhỏ nhất củamax(best[i], prefix[j] - prefix[i])vớiitừp-1đếnj-1. - Lưu các giá trị nhỏ nhất đó vào một hàng mới và đặt hàng đó làm
best. - Trả về
best[n].
def splitArray(nums, k):
n = len(nums)
# prefix[j] is the sum of the first j values
prefix = [0] * (n + 1)
for i, x in enumerate(nums):
prefix[i + 1] = prefix[i] + x
# best[j]: the smallest largest part when the first j values form one part
best = prefix[:]
for parts in range(2, k + 1):
nxt = [0] * (n + 1)
for j in range(parts, n + 1):
lowest = prefix[j] # never worse than one part holding everything
for i in range(parts - 1, j):
# the first i values form parts-1 parts, nums[i..j-1] is the last part
worst = max(best[i], prefix[j] - prefix[i])
if worst < lowest:
lowest = worst
nxt[j] = lowest
best = nxt
return best[n]Tìm kiếm nhị phân trên tổng lớn nhất
Ý tưởng
Để kiểm tra giới hạn c, duyệt từ trái sang phải và thêm các giá trị vào phần hiện tại khi tổng của phần đó vẫn không vượt quá c, rồi bắt đầu phần mới. Cách này dùng số phần ít nhất có thể. So với bất kỳ cách chia hợp lệ nào, phần đầu tiên của cách tham lam kết thúc ở vị trí ít nhất cũng xa bằng, nên phần tiếp theo của nó bắt đầu ở vị trí bằng hoặc sau phần tiếp theo của cách kia. Các giá trị đến hết phần đó tạo thành một đoạn của một phần vừa giới hạn, và khi không có giá trị âm, tổng của một đoạn không bao giờ lớn hơn tổng của cả phần. Vì vậy, cách tham lam không bao giờ bị tụt lại.
Dùng ít hơn k phần cũng tốt như dùng k phần: chia một phần có từ hai giá trị trở lên thành hai phần không bao giờ làm tăng tổng. Vì vậy, phép kiểm tra là partsNeeded(c) ≤ k.
Dùng tìm kiếm nhị phân để tìm giới hạn nhỏ nhất thỏa mãn, trong khoảng từ max(nums), vì giá trị đó thuộc một phần, đến sum(nums), vốn luôn vừa trong một phần.
Thuật toán
- Đặt
lo = max(nums)vàhi = sum(nums). - Trong khi
lo < hi, lấymid = lo + (hi - lo) / 2. - Đếm số phần tham lam với giới hạn
mid: bắt đầu với 1 phần có tổng bằng 0; khi thêm một giá trị khiến tổng vượt quámid, thêm một phần và bắt đầu lại tổng bằng giá trị đó. - Nếu số phần không vượt quá
k, đặthi = mid; nếu không, đặtlo = mid + 1. - Trả về
lo.
def splitArray(nums, k):
def parts_needed(cap):
# Fill each part left to right and start a new one only when the next value would pass cap.
parts, current = 1, 0
for x in nums:
if current + x > cap:
parts += 1
current = x
else:
current += x
return parts
lo, hi = max(nums), sum(nums) # one value per part at best, everything in one part at worst
while lo < hi:
mid = (lo + hi) // 2
if parts_needed(mid) <= k:
hi = mid # mid works, so the answer is mid or smaller
else:
lo = mid + 1 # mid needs more than k parts, so the answer is larger
return lo
Lỗi thường gặp và trường hợp biên
- Bắt đầu
lonhỏ hơnmax(nums). Thuật toán tham lam đặt một giá trị lớn hơn giới hạn vào một phần riêng, nên nó cho rằng giới hạn 5 vẫn ổn với[1, 9]khik = 2. - Kiểm tra
partsNeeded(c) == k. Với[3, 0, 4]vàk = 3, giới hạn 4 chia thành[3, 0],[4], nên không có giới hạn nào đạt yêu cầu. - Đếm số phần bắt đầu từ 0, khiến mọi kết quả đếm đều nhỏ hơn 1.
- Đặt
hi = mid - 1khimidthỏa mãn, khiến ta bỏ lỡ đáp án nếu nó chính làmid. - Bắt đầu
icủa quy hoạch động từ 0. Ô có ít giá trị hơn số phần sẽ được tính là chi phí 0, và với[100, 1, 1]cùngk = 3, kết quả là 2 thay vì 100.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán Split Array Largest Sum là gì?
Độ phức tạp thời gian O(n log S) và độ phức tạp không gian phụ thêm O(1), trong đó S là tổng của nums: mỗi lần kiểm tra tham lam chỉ cần duyệt qua một lượt, và khoảng giới hạn giảm một nửa sau mỗi lần kiểm tra. Có khoảng 29 lần kiểm tra khi S = 5 × 10^8. Quy hoạch động cần thời gian O(k·n²).
Tại sao bước kiểm tra tính khả thi có tính đơn điệu?
Nếu tổng của từng phần sau khi chia không vượt quá c, thì cách chia đó cũng phù hợp với c+1. Vì vậy, khi một giới hạn phù hợp, mọi giới hạn lớn hơn đều phù hợp, và các câu trả lời tạo thành một dãy gồm các câu trả lời không, rồi đến các câu trả lời có. Tìm kiếm nhị phân tìm câu trả lời có đầu tiên.
Tìm kiếm nhị phân có hoạt động với số âm không?
Không. Giá trị âm có thể làm giảm tổng, nên thuật toán tham lam có thể kết thúc một phần quá sớm, còn việc chia một phần có thể khiến một mảnh có giá trị lớn hơn cả phần ban đầu. Quy hoạch động không dựa trên giả định nào trong số đó nên vẫn cho kết quả chính xác.
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 splitArray(nums, k):
# Viết code tại đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
nums = [6, 2, 9, 4, 7, 3] k = 3
Đầu ra mong đợi
13