🔗 🔴 2742. Painting the Walls 2425

tags: Weekly Contest 350 動態規劃(DP) 陣列(Array)

題意

給定兩個長度為 nn 的整數陣列 cost\text{cost}time\text{time},分別表示繪製 nn 面不同牆壁的成本和所需時間。

現在有兩位油漆工可供選擇:

  • 一位付費油漆工,用 time[i]\text{time}[i] 單位時間繪製第 ii 面牆壁,花費 cost[i]\text{cost}[i] 單位金錢。
  • 一位免費油漆工,以 11 單位時間繪製任何牆壁,費用為 00。但只能在付費油漆工已經在工作時,才能使用。

請返回繪製 nn 面牆壁所需的最少金額。

約束條件

  • 1cost.length5001 \leq \text{cost.length} \leq 500
  • cost.length==time.length\text{cost.length} == \text{time.length}
  • 1cost[i]1061 \leq \text{cost}[i] \leq 10^6
  • 1time[i]5001 \leq \text{time}[i] \leq 500

思路

方法一:狀態優化的動態規劃

dfs(i,j,k)dfs(i, j, k) 表示考慮前 ii 面牆壁,且付費油漆工耗時 jj 單位時間,免費油漆工耗時 kk 單位時間的最小花費。則對於第 ii 面牆壁,有讓付費油漆工繪製和讓免費油漆工繪製兩種選擇。故可以寫出以下遞迴式:

  • dfs(i,j,k)=min(dfs(i1,j,k+1),dfs(i1,j+time[i1],k)+cost[i1])dfs(i, j, k) = min(dfs(i-1, j, k + 1), dfs(i-1, j+time[i-1], k) + cost[i-1])
    • 若第 ii 面牆壁由免費油漆工繪製,則 jj 不變,kk11 後遞迴;
    • 若第 ii 面牆壁由付費油漆工繪製,則 jj 加上 time[i1]time[i-1]kk 不變後遞迴,並將遞迴結果加上 cost[i1]cost[i-1]
    • 兩者取最小值即可。
  • 終止條件:dfs(0,j,k)=0dfs(0, j, k) = 0 if jkj \geq k else \infty
    • 終止時需要付費時間超過免費的時間,否則不合法,返回 \infty
  • 遞迴入口:dfs(n,0,0)dfs(n, 0, 0)
  • 剪枝:jkij - k \geq i 時,表示免費油漆工可以繪製完剩餘的所有牆壁,返回 00

但上述遞迴式的狀態數量過多,會 MLE ,因此需要進行 狀態優化 。可以發現,我們只關注 jjkk 的差值,因此可以重新定義遞迴式,令 dfs(i,j)dfs(i, j) 表示考慮前 ii 面牆壁,且付費油漆工耗時減去免費油漆工耗時為 jj 單位時間的最小花費。則遞迴式為:

  • dfs(i,j)=min(dfs(i1,j1),dfs(i1,j+time[i1])+cost[i1])dfs(i, j) = min(dfs(i-1, j-1), dfs(i-1, j+time[i-1]) + cost[i-1])
    • 若第 ii 面牆壁由免費油漆工繪製,則 jj 減去 11 後遞迴;
    • 若第 ii 面牆壁由付費油漆工繪製,則 jj 加上 time[i1]time[i-1] 後遞迴,並將遞迴結果加上 cost[i1]cost[i-1]
  • 終止條件:dfs(0,j)=0dfs(0, j) = 0 if j0j \geq 0 else \infty
    • 終止時需要付費時間超過免費的時間,否則不合法,返回 \infty
  • 遞迴入口:dfs(n,0)dfs(n, 0)
  • 剪枝:jij \geq i 時,表示免費油漆工可以繪製完剩餘的所有牆壁,返回 00

複雜度分析

  • 時間複雜度:O(n2)O(n^2) ,其中 nn 為牆壁數量,狀態數量為 O(n2)O(n^2) ,每個狀態需要 O(1)O(1) 的時間計算。
  • 空間複雜度:O(n2)O(n^2) ,狀態數量為 O(n2)O(n^2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)

@cache
def dfs(i: int, j: int) -> int:
if j >= i: # 剩餘的付費時間超過牆數
return 0
if i == 0:
# return 0 if j >= 0 else float('inf')
return float('inf') # 已經檢查過 j >= i 的情況
return min(dfs(i - 1, j - 1), # 免費刷第 i 堵牆 (下標為 i-1 )
dfs(i - 1, j + time[i - 1]) + cost[i - 1]) # 付費刷第 i 堵牆

