零 標題:演算法(leetcode,附思維導圖 + 全部解法)300題之(2312)賣木頭塊
一 題目描述
二 解法總覽(思維導圖)
三 全部解法
面試官:看你準備得差不多了,我們開始面試吧。
狂徒張三:okk~
面試官:題目看得差不多了的話,來說說你的想法、思路哈~
狂徒張三:因為題目中,含有 “最” 字眼,所以我覺得應該優先考慮使用 “動態規劃” 。
面試官: 那你覺得使用動態規劃的條件有哪些呢?
狂徒張三:我個人認為,應該需要具備2個條件:
**1)最優子結構
2)無後效性
**
面試官: 很好,那你知道動態規劃的本質和解題步驟分別是什麼嗎?
狂徒張三:
**1)本質:一種以空間換時間的技術
2)解題步驟:分3步。狀態定義: 每個狀態的決策,存放每個狀態的變數;狀態轉移方程: 當前狀態與之前狀態之間的轉換關係;初始狀態: 初始的狀態或者邊界條件等。**
面試官:小夥子,可以呀。我看你也差不多熱完身了,那你就用如上知識解下這道題吧~
旁白:過了5-10分鐘,張三遲遲寫不出程式碼。
面試官:(一臉凝重、困惑)難道你只背了相關概念,沒進行過相關題目的編碼嗎?
狂徒張三:(張三面漏怯色)額。。。。
面試官:這樣,你把木塊想象成大西瓜,寫起程式碼來也會嘎嘎的清涼和爽快哦~
那題目就變成了 —— 你有1個二維(長度為w、寬度為h)的大西瓜,你可以選擇直接把它賣掉(若此時得有人正好買長度為w、寬度為h),不然的話此時的大西瓜只能獲得0元
狂徒張三:對的,然後我們也可以選擇不賣此時的大西瓜,進行橫向、縱向的切瓜,把大西瓜不斷切成不同的小西瓜,最後從這些切瓜方案中計算出當前大西瓜的能賣處的最大價錢。
面試官:是的,那你這邊根據之前所說,寫下 狀態定義 和 狀態轉移方程吧~
狂徒張三:好的。
我理解的狀態定義 —— dpi,長度為i、寬度為j時,能得到的最多錢數。
狀態轉移方程 —— 橫向切瓜時:dpi = max(dpi, dpk + dpi - k),k的範圍為 [1, i - 1]。
縱向切瓜時:dpi = max(dpi, dpi + dpi),k的範圍為 [1, j - 1]。
面試官:那狀態的初始化呢?
狂徒張三:根據陣列 prices ,進行初始化 —— 當 i、j 存在於 prices 裡的0、1下標位置上時,dpi = prices對應的元素下標。
面試官:很好,既然思路已經理清了,那就開始你的表演,啊不、開始你的程式碼編寫吧~
旁邊:張三瞬間如同任督二脈被打通,三下五除二,不到10分鐘便把程式碼敲打了出來~
1 方案1
1)程式碼:
// 方案1 “動態規劃法 - 普通版”。
// “技巧:題幹中含有 最 字眼,優先考慮動態規劃(本質:以空間換時間的技術)。”
// 參考:
// 1)https://leetcode.cn/problems/selling-pieces-of-wood/solution/mai-mu-tou-kuai-by-leetcode-solution-gflg/
// 2)https://leetcode.cn/problems/selling-pieces-of-wood/solution/by-endlesscheng-mrmd/
// 想法(這裡把木塊想象成大西瓜,寫起程式碼來也會嘎嘎的清涼和爽快哦~):
// 1)狀態定義:dp[i][j],長度為i、寬度為j時,能得到的最多錢數。
// 2)狀態轉移:
// 2.1)橫向切:dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]),k的範圍為 [1, i - 1]。
// 2.2)縱向切:dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]),k的範圍為 [1, j - 1]。
// 思路:
// 1.1)狀態初始化:l = prices.length;
// dp = new Array(m + 1).fill(1).map(v => new Array(n + 1).fill(0));
// 思考:二維的每個元素為啥都先 預設填充0 ?
// 1.2)狀態初始化:遍歷 陣列 prices ,進一步初始化 陣列 dp 。
// 2)核心:狀態轉移。
// 2.1)橫向切:dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]),k的範圍為 [1, i - 1]。
// 2.2)縱向切:dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]),k的範圍為 [1, j - 1]。
// 3)返回結果 dp[m][n] 。
var sellingWood = function(m, n, prices) {
// 1.1)狀態初始化:l = prices.length;
// dp = new Array(m + 1).fill(1).map(v => new Array(n + 1).fill(0));
// 思考:二維的每個元素為啥都先 預設填充0 ?
const l = prices.length;
let dp = new Array(m + 1).fill(1).map(v => new Array(n + 1).fill(0));
// 1.2)狀態初始化:遍歷 陣列 prices ,進一步初始化 陣列 dp 。
for (let i = 0; i < l; i++) {
const [width, height, price] = prices[i];
dp[width][height] = price;
}
// 2)核心:狀態轉移。
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// 2.1)橫向切:dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]),k的範圍為 [1, i - 1]。
for (let k = 1; k < i; k++) {
dp[i][j] = Math.max(dp[i][j], dp[k][j] + dp[i - k][j]);
}
// 2.2)縱向切:dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]),k的範圍為 [1, j - 1]。
for (let k = 1; k < j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[i][j - k]);
}
}
}
// 3)返回結果 dp[m][n] 。
return dp[m][n];
};
2 方案2
面試官:Good。程式碼結構很有層次感,註釋也放在了很合適的位置~
狂徒張三:畢竟這個“二維的大西瓜”是保熟的,我敢保證這裡的演算法一定是最優的,能夠保證我們的大西瓜賣出最高的價錢。
面試官:你確定你這個“大西瓜切割演算法”保熟嗎?我看不一定吧?
狂徒張三:我是1個正經的演算法人,還能給你寫法“生瓜演算法”不成?
面試官:我問你,這“大西瓜切割演算法”保熟嗎?
狂徒張三:你就說我這次面試能不能過吧~
面試官:
狂徒張三:那我在看看、想想優化點吧
旁白:只見張三在紙上齊颼颼的寫起了程式碼執行過程。
...
dp5 = max(dp5, dp1 + dp4, dp2 + dp3, dp3 + dp2, dp2 + dp1)
...
狂徒張三:看起來確實有優化點 —— 存在大量的冗餘計算,我們下標k只需列舉到一半的位置即可 —— 即 k的範圍為 [1, i / 2(向下取整)] 。
1)程式碼:
// 方案2 “動態規劃法 - 優化版”。
// “技巧:題幹中含有 最 字眼,優先考慮動態規劃(本質:以空間換時間的技術)。”
// 參考:
// 1)https://leetcode.cn/problems/selling-pieces-of-wood/solution/mai-mu-tou-kuai-by-leetcode-solution-gflg/
// 2)https://leetcode.cn/problems/selling-pieces-of-wood/solution/by-endlesscheng-mrmd/
// 想法(這裡把木塊想象成大西瓜,寫起程式碼來也會嘎嘎的清涼和爽快哦~):
// 1)狀態定義:dp[i][j],長度為i、寬度為j時,能得到的最多錢數。
// 2)狀態轉移:
// 2.1)橫向切:dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]),k的範圍為 [1, i - 1]。
// 2.2)縱向切:dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]),k的範圍為 [1, j - 1]。
// 思路:
// 1.1)狀態初始化:l = prices.length;
// dp = new Array(m + 1).fill(1).map(v => new Array(n + 1).fill(0));
// 思考:二維的每個元素為啥都先 預設填充0 ?
// 1.2)狀態初始化:遍歷 陣列 prices ,進一步初始化 陣列 dp 。
// 2)核心:狀態轉移(有優化,存在對稱性,k列舉到i、j的1半的位置即可)。
// 2.1)橫向切:dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]),k的範圍為 [1, i / 2(向下取整)]。
// 2.2)縱向切:dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]),k的範圍為 [1, j / 2(向下取整)]。
// 3)返回結果 dp[m][n] 。
var sellingWood = function(m, n, prices) {
// 1.1)狀態初始化:l = prices.length;
// dp = new Array(m + 1).fill(1).map(v => new Array(n + 1).fill(0));
// 思考:二維的每個元素為啥都先 預設填充0 ?
const l = prices.length;
let dp = new Array(m + 1).fill(1).map(v => new Array(n + 1).fill(0));
// 1.2)狀態初始化:遍歷 陣列 prices ,進一步初始化 陣列 dp 。
for (let i = 0; i < l; i++) {
const [width, height, price] = prices[i];
dp[width][height] = price;
}
// 2)核心:狀態轉移。
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// 2.1)橫向切:dp[i][j] = max(dp[i][j], dp[k][j] + dp[i - k][j]),k的範圍為 [1, i / 2(向下取整)]。
for (let k = 1; k <= Math.floor(i / 2); k++) {
dp[i][j] = Math.max(dp[i][j], dp[k][j] + dp[i - k][j]);
}
// 2.2)縱向切:dp[i][j] = max(dp[i][j], dp[i][k] + dp[i][j - k]),k的範圍為 [1, j / 2(向下取整)]。
for (let k = 1; k <= Math.floor(j / 2); k++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[i][j - k]);
}
}
}
// 3)返回結果 dp[m][n] 。
return dp[m][n];
};
旁白:張三寫完了如上程式碼,急忙問面試官。
狂徒張三:通過面試了吧?
面試官:
狂徒張三:
又1個offer,然後馬上就要出任 CEO 了,我晚上應該是去吃 沙縣小吃 呢? 還是 蘭州拉麵 呢?哎,選擇太多也是一種煩惱!
四 資源分享 & 更多
1 歷史文章 - 總覽
2 博主簡介
碼農三少 ,一個致力於編寫 極簡、但齊全題解(演算法) 的博主。
專注於 一題多解、結構化思維 ,歡迎一起刷穿 LeetCode ~