Knapsack Part 2
3.1 Bài toán cái túi với số lượng vô hạn
Bài toán cái túi với số lượng vô hạn: Có loại vật phẩm và một cái túi có trọng lượng tối đa là . Vật phẩm thứ có trọng lượng là và giá trị là , số lượng mỗi loại vật phẩm không có giới hạn. Hỏi trong trường hợp tổng trọng lượng không vượt quá trọng lượng tối đa của cái túi, giá trị lớn nhất có thể đạt được là bao nhiêu?
3.1 Ý tưởng của bài toán cái túi với số lượng vô hạn
Đặc điểm của bài toán cái túi với số lượng vô hạn: Mỗi loại vật phẩm có số lượng không giới hạn.
Chúng ta có thể tham khảo định nghĩa trạng thái và ý tưởng cơ bản của "bài toán cái túi 0-1" để giải quyết vấn đề cái túi với số lượng vô hạn. Với một cái túi có trọng lượng là , tối đa có thể chứa vật phẩm thứ . Vì vậy, chúng ta có thể thêm một vòng lặp để duyệt qua số lượng vật phẩm thứ có thể chọn (từ đến ), từ đó chuyển đổi "bài toán cái túi với số lượng vô hạn" thành "bài toán cái túi 0-1".
Ý tưởng 1: Quy hoạch động + duyệt cơ bản hai chiều
1. Phân đoạn
Phân đoạn theo số thứ tự của vật phẩm và trọng lượng tối đa của cái túi.
2. Định nghĩa trạng thái
Định nghĩa trạng thái là: Đặt loại vật phẩm đầu tiên vào một cái túi có trọng lượng tối đa là , có thể đạt được giá trị lớn nhất.
Trạng thái là một mảng hai chiều, trong đó chiều thứ nhất đại diện cho "loại vật phẩm đang xem xét hiện tại", chiều thứ hai đại diện cho "trọng lượng tối đa của cái túi", giá trị trong mảng hai chiều đại diện cho "giá trị lớn nhất có thể đạt được".
3. Công thức chuyển trạng thái
Vì mỗi loại vật phẩm có thể chọn số lượng không giới hạn, nên trạng thái có thể chọn giá trị lớn nhất từ các lựa chọn sau:
- Không chọn bất kỳ vật phẩm thứ nào: Giá trị lớn nhất có thể đạt được là .
- Chọn vật phẩm thứ : Giá trị lớn nhất có thể đạt được là .
- Chọn vật phẩm thứ : Giá trị lớn nhất có thể đạt được là .
- ……
- Chọn vật phẩm thứ : Giá trị lớn nhất có thể đạt được là .
Lưu ý: Điều kiện để chọn vật phẩm thứ là .
Do đó, công thức chuyển trạng thái là:
.
4. Điều kiện ban đầu
- Nếu trọng lượng tối đa của cái túi là , thì bất kể chọn vật phẩm nào, giá trị lớn nhất có thể đạt được sẽ luôn là , tức là .
- Bất kể trọng lượng tối đa của cái túi là bao nhiêu, giá trị lớn nhất có thể đạt được từ loại vật phẩm đầu tiên sẽ luôn là , tức là .
5. Kết quả cuối cùng
Dựa vào định nghĩa trạng thái đã định nghĩa trước đó, đại diện cho việc đặt loại vật phẩm đầu tiên vào một cái túi có trọng lượng tối đa là , để đạt được giá trị lớn nhất. Vậy kết quả cuối cùng là , trong đó là số lượng loại vật phẩm và là trọng lượng tối đa của cái túi.
Ý tưởng 1: Code
class Solution:
# Ý tưởng 1: Quy hoạch động + ý tưởng cơ bản hai chiều
def completePackMethod1(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)]
# Duyệt qua từng loại vật phẩm
for i in range(1, size + 1):
# Duyệt qua trọng lượng tối đa của cái túi
for w in range(W + 1):
# Duyệt qua số lượng vật phẩm thứ i - 1 có thể chọn
for k in range(w // weight[i - 1] + 1):
# dp[i][w] lấy giá trị lớn nhất từ tất cả dp[i - 1][w - k * weight[i - 1] + k * value[i - 1] có thể chọn
dp[i][w] = max(dp[i][w], dp[i - 1][w - k * weight[i - 1]] + k * value[i - 1])
return dp[size][W]
Ý tưởng 1: Phân tích độ phức tạp
- Độ phức tạp thời gian: , trong đó là số lượng loại vật phẩm, là trọng lượng tối đa của cái túi, là trọng lượng của vật phẩm thứ .
- Độ phức tạp không gian: .
3.2 Tối ưu công thức chuyển trạng thái của bài toán cái túi với số lượng vô hạn
Trong phương pháp trước đó, đối với mỗi loại vật phẩm, chúng ta phải duyệt qua tất cả các số lượng vật phẩm khả thi , điều này làm tăng đáng kể độ phức tạp thời gian của thuật toán.
Trong thực tế, chúng ta có thể tối ưu công thức chuyển trạng thái trước đó để giảm thiểu độ phức tạp thời gian của thuật toán.
Chúng ta sẽ triển khai công thức chuyển trạng thái trước đó:
Và chúng ta sẽ triển khai công thức chuyển trạng thái sau khi mở rộng:
Và với chúng ta có:
Và chúng ta có thể quan sát được:
- Công thức có tổng cộng mục, công thức có tổng cộng mục.
- Công thức và toàn bộ công thức khác nhau chính xác một giá trị .
Do đó, chúng ta có thể thêm vào công thức , sau đó thay thế vào công thức , và ta sẽ có công thức chuyển trạng thái sau khi tối ưu hóa:
Công thức chuyển trạng thái sau khi tối ưu hóa đã loại bỏ sự phụ thuộc vào số lượng vật phẩm, do đó không cần duyệt qua nữa, giảm ba vòng lặp xuống còn hai vòng lặp.
Lưu ý: Công thức thỏa mãn điều kiện . Khi , .
Do đó, công thức chuyển trạng thái là:
Dựa vào công thức chuyển trạng thái trên, chúng ta có thể thấy rằng công thức này rất giống với công thức chuyển trạng thái của bài toán cái túi 0-1 trong "Ý tưởng 1".
Điểm khác biệt duy nhất là:
- Trạng thái trong bài toán cái túi 0-1 là , đây là trạng thái ở giai đoạn thứ .
- Trạng thái trong bài toán cái túi với số lượng vô hạn là , đây là trạng thái ở giai đoạn thứ .
Ý tưởng 2: Quy hoạch động + Tối ưu công thức chuyển trạng thái
1. Phân đoạn
Phân đoạn theo số thứ tự của vật phẩm và trọng lượng tối đa của cái túi.
2. Định nghĩa trạng thái
Định nghĩa trạng thái là: Đặt loại vật phẩm đầu tiên vào một cái túi có trọng lượng tối đa là , có thể đạt được giá trị lớn nhất.
Trạng thái là một mảng hai chiều, trong đó chiều thứ nhất đại diện cho "loại vật phẩm đang xem xét hiện tại", chiều thứ hai đại diện cho "trọng lượng tối đa của cái túi", giá trị trong mảng hai chiều đại diện cho "giá trị lớn nhất có thể đạt được".
3. Công thức chuyển trạng thái
4. Điều kiện ban đầu
- Nếu trọng lượng tối đa của cái túi là , thì bất kể chọn vật phẩm nào, giá trị lớn nhất có thể đạt được sẽ luôn là , tức là .
- Bất kể trọng lượng tối đa của cái túi là bao nhiêu, giá trị lớn nhất có thể đạt được từ loại vật phẩm đầu tiên sẽ luôn là , tức là .
5. Kết quả cuối cùng
Dựa vào định nghĩa trạng thái đã định nghĩa trước đó, đại diện cho việc đặt loại vật phẩm đầu tiên vào một cái túi có trọng lượng tối đa là , để đạt được giá trị lớn nhất. Vậy kết quả cuối cùng là , trong đó là số lượng loại vật phẩm và là trọng lượng tối đa của cái túi.
Ý tưởng 2: Code
class Solution:
# Ý tưởng 2: Quy hoạch động + Tối ưu công thức chuyển trạng thái
def completePackMethod2(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)]
# Duyệt qua từng loại vật phẩm
for i in range(1, size + 1):
# Duyệt qua trọng lượng tối đa của cái túi
for w in range(W + 1):
# Vật phẩm thứ i - 1 không thể chọn
if w < weight[i - 1]:
# dp[i][w] lấy giá trị lớn nhất từ tất cả dp[i - 1][w] có thể chọn
dp[i][w] = dp[i - 1][w]
else:
# dp[i][w] lấy giá trị lớn nhất từ tất cả dp[i - 1][w] và dp[i][w - weight[i - 1]] + value[i - 1] có thể chọn
dp[i][w] = max(dp[i - 1][w], dp[i][w - weight[i - 1]] + value[i - 1])
return dp[size][W]
Ý tưởng 2: Phân tích độ phức tạp
- Độ phức tạp thời gian: , trong đó là số lượng loại vật phẩm, là trọng lượng tối đa của cái túi.
- Độ phức tạp không gian: .
3.3 Tối ưu hóa mảng trượt cho bài toán cái túi đầy đủ
Bằng cách quan sát phương trình chuyển trạng thái trong "Ý tưởng 2":
Chúng ta có thể thấy rằng chúng ta chỉ cần sử dụng hàng hiện tại (hàng ) của , , và hàng trước đó (hàng ) của .
Do đó, chúng ta không cần lưu trữ tất cả các giai đoạn của trạng thái. Chúng ta chỉ cần sử dụng một mảng một chiều để lưu trữ các trạng thái của giai đoạn trước đó và tối ưu hóa không gian bằng cách sử dụng kỹ thuật "mảng trượt" (loại bỏ chiều đầu tiên của trạng thái lập trình động).
Ý tưởng 3: Lập trình động + Tối ưu hóa mảng trượt
1. Chia thành các giai đoạn
Chia các giai đoạn dựa trên giới hạn trọng lượng hiện tại của túi.
2. Định nghĩa trạng thái
Định nghĩa trạng thái là: giá trị tối đa có thể đạt được bằng cách đặt các mặt hàng vào túi với giới hạn trọng lượng là .
3. Phương trình chuyển trạng thái
Lưu ý: Ở đây, là "giá trị trạng thái của giai đoạn thứ " sau khi tính toán vòng lặp thứ .
Vì chúng ta cần sử dụng tính toán trong vòng lặp thứ khi tính toán , chúng ta cần tính toán theo "thứ tự chuyển tiếp" (từ ) để có được kết quả chính xác.
Khi , chỉ có thể lấy giá trị của giai đoạn trước đó , tức là giá trị không thay đổi. Phần này có thể bỏ qua. Do đó, khi tính toán theo thứ tự chuyển tiếp, chúng ta chỉ cần bắt đầu lặp từ .
4. Điều kiện ban đầu
- Bất kể giới hạn trọng lượng của túi là bao nhiêu, nếu không chọn mặt hàng nào, giá trị tối đa có thể đạt được luôn là , tức là .
5. Kết quả cuối cùng
Dựa trên trạng thái được định nghĩa trước đó, biểu thị giá trị tối đa có thể đạt được bằng cách đặt các mặt hàng vào túi với giới hạn trọng lượng là . Kết quả cuối cùng là , trong đó là giới hạn trọng lượng của túi.
Ý tưởng 3: Code
class Solution:
# Ý tưởng 3: Lập trình động + Tối ưu hóa mảng trượt
def completePackMethod3(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [0 for _ in range(W + 1)]
# Liệt kê các mặt hàng đầu tiên
for i in range(1, size + 1):
# Lặp qua trọng lượng của túi theo thứ tự chuyển tiếp
for w in range(weight[i - 1], W + 1):
# dp[w] lấy giá trị lớn nhất giữa "giá trị tối đa đạt được bằng cách đặt các mặt hàng đầu tiên vào túi với giới hạn trọng lượng là w" và "giá trị tối đa đạt được bằng cách đặt các mặt hàng đầu tiên vào túi với giới hạn trọng lượng là w - weight[i - 1], sau đó thêm 1 mặt hàng của mặt hàng thứ i - 1"
dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1])
return dp[W]
Bằng cách so sánh mã cho "Tối ưu hóa mảng trượt cho bài toán cái túi 0-1" và "Tối ưu hóa mảng trượt cho bài toán cái túi đầy đủ", chúng ta có thể thấy rằng sự khác biệt duy nhất nằm ở:
- Mã cho "Tối ưu hóa mảng trượt cho bài toán cái túi 0-1" sử dụng phương pháp "lặp ngược từ ".
- Mã cho "Tối ưu hóa mảng trượt cho bài toán cái túi đầy đủ" sử dụng phương pháp "lặp theo thứ tự chuyển tiếp từ ".
Ý tưởng 3: Phân tích độ phức tạp
- Độ phức tạp thời gian: , trong đó là số lượng loại mặt hàng và là giới hạn trọng lượng của túi.
- Độ phức tạp không gian: .