Fractional Knapsack
Bạn có n món đồ. Món đồ i có giá trị values[i] và cân nặng weights[i]. Túi của bạn chứa được tổng khối lượng tối đa là capacity, và bạn có thể lấy một phần bất kỳ của món đồ: phần món đồ i nặng w có giá trị là values[i] × w / weights[i].
Viết một hàm có tên fractionalKnapsack để trả về tổng giá trị lớn nhất mà túi có thể chứa, làm tròn xuống thành số nguyên.
Hàm
- valuesinteger-array
- giá trị của từng phần tử
- weightsinteger-array
- trọng lượng của mỗi món đồ
- capacityinteger
- trọng lượng tối đa mà túi có thể chứa
- Trả vềinteger
- tổng giá trị lớn nhất có thể chứa vừa trong túi, làm tròn xuống
Ràng buộc
1 ≤ n ≤ 5000, trong đón = values.length = weights.length1 ≤ values[i], weights[i] ≤ 1041 ≤ capacity ≤ 108
Ví dụ
- Đầu vào
- values = [40, 90, 30, 50]weights = [8, 30, 5, 20]capacity = 40
- Đầu ra
- 151
- Giải thích
- Mỗi đơn vị trọng lượng, các vật phẩm có giá trị lần lượt là 5, 3, 6 và 2.5. Vật phẩm 2 và 0 được lấy nguyên vẹn, tổng giá trị là 70 và còn chỗ cho 27 đơn vị. Lấy thêm 27 trong số 30 đơn vị của vật phẩm 1, được thêm 81, tổng cộng là 151.
- Đầu vào
- values = [10, 7]weights = [4, 3]capacity = 5
- Đầu ra
- 12
- Giải thích
- Mỗi đơn vị của vật phẩm 0 có giá trị 2.5, còn vật phẩm 1 có giá trị khoảng 2.33, nên lấy trọn vật phẩm 0 với giá trị 10. Một trong 3 đơn vị của vật phẩm 1 cộng thêm
7 / 3, và 12 cùng một phần ba được làm tròn xuống thành 12.
- Đầu vào
- values = [5, 8]weights = [2, 3]capacity = 100
- Đầu ra
- 13
- Giải thích
- Cả hai món đều vừa đủ chỗ, nên bạn lấy tất cả: 5 + 8 = 13.
Câu hỏi mở rộng
Bạn có thể tìm đáp án trong thời gian O(n) mà không cần sắp xếp tất cả các mục không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Nếu chiếc túi còn đủ chỗ cho thêm một đơn vị trọng lượng, bạn sẽ lấy đơn vị đó từ món đồ nào?
Một đơn vị khối lượng có giá trị nhất khi thuộc về món đồ có giá trị trên mỗi đơn vị khối lượng cao nhất. Hãy cho món đồ có mật độ cao nhất vào túi trước, rồi đến các món có mật độ thấp hơn; chỉ món cuối cùng bạn chọn mới bị cắt bớt.
Sắp xếp các món đồ theo mật độ giảm dần, so sánh
values[a] × weights[b]vớivalues[b] × weights[a]để không cần phép chia. Lấy nguyên món đồ khi chúng còn vừa, sau đó cộngvalues[i] × room / weights[i]với phép chia số nguyên cho món đồ lấp đầy ba lô.
Lời giải
Vì bạn có thể cắt các món đồ, bài toán này dùng thuật toán tham lam, khác với bài toán ba lô 0/1: mỗi đơn vị trọng lượng nên lấy từ món đồ còn lại có giá trị trên mỗi đơn vị cao nhất. Hãy sắp xếp theo mật độ giá trị đó rồi lần lượt cho đồ vào túi. So sánh mật độ bằng cách nhân chéo và chỉ làm tròn xuống với món đồ bạn cắt, để không bao giờ phải lưu phân số.
Chọn vật phẩm có mật độ cao nhất ở mỗi lượt
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Đổ đầy túi theo thứ tự từ món có mật độ cao nhất trở xuống: lấy cả món nếu món đó vừa túi, nếu không thì lấy phần vừa khít túi rồi dừng lại. Tìm món chưa dùng có mật độ cao nhất bằng cách quét tất cả các món, so sánh values[i] × weights[j] với values[j] × weights[i] thay vì chia. Với trọng lượng dương, phép kiểm tra này cho kết quả tương tự, và cả hai tích đều không vượt quá 10^8.
Chọn món có mật độ cao nhất trước là tốt nhất: nếu túi đang chứa một phần trọng lượng của món có mật độ thấp hơn trong khi chưa lấy hết món có mật độ cao hơn, đổi phần trọng lượng đó lấy món có mật độ cao hơn sẽ không làm giảm giá trị. Tiếp tục đổi, bạn sẽ đạt được cách xếp túi theo thứ tự mật độ giảm dần.
Tối đa n vòng lặp, mỗi vòng quét các món chưa dùng, tương đương khoảng n²/2 = 12.5 triệu phép so sánh với n = 5000.
Thuật toán
- Đánh dấu tất cả các phần tử là chưa dùng, đặt
room = capacityvàtotal = 0. - Trong khi vẫn còn chỗ, quét các phần tử chưa dùng để tìm phần tử có mật độ cao nhất, bằng cách so sánh mật độ qua phép nhân chéo.
- Nếu không còn phần tử nào, hãy dừng lại. Nếu không, đánh dấu phần tử đó là đã dùng.
- Nếu phần tử đó vừa với sức chứa, cộng giá trị của nó vào
totalvà trừ trọng lượng của nó khỏiroom. Nếu không, cộngvalues[i] × room / weights[i]bằng phép chia lấy phần nguyên rồi dừng lại. - Trả về
total.
def fractionalKnapsack(values, weights, capacity):
n = len(values)
used = [False] * n
total = 0
room = capacity
while room > 0:
best = -1
for i in range(n):
# i is denser than best: values[i] / weights[i] > values[best] / weights[best], cross-multiplied
if not used[i] and (best == -1 or values[i] * weights[best] > values[best] * weights[i]):
best = i
if best == -1:
break # every item is already in the bag
used[best] = True
if weights[best] <= room:
total += values[best]
room -= weights[best]
else:
total += values[best] * room // weights[best] # the part that fits, rounded down
room = 0
return totalSắp xếp theo giá trị trên mỗi đơn vị trọng lượng
Ý tưởng
Quá trình quét liên tục tìm món đồ có mật độ cao nhất, nhưng thứ tự không thay đổi qua các lượt. Chỉ cần sắp xếp các món đồ một lần theo thứ tự mật độ giảm dần, dùng cùng cách so sánh nhân chéo. Các món đồ có mật độ bằng nhau có thể xếp theo thứ tự nào cũng được, vì giá trị trên mỗi đơn vị của chúng như nhau.
Lấy trọn món đồ khi còn đủ sức chứa. Món đầu tiên không vừa là món duy nhất bạn lấy một phần: món đó cộng thêm values[i] × room / weights[i], và phép chia số nguyên làm tròn xuống. Mọi món trước đó đều được lấy trọn, nên làm tròn số hạng này sẽ cho kết quả chính xác. Việc sắp xếp tốn O(n log n), còn quá trình duyệt tốn O(n).
Thuật toán
- Tạo danh sách chỉ số của các vật phẩm, rồi sắp xếp theo mật độ giảm dần bằng phép so sánh nhân chéo.
- Đặt
room = capacityvàtotal = 0. - Với mỗi vật phẩm theo thứ tự đó, nếu
weights[i] ≤ room, hãy cộng giá trị của vật phẩm vào tổng và trừ khối lượng của vật phẩm khỏiroom. - Nếu không, hãy cộng
values[i] × room / weights[i]bằng phép chia lấy phần nguyên rồi dừng lại. - Trả về
total.
from functools import cmp_to_key
def fractionalKnapsack(values, weights, capacity):
# a goes before b when values[a] / weights[a] > values[b] / weights[b],
# tested as values[a] * weights[b] > values[b] * weights[a] to stay in whole numbers.
def denser_first(a, b):
return values[b] * weights[a] - values[a] * weights[b]
order = sorted(range(len(values)), key=cmp_to_key(denser_first))
total = 0
room = capacity
for i in order:
if weights[i] <= room:
total += values[i] # the whole item fits
room -= weights[i]
else:
total += values[i] * room // weights[i] # the part that fits, rounded down
break
return total
Lỗi thường gặp và trường hợp biên
- Sắp xếp chỉ theo giá trị hoặc trọng lượng. Trong ví dụ đầu tiên, vật phẩm có giá trị 90 chỉ đứng thứ ba về mật độ. Nếu chọn vật phẩm đó trước, bạn chỉ còn chỗ cho một nửa vật phẩm có giá trị 50, tổng cộng là 115 thay vì 151.
- Chia trước rồi mới nhân. Với giá trị 13, trọng lượng 7 và sức chứa còn lại là 3, phép tính số nguyên
13 / 7 * 3cho kết quả 3, còn13 * 3 / 7cho kết quả đúng là 5. - Cho rằng lúc nào cũng phải cắt một vật phẩm. Trong ví dụ thứ ba, mọi vật phẩm đều vừa với sức chứa, nên đáp án là tổng giá trị của tất cả vật phẩm, bằng 13.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán FractionalKnapsack là bao nhiêu?
Sắp xếp (sorting) các mục theo giá trị trên mỗi đơn vị trọng lượng tốn O(n log n), còn việc lấp đầy chỉ cần một lượt, nên tổng thời gian là O(n log n) và cần O(n) bộ nhớ để lưu thứ tự. Quét tìm mục có mật độ cao nhất sau mỗi lượt tốn O(n²).
Vì sao thuật toán tham lam hiệu quả với bài toán ba lô phân số nhưng không hiệu quả với bài toán ba lô 0/1?
Việc chia nhỏ cho phép bạn đổi trọng lượng từ một món đồ có mật độ thấp hơn sang món có mật độ cao hơn. Nếu không chia nhỏ, một món đồ có mật độ cao có thể để lại khoảng trống mà không món nào khác lấp đầy. Với các giá trị 8, 11, 15, trọng lượng 2, 3, 4 và sức chứa 7, chọn món có mật độ cao nhất trước sẽ lấy các trọng lượng 2 và 4, được 23, trong khi các trọng lượng 3 và 4 cho kết quả 26. Phiên bản 0/1 cần quy hoạch động.
Làm thế nào để giải bài toán FractionalKnapsack mà không dùng số dấu phẩy động?
Với trọng số dương, a / b > c / d tương đương với phép kiểm tra a × d > c × b, vì vậy hãy so sánh mật độ bằng cách nhân chéo. Giá trị tốt nhất gồm một số nguyên cộng với một phân số, values[i] × room / weights[i] cho vật phẩm bị cắt, nên một phép chia số nguyên sẽ cho kết quả làm tròn xuống 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 fractionalKnapsack(values, weights, capacity):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
values = [40, 90, 30, 50] weights = [8, 30, 5, 20] capacity = 40
Đầu ra mong đợi
151