有一段時間沒作題了,前幾天打開作了幾道,其中有一道DP。算法
從一開始的思路錯誤,到後面優化。由於整個過程花了一些時間,因此想把這個過程記錄一下。數組
來源:力扣(LeetCode)
連接:leetcode-cn.com/problems/ma…
著做權歸領釦網絡全部。商業轉載請聯繫官方受權,非商業轉載請註明出處。網絡
爲了方便看,把原題貼出來以下⬇️函數
打開題,瞅了瞅,求最大報酬?那確定用動態規劃了。提及來這道題是否是在哪裏見過。剛打開題就一直想狀態轉移方程,想DP數組。(舒適提示:如下內容爲初步思考過程,想看正確的思路可跳過這部分)優化
有了,我用一個二維數組slice[][]
,用 slice[i][j]
表示從時間 i~j
能夠得到的最大收益,這樣結果就是slice[minStartTime][maxEndTime]
。(由於我用Go,用切片表示,下文的數組在Go裏用切片表示)ui
思路肯定後,開始思考狀態轉移方程,填充二維數組。spa
在每段時間線內挑選工做,遍歷工做列表,若是該工做在時間線內,代表該工做可選,計算最大收益。3d
// 第k項工做
if startTime[k] >= i && endTime <= j {
// 分狀況
if startTime[k] > i && endTime[k] < j {
// 工做在中間時間段
// 最大收益=max(原來的最大收益, profit[k]+左邊時間段的最大收益處+右邊時間段的最大收益)
slice[i][j] = max(slice[i][j],
profit[k] + slice[i][startTime[k]] + slcie[endTime[k]][j])
} else if startTime[k] > i {
// 工做在i時間點以後開始,且j恰好結束(endTime[k] == j)
slice[i][j] = max(slice[i][j],
profit[k] + slice[i][startTime[k]])
} else if endTime[k] < j {
// 工做恰好在i開始,且在j以前結束
slice[i][j] = max(slice[i][j],
profit[k] + slice[endTime[k]][j])
} else {
// 工做恰好在i開始,且恰好在j結束
slice[i][j] = max(slice[i][j], profit[k])
}
}
複製代碼
思路理清了,codingcode
func jobScheduling(startTime []int, endTime []int, profit []int) int {
// 工做數量
n := len(startTime)
minTime := math.MaxInt32
maxTime := -1
for i := 0; i < n; i++ {
if startTime[i] < minTime {
minTime = startTime[i]
}
if endTime[i] > maxTime {
maxTime = endTime[i]
}
}
// slice[i][j] 表示 i~j 時間段所能得到的最大收益
slice := make([][]int, maxTime+2)
for i := minTime; i < maxTime+1; i++ {
slice[i] = make([]int, maxTime+2)
}
for i := maxTime+1; i >= minTime; i-- {
for j := i+1; j <= maxTime; j++ {
// 挑選工做
for k := 0; k < n; k++ {
// 工做在時間段內
if startTime[k] >= i && endTime[k] <= j {
if startTime[k] > i && endTime[k] < j {
slice[i][j] = max(slice[i][j], slice[i][startTime[k]] + profit[k] + slice[endTime[k]][j])
} else if startTime[k] > i {
slice[i][j] = max(slice[i][j], slice[i][startTime[k]] + profit[k])
} else if endTime[k] < j {
slice[i][j] = max(slice[i][j], profit[k] + slice[endTime[k]][j])
} else {
slice[i][j] = max(slice[i][j], profit[k])
}
}
}
}
}
return slice[minTime][maxTime]
}
func max(x, y int) int {
if x > y {
return x
} else {
return y
}
}
複製代碼
想必有大佬已經看出來有多愚蠢了,是的我根本沒看提示裏的取值範圍。空間理所固然爆了23333,時間也得來到3次方,只過了十幾個test case,打開看詳情的時候人懵了,數值幾萬幾萬的。望各位大佬不要嘲笑,小弟只是個新手。
後面又開始從新想狀態轉移方程,也沒想到什麼好辦法(牢記之後得看數值範圍,且不要急着想DP數組)。
確實一直死磕想狀態轉移也想不出什麼了。算了,先用暴力寫寫看吧(暴力大概是咱們普通人的最愛吧)。
大體思路: 把全部可選的狀況整理出來,再選擇收益最高的狀況。
由於是在一條時間線上工做,交叉的部分不能再選。先按開始時間排一下序。
// 既然是暴力,排序不是核心,就先隨便排排
n := len(startTime)
for i := 0; i < n; i++ {
idx := i
for j := i+1; j < n; j++ {
if startTime[j] < startTime[idx] {
idx = j
}
}
// 結束時間,對應利潤也要相應的交換
startTime[i], startTime[idx] = startTime[idx], startTime[i]
endTime[i], endTime[idx] = endTime[idx], endTime[i]
profit[i], profit[idx] = profit[idx], profit[i]
}
複製代碼
排完序以後,工做的時間線就變成了第一項的開始時間,到最後一項的結束時間,因此能夠用每一項工做的索引去表示時間範圍。若是說上一項工做的結束時間比該項的開始時間還大,則須要跳過這一項工做,直接尋找下一項工做。
// 寫一個函數,返回 從idx索引的開始時間到整條時間線結束所能得到的最大收益
// maxProfit(0)表示從開始到結束獲得最大收益
func maxProfit(idx int) int {
// 索引超過了最大的,表示沒有工做了,直接返回收益爲0
if idx >= N {
return 0
}
// 計算下一個可選的工做, 即開始時間在本EndTime[idx]以後
i := idx + 1
for ; i < len(StartTime) && StartTime[i] < EndTime[idx]; i++ {}
// 用兩個變量表示,選擇該項工做了的最大收益,不選該工做的最大收益
// (選與不選,揹包???emmm有內味了)
var p1, p2 int
// 選擇該工做收益
p1 = Profit[idx] + maxProfit(i)
// 不選該工做,將時間段空出來給其餘工做,索引項直接從該項的下一項開始
p2 = maxProfit(idx+1)
if p1 > p2 {
return p1
} else {
return p2
}
}
複製代碼
由於是暴力法,理所固然是會超時的, 只過了幾個test case,完整的代碼就不貼出來了。
但一番思考事後,相信你們都能看出一點端倪了(最喜歡 選與不選 這個詞了)。
其實已經比較明顯了,每一次執行maxProfit(idx)
都有可能重複計算,即所謂的重疊子問題,到這裏能夠從新思考狀態轉移方程了。但這一步咱們先採用記憶化的方法(自頂向下動態規劃?)。
咱們引入一個 memory[]
數組,用來存放 maxProfit(idx)
的值,在調用 maxProfit
時,若是須要繼續向下調用,咱們先判斷向下調用的idx
是否已經存在 memory[]
中,有的話直接替代,而不是調用函數。
coding
func jobScheduling(startTime []int, endTime []int, profit []int) int {
// 按開始時間排序
// 排序和前面同樣,如今不是重點,後面再優化
// 爲了方便設置爲全局變量
N = len(startTime)
StartTime = startTime
EndTime = endTime
Profit = profit
memory = make([]int, N+1)
// 將各項都初始化爲-1表示未存儲
for i := 0; i <= N; i++ {
memory[i] = -1
}
return maxProfit(0)
}
var StartTime []int
var EndTime []int
var Profit []int
var memory []int
var N int
func maxProfit(idx int) int {
// 沒有工做了
if idx >= N {
return 0
}
// 計算下一個可選的索引
i := idx + 1
for ; i < len(StartTime) && StartTime[i] < EndTime[idx]; i++ {}
var p1, p2 int
// 選擇該工做的最高報酬
// 若是memory有保存相應結果,直接使用
if memory[i] != -1 {
p1 = Profit[idx] + memory[i]
} else {
p1 = Profit[idx] + maxProfit(i)
}
// 不選擇該工做的最高報酬
if memory[idx+1] != -1 {
p2 = memory[idx+1]
} else {
p2 = maxProfit(idx+1)
}
// 結果保存到memory
if p1 > p2 {
memory[idx] = p1
} else {
memory[idx] = p2
}
return memory[idx]
}
複製代碼
經過記憶化(緩存),減小重複問題的計算,減小函數調用次數。執行一下。
經過了耶,但這個。。。QWQ一言難盡用時。來優化吧。
經過上一節的改造,已經能夠經過全部的test case,接下來就是優化階段。不過再此以前,再改造一下,把遞歸改爲迭代(就是dp啦)。
用一個數組dp[]
存放收益,dp[i]
表示從startTime[i]
開始到整條時間線結束所能得到的最大收益。填充dp
數組,最後return dp[0]
便可。
狀態轉移方程dp[i] = max(profit[i]+dp[下一個可選], dp[i+1])
func jobScheduling(startTime []int, endTime []int, profit []int) int {
// 按開始時間排序
// 排序和前面同樣,如今不是重點,後面再優化
//預留一個位置,防越界
dp := make([]int, n+1)
for i := n - 1; i >= 0; i-- {
j := i + 1
for ; j < n && startTime[j] < endTime[i]; j++ {}
dp[i] = max(profit[i] + dp[j], dp[i+1])
}
return dp[0]
}
複製代碼
改形成迭代後代碼就看起來十分精簡了,核心就那麼幾行(把遞歸改爲迭代並不會減小多少時間,只是減小了遞歸調用函數保存運行的堆棧空間)。
dp的代碼十分精簡,雖然內部有嵌套一層循環,但實際上時間複雜度連O(n^2)都不到。可想而之,形成用時過長的緣由實際上是排序的問題。再次看一下排序的代碼。
n := len(startTime)
for i := 0; i < n; i++ {
idx := i
for j := i+1; j < n; j++ {
if startTime[j] < startTime[idx] {
idx = j
}
}
startTime[i], startTime[idx] = startTime[idx], startTime[i]
endTime[i], endTime[idx] = endTime[idx], endTime[i]
profit[i], profit[idx] = profit[idx], profit[i]
}
複製代碼
能夠看到每次排序,都須要交換三組數據,要知道交換是比較耗時的,況且是三組。想辦法把交換去掉。
// 按開始時間排序
sortIdx := make([]int, n)
used := make([]bool, n)
for i := 1; i < n; i++ {
if startTime[i] < startTime[sortIdx[0]] {
sortIdx[0] = i
}
}
used[sortIdx[0]] = true
for i := 1; i < n; i++ {
min := math.MaxInt32
idx := 0
for j := 0; j < n; j++ {
if startTime[j] <= min && startTime[j] >= startTime[sortIdx[i-1]] && !used[j] {
idx = j
min = startTime[j]
}
}
sortIdx[i] = idx
used[idx] = true
}
複製代碼
用一個sortIdx[]
保存排序後的索引,好比sortIdx[0] = 5
表示開始時間最小的是第六項工做,用一個標記數組used[]
來輔助,這樣就去掉了三次交換的工做。以後調整一下下面的代碼就行了。
跑一下
少了一半差很少吧,不過還遠遠不夠。
本身再怎麼寫排序,也比不上內置的排序算法。使用比較器,經過定義本身的比較規則使用內置的排序算法。(提及來爲何不一開始就用呢,就沒這麼多事了,真是愚昧啊)
定義一個結構體(類?),把每一項工做的開始,結束,收益對應起來。
type Work struct {
start int
end int
profit int
}
type WorkSlice []Work
複製代碼
實現比較接口,定義比較規則。其餘語言也是同理,自定義比較器,使用接口等等,具體看相應的文檔。
func (ws WorkSlice) Len() int {
return len(ws)
}
func (ws WorkSlice) Less(i, j int) bool {
return ws[i].start < ws[j].start
}
func (ws WorkSlice) Swap(i, j int) {
ws[i], ws[j] = ws[j], ws[i]
}
複製代碼
這樣就可使用內置的sort包調用排序算法了。
完整coding
func jobScheduling(startTime []int, endTime []int, profit []int) int {
n := len(startTime)
ws := make([]Work, n)
// 初始化
for i := 0; i < n; i++ {
ws[i] = Work{startTime[i], endTime[i], profit[i]}
}
// 排序
sort.Sort(WorkSlice(ws))
dp := make([]int, n+1)
for i := n - 1; i >= 0; i-- {
j := i + 1
for ; j < n && ws[j].start < ws[i].end; j++ {}
dp[i] = max(ws[i].profit + dp[j], dp[i+1])
}
return dp[0]
}
type Work struct {
start int
end int
profit int
}
type WorkSlice []Work
func (ws WorkSlice) Len() int {
return len(ws)
}
func (ws WorkSlice) Less(i, j int) bool {
return ws[i].start < ws[j].start
}
func (ws WorkSlice) Swap(i, j int) {
ws[i], ws[j] = ws[j], ws[i]
}
func max(x, y int) int {
if x > y {
return x
} else {
return y
}
}
複製代碼
提交一下,over
To be continue