Target 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 target. Đặt dấu cộng hoặc dấu trừ trước mỗi số, giữ nguyên thứ tự rồi tính giá trị biểu thức. Hãy trả về số cách chọn dấu để biểu thức bằng target. Mỗi cách trong 2^n cách đều được tính riêng, vì vậy +0 và -0 là hai cách chọn khác nhau.
Hàm
- numsinteger-array
- các số, mỗi số đều có một dấu
- targetinteger
- giá trị mà biểu thức phải bằng
- Trả vềinteger
- số lựa chọn dấu mà biểu thức của chúng bằng giá trị mục tiêu
Ràng buộc
1 ≤ nums.length ≤ 200 ≤ nums[i] ≤ 1000- Tổng của
numstối đa là 1000. -1000 ≤ target ≤ 1000- Đáp án tối đa là 220, nằm gọn trong phạm vi số nguyên có dấu 32 bit.
Ví dụ
- Đầu vào
- nums = [2, 1, 1]target = 2
- Đầu ra
- 2
- Giải thích
+2 +1 -1và+2 -1 +1đều bằng 2. Nếu đặt dấu trừ trước số 2, giá trị lớn nhất bạn có thể đạt được là -2 + 1 + 1 = 0, nên không có lựa chọn nào khác phù hợp.
- Đầu vào
- nums = [0, 4, 4]target = 0
- Đầu ra
- 4
- Giải thích
- Hai số 4 cần có dấu trái ngược nhau,
+4 -4hoặc-4 +4, và trong cả hai trường hợp, số 0 có thể mang dấu nào cũng được. Như vậy có 2 × 2 = 4 cách chọn.
- Đầu vào
- nums = [6, 3]target = 4
- Đầu ra
- 0
- Giải thích
- Có bốn giá trị có thể là 9, 3, -3 và -9. Đổi dấu một giá trị làm tổng thay đổi gấp đôi giá trị đó, vì vậy mọi kết quả đều là số lẻ, chẳng hạn 6 + 3 = 9, và không bao giờ đạt được mục tiêu chẵn là 4.
Câu hỏi mở rộng
Bạn có thể xử lý tối đa 40 số có giá trị lên đến 10^9 khi không thể lưu trong bộ nhớ một mảng có chỉ số theo tổng không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Bạn có thể thử cả hai dấu cho mỗi số, tức là có
2^nbiểu thức. Hãy chú ý rằng nhiều tiền tố dấu khác nhau dẫn đến cùng một tổng hiện tại, và từ đó trở đi, chúng hoạt động giống hệt nhau.Gọi
Plà tổng các số được cộng vàNlà tổng các số bị trừ. Bạn biếtP - N = targetvàP + N = sum(nums). Điều đó cho bạn biết gì vềP?P = (sum + target) / 2, vậy hãy đếm số tập con củanumscó tổng bằng giá trị đó. Dùngways[s]để lưu số tập con có tổng bằngs, rồi cập nhật nó cho từng số, duyệtstừ lớn xuống nhỏ. Trả về 0 ngay nếusum + targetlà số lẻ hoặc|target| > sum.
Lời giải
Mỗi số có hai dấu, nên có 2^n biểu thức. Đếm theo vị trí và tổng lũy kế giúp gộp các tiền tố dấu có cùng tổng. Tốt hơn nữa, các số mang dấu cộng tạo thành một tập con có tổng cố định, vì vậy bài toán trở thành đếm các tập con có tổng cho trước, với thời gian O(n × S), trong đó S là tổng của nums.
Hãy thử cả hai dấu với mỗi số
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Duyệt các số từ trái sang phải, giữ tổng đang tính, rồi rẽ nhánh hai lần tại mỗi số: cộng hoặc trừ số đó. Khi mọi số đều có dấu, đếm 1 nếu tổng bằng target. Mỗi lựa chọn dấu tương ứng với đúng một đường đi, vì vậy cộng các số đếm lại sẽ ra đáp án.
Mỗi lựa chọn tạo ra một nút lá: 2^20 ≈ 10^6 nút lá với 20 số, và mỗi số thêm vào sẽ làm khối lượng công việc tăng gấp đôi.
Thuật toán
- Viết
count(i, total), số cách gán dấu chonums[i..]khi các số trướciđã có tổng bằngtotal. - Nếu
i == n, trả về 1 khitotal == target, nếu không thì trả về 0. - Nếu không, trả về
count(i+1, total + nums[i]) + count(i+1, total - nums[i]). - Đáp án là
count(0, 0).
def findTargetSumWays(nums, target):
n = len(nums)
def count(i, total):
# Ways to sign nums[i:] so that the whole expression ends at target
if i == n:
return 1 if total == target else 0
return count(i + 1, total + nums[i]) + count(i + 1, total - nums[i])
return count(0, 0)Ghi nhớ theo vị trí và tổng đang tính
Ý tưởng
count(i, total) chỉ phụ thuộc vào i và total, không phụ thuộc vào các dấu đã tạo ra tổng đó. Với nums = [1, 1, 1, 1], các tiền tố +1 -1 và -1 +1 đều đạt tổng 0 tại vị trí 2, nên tìm kiếm từ đó giống nhau. Hãy lưu mỗi kết quả ngay lần đầu tiên và dùng lại.
Tổng đang tính luôn nằm trong khoảng từ -S đến S, nên hãy dịch tổng đi S để dùng làm chỉ số: memo[i][total + S]. Có nhiều nhất n × (2S+1) trạng thái, khoảng 4 × 10^4 với 20 số có tổng là 1000, mỗi trạng thái chỉ cần một lượng thao tác không đổi.
Thuật toán
- Tính
S = sum(nums)và tạo một bảng ghi nhớn × (2S+1)được điền giá trị -1. - Trong
count(i, total), trả về trường hợp cơ sở tạii == nnhư trước. - Nếu
memo[i][total + S]là -1, điền vào đó giá trịcount(i+1, total + nums[i]) + count(i+1, total - nums[i]). - Trả về giá trị đã lưu và gọi
count(0, 0).
def findTargetSumWays(nums, target):
n = len(nums)
whole = sum(nums)
# memo[i][total + whole]: ways for nums[i:] from this running total, -1 while unknown
memo = [[-1] * (2 * whole + 1) for _ in range(n)]
def count(i, total):
if i == n:
return 1 if total == target else 0
if memo[i][total + whole] == -1:
memo[i][total + whole] = count(i + 1, total + nums[i]) + count(i + 1, total - nums[i])
return memo[i][total + whole]
return count(0, 0)Đếm số tập con có tổng bằng (S + target) / 2
Ý tưởng
Gọi P là tổng các số mang dấu cộng và N là tổng các số còn lại. Khi đó P - N = target và P + N = S, nên 2P = S + target. Một cách chọn dấu hợp lệ khi và chỉ khi nhóm mang dấu cộng có tổng bằng (S + target) / 2, và mỗi tập con như vậy tương ứng với một cách chọn dấu.
ways[s] đếm số tập con của các số đã xét có tổng bằng s, bắt đầu với ways[0] = 1. Với mỗi số x, một tập con có tổng bằng s có thể bỏ qua x hoặc thêm số đó vào một tập con có tổng bằng s - x. Duyệt s từ lớn xuống nhỏ, để ways[s - x] vẫn giữ giá trị trước khi xét x và không số nào được dùng hai lần. Một số 0 cộng mỗi mục vào chính nó, nhân đôi mọi số đếm: tương ứng với hai cách chọn dấu cho số 0. Đó là n × goal phép cộng, nhiều nhất là 2 × 10^4.
Thuật toán
- Tính
S = sum(nums). Nếu|target| > ShoặcS + targetlà số lẻ, trả về 0. - Đặt
goal = (S + target) / 2, tạowaysgồmgoal + 1số 0 và đặtways[0] = 1. - Với mỗi
xtrongnums, duyệtstừgoalgiảm dần đếnx, cộngways[s - x]vàoways[s]. - Trả về
ways[goal].
def findTargetSumWays(nums, target):
whole = sum(nums)
# plus - minus = target and plus + minus = whole, so plus = (whole + target) / 2
if abs(target) > whole or (whole + target) % 2 == 1:
return 0
goal = (whole + target) // 2
# ways[s]: how many subsets of the values seen so far add up to s
ways = [0] * (goal + 1)
ways[0] = 1
for x in nums:
# From the top down, so each value is used at most once
for s in range(goal, x - 1, -1):
ways[s] += ways[s - x]
return ways[goal]
Lỗi thường gặp và trường hợp biên
- Bỏ qua bước kiểm tra tính chẵn lẻ. Với
[6, 3]và mục tiêu 4,(9 + 4) / 2làm tròn xuống thành 6, và tập con{6}cho kết quả 1 thay vì 0. - Bỏ qua bước kiểm tra phạm vi. Với
[1, 2, 3]và mục tiêu -10,(6 - 10) / 2 = -2, dẫn đến kích thước mảng âm. - Điền
waystừ dưới lên. Khi đó, một số sẽ được thêm vào tập con vốn đã sử dụng số đó:[1, 3]với mục tiêu 2 cho kết quả 2 thay vì 1. - Coi số 0 là một lựa chọn.
[0, 0, 0, 1, 0, 0]với mục tiêu -1 có 32 đáp án, mỗi đáp án ứng với một cách gán dấu cho năm số 0. - Dùng tổng đang tính, vốn có thể âm, làm chỉ số cho bảng ghi nhớ. Hãy dịch mọi tổng đi
S, hoặc dùng bản đồ băm.
Câu hỏi thường gặp3
Target Sum liên quan thế nào đến bài toán tổng tập con?
Các số có dấu cộng phải có tổng bằng (S + target) / 2, vì vậy đáp án là số lượng tập con của nums có tổng đó. Bất kỳ quy hoạch động nào đếm số tập con có tổng bằng giá trị cần tìm đều giải được bài toán này khi bạn bổ sung hai điều kiện kiểm tra: giá trị phải là số nguyên và |target| không được vượt quá S.
Độ phức tạp thời gian của Target Sum là gì?
Quy hoạch động đếm tập hợp con chạy trong thời gian O(n × S) và dùng bộ nhớ O(S), trong đó S là tổng của nums. Đệ quy có ghi nhớ có cùng độ phức tạp thời gian nhưng dùng bảng n × (2S+1). Thử mọi dấu có độ phức tạp O(2^n).
Có thể giải bài toán tổng mục tiêu bằng một từ điển băm lưu các tổng đang chạy không?
Có. Ánh xạ mỗi tổng đang chạy có thể đạt được với số lượng tiền tố dấu tạo ra tổng đó, bắt đầu bằng {0: 1}, rồi tạo ánh xạ tiếp theo từ total + x và total - x cho mỗi số. Đáp án là số lượng tại target. Cách này tương đương với đệ quy có ghi nhớ, chạy theo chiều thuận và có cùng thời gian O(n × S).
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 findTargetSumWays(nums, target):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
nums = [2, 1, 1] target = 2
Đầu ra mong đợi
2