return dfs(n, 0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int paintWalls(vector<int>& cost, vector<int>& time) {
int n = cost.size();
unordered_map<int, int> memo[n + 1]; // j can be negative, if use vector, need to shift
function<int(int, int)> dfs = [&](int i, int j) {
if (j >= i) return 0;
if (i == 0) return INT_MAX / 2; // avoid overflow
if (memo[i].count(j)) return memo[i][j];
return memo[i][j] = min(dfs(i - 1, j - 1),
dfs(i - 1, j + time[i - 1]) + cost[i - 1]);
};
return dfs(n, 0);
}
};

方法二:轉換為 0-1 背包問題

令付費油漆工的刷牆數量為 paidpaid,工作時間為 paid_timepaid\_time,免費油漆工的刷牆數量和工作時間為 free=free_timefree = free\_time ,則根據題意, paid+free=npaid + free = n ,且需要滿足 paid_timefreepaid\_time \geq free。則可以得到 paid_timenpaidpaid\_time \geq n - paid ,移項得 paid_time+paidnpaid\_time + paid \geq n

則可以將問題轉換為 0-1 背包問題。將第 ii 面牆壁視為一個體積為 time[i]+1time[i] + 1 、價值為 cost[i]cost[i] 的物品,背包容量為 nn 。問題等同於在背包容量為 nn 的情況下,選取物品使得總價值最小,並且體積至少等於背包容量。

dfs(i,j)dfs(i, j) 表示考慮前 ii 面牆壁,且還需要的容量為 jj 的最小花費。對於第 ii 面牆壁,有選和不選兩種選擇,故可以得出以下遞迴式:

  • dfs(i,j)=min(dfs(i1,j),dfs(i1,j(time[i1]+1))+cost[i1])dfs(i, j) = min(dfs(i-1, j), dfs(i-1, j - (time[i-1] + 1)) + cost[i-1])
    • 不選第 ii 面牆壁,則 jj 不變;
    • 選第 ii 面牆壁,則 jj 減去 time[i1]+1time[i-1] + 1,並將遞迴結果加上 cost[i1]cost[i-1]
  • 終止條件:dfs(0,j)=0dfs(0, j) = 0 if j0j \leq 0 else \infty
    • 終止時需要的容量小於等於 00,返回 00,否則返回 \infty
  • 遞迴入口:dfs(n,n)dfs(n, n)
  • 剪枝:j0j \leq 0 時,表示已經選取了足夠的物品,故不用考慮後續物品,直接返回 00

複雜度分析

  • 時間複雜度:O(n2)O(n^2) ,其中 nn 為牆壁數量,狀態數量為 O(n2)O(n^2) ,每個狀態需要 O(1)O(1) 的時間計算。
  • 空間複雜度:O(n2)O(n^2) ,狀態數量為 O(n2)O(n^2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)

@cache # Memoization
def dfs(i: int, j: int) -> int: # i: 已經考慮到第 i 堵牆,j: 還需要的體積
if j <= 0:
return 0
if i == 0:
return float('inf')
return min(dfs(i - 1, j), # 不選
dfs(i - 1, j - (time[i - 1] + 1)) + cost[i - 1]) # 選

return dfs(n, n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int paintWalls(vector<int>& cost, vector<int>& time) {
int n = cost.size();
vector<vector<int>> memo(n + 1, vector<int>(n + 1, -1));
function<int(int, int)> dfs = [&](int i, int j) {
if (j <= 0) return 0;
if (i == 0) return INT_MAX / 2; // avoid overflow
if (memo[i][j] != -1) return memo[i][j];
return memo[i][j] = min(dfs(i - 1, j),
dfs(i - 1, j - time[i - 1] - 1) + cost[i - 1]);
};
return dfs(n, n);
}
};

方法三:優化為一維空間

和方法二的思路相同,但注意到轉移時都只跟上一層有關,因此可以進一步優化空間複雜度。

需要注意,由於每個物品只能被使用一次,因此在更新 dp[j]dp[j] 時,需要從右到左更新,以避免重複使用同一物品。

複雜度分析

  • 時間複雜度:O(n2)O(n^2)
  • 空間複雜度:O(n)O(n)
1
2
3
4
5
6
7
8
9
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)
dp = [float('inf') for _ in range(n + 1)]
dp[0] = 0
for c, t in zip(cost, time):
for j in range(n, 0, -1):
dp[j] = min(dp[j], dp[max(j - t - 1, 0)] + c)
return dp[n]
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int paintWalls(vector<int>& cost, vector<int>& time) {
int n = cost.size();
vector<int> dp(n + 1, INT_MAX / 2);
dp[0] = 0;
for (int i = 0; i < n; i++)
for (int j = n; j > 0; j--)
dp[j] = min(dp[j], dp[max(j - time[i] - 1, 0)] + cost[i]);
return dp[n];
}
};

寫在最後

Cover photo is generated by @たろたろ, thanks for their work!