LeetCode 1235.規劃兼職工做

有一段時間沒作題了,前幾天打開作了幾道,其中有一道DP。算法

從一開始的思路錯誤,到後面優化。由於整個過程花了一些時間,因此想把這個過程記錄一下。數組

原題

連接在此 1235. 規劃兼職工做緩存

來源:力扣(LeetCode)
連接:leetcode-cn.com/problems/ma…
著做權歸領釦網絡全部。商業轉載請聯繫官方受權,非商業轉載請註明出處。網絡

爲了方便看,把原題貼出來以下⬇️函數

img
img

初步思考

打開題,瞅了瞅,求最大報酬?那確定用動態規劃了。提及來這道題是否是在哪裏見過。剛打開題就一直想狀態轉移方程,想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]
}
複製代碼

經過記憶化(緩存),減小重複問題的計算,減小函數調用次數。執行一下。

img

經過了耶,但這個。。。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[]來輔助,這樣就去掉了三次交換的工做。以後調整一下下面的代碼就行了。

跑一下

img

少了一半差很少吧,不過還遠遠不夠。

本身再怎麼寫排序,也比不上內置的排序算法。使用比較器,經過定義本身的比較規則使用內置的排序算法。(提及來爲何不一開始就用呢,就沒這麼多事了,真是愚昧啊)

定義一個結構體(類?),把每一項工做的開始,結束,收益對應起來。

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

img

To be continue

相關文章
相關標籤/搜索