Bài toán
Link đến bài toán: 2742. Painting the Walls
Phân tích
Đề bài đọc khá khó hiểu, /ᐠﹷ ‸ ﹷ ᐟ\ノ để giải thích cho dễ hiểu thì đề bài yêu cầu như thế này:
- Có 2 mảng
cost
vàtime
, cùng độ dài,cost[i]
là chi phí để sơn tường thứi
,time[i]
là thời gian để sơn tường thứi
. paid painter
sẽ sơn 1 bức tường với chi phí làcost[i]
và thời gian làtime[i]
.free painter
sẽ sơn 1 bức tường với chi phí là 0, thời gian là 1.- Mục tiêu là tìm cách sơn tường với chi phí nhỏ nhất.
Nhìn 1 cách đơn giản hóa bài toán, thì khi ta chọn một bức tường,
thực chất ta đang sơn time[i] + 1
(free painter
sơn time[i] bức, paid painter
sơn 1 bức)
bức tường với chi phí là cost[i]
.
Vậy, thực chất đây là một bài toán knapsack chọn hoặc không chọn. Ta có thể giải bài toán này bằng đệ quy và tối ưu bằng dynamic programming.
Công thức đệ quy:
f(i, remain) = min(f(i + 1, remain), f(i + 1, remain - 1 - time[i]) + cost[i])
Trong đó
f(i, remain)
là chi phí nhỏ nhất để sơnremain
bức tường từi
đếnlen(cost) - 1
f(i + 1, remain)
có thể hiểu là ta không sơn tường thứi
f(i + 1, remain - 1 - time[i]) + cost[i]
có thể hiểu là ta sơn tường thứi
. Chi phí bằng chi phí để sơn tường thứi
cộng với chi phí để sơnremain - 1 - time[i]
bức tường từi + 1
đếnlen(cost) - 1
.
Độ phức tạp là O(2n).
Giải thuật
Đệ quy
- Base case:
- remain <= 0 => ta đã sơn xong tất cả các tường => return 0
- i == len(cost) => không thể dùng thêm painter nào nữa => return một số lớn
- Nếu
remain > 0
vài < len(cost)
:- Tính minCost nếu ta không sơn tường i:
minCost = f(i + 1, remain)
- Tính minCost nếu ta sơn tường i:
minCost = min(minCost, f(i + 1, remain - 1 - time[i]) + cost[i])
- Tính minCost nếu ta không sơn tường i:
- Trả về
minCost
nhỏ nhất.
func minCost(cost []int, time []int) int {
return f(0, len(cost), 0, cost, time)
}
func f(i, n, remain int, cost, time []int) int {
if remain <= 0 {
return 0
}
if i == n {
return 1e9
}
minCost := f(i + 1, n, remain, cost, time)
minCost = min(minCost, f(i + 1, n, remain - 1 - time[i], cost, time) + cost[i])
return minCost
}
Tối ưu: Nếu để ý kĩ, ta sẽ thấy nhiều trường hợp f(i, remain)
được gọi lại nhiều lần. Ta có thể tối ưu bằng cách
dùng một mảng nhớ lại kết quả của các bài toán con đã được giải. Hoặc có thể dủng quy hoạch động? (=^OwO^=)
Dynamic programming
- Khởi tạo mảng
dp
vớidp[i][j]
là chi phí nhỏ nhất để sơnj
bức tường từi
đếnlen(cost) - 1
, giá trị khởi tạo là0
. - Đặt
dp[len(cost) - 1][0] = 1e9
. (Hoặc 1 số lớn nào đó khác) - Với
i
từlen(cost) - 1
đến0
:- Với
remain
từ1
đếnlen(cost)
:- Tính minCostNotPaint khi ta không sơn tường
i
:minCost = dp[i + 1][remain]
. - Tính minCostPaint khi ta sơn tường
i
:minCost = dp[i + 1][max(0, remain - 1 - time[i])] + cost[i]
. dp[i][remain] = min(minCostNotPaint, minCostPaint)
.
- Tính minCostNotPaint khi ta không sơn tường
- Với
- Trả về
dp[0][len(cost)]
.
func minCost(cost []int, time []int) int {
n := len(cost)
dp := make([][]int, n + 1)
for i := 0; i <= n; i++ {
dp[i] = make([]int, n + 1)
dp[i][0] = 1e9
}
for i := n - 1; i >= 0; i-- {
for remain := 1; remain <= n; remain++ {
minCostNotPaint := dp[i + 1][remain]
minCostPaint := dp[i + 1][max(0, remain - 1 - time[i])] + cost[i]
dp[i][remain] = min(minCostNotPaint, minCostPaint)
}
}
return dp[0][n]
}
Độ phức tạp là O(n2). Độ phức tạp không gian là O(n2).
Tối ưu: Ta thấy rằng ta chỉ cần lưu lại 2 hàng của mảng dp
thôi. Lúc này, độ phức tạp không gian sẽ là O(n)
.
Các bạn tự giải tiếp nhé, bài này được viết vào nửa đêm, tác giả thì đã buồn ngủ và lười viết tiếp. /ᐠ - ˕ -マ Ⳋ