動態規劃學習--- 數組查找最大遞增路徑長度

題目描述

leetcode地址

源碼

給定一個僅含數字的數組,查找嚴格遞增子序列的長度。
比如:[5, 7, -24,12,13,2,3,12,5,6,35]
最長子序列長度是6,即[-24, 2, 3, 5, 6, 35]

方法一:遞歸查找

這個題的難點在于,不能一味地取比前一個值大的數,這樣得不到最優解,所以,對于比前一個值大的數,需要考察取或不取2種情況。
在遞歸查找時,對于比前一個數小的數,直接考察下一個位置,路徑不變。比前一個數大的數,需要遞歸2次,取當前位置和不取當前位置,取最長的路徑
代碼實現:

var lengthOfLIS = function(nums) {
    return recursive(0, -Infinity)
    function recursive(pos, value) {
        // 邊界處理
        if(pos >= nums.length) return 0
        let path1 = 0, path2 = 0;
        // 如果當前位置比先前的值大,則要考慮取當前值和不取當前值時得到的路徑長度。
        if(nums[pos] > value) {
            path1 = 1 + recursive(pos+1, nums[pos])
        }
       //不論當前位置的值大小,都需要考察不取當前值的情況
        path2 = recursive(pos+1, value)
        return Math.max(path1, path2)
    }
};

時間復雜度:O(2^n)
空間復雜度:O(n^2)
很遺憾,用此解法在leetcode上運行是無法通過的,時間復雜度太高。

方法二:帶記憶能力的遞歸查找

這個題目的第二個難點在于,如何存儲已經查找過的位置。
用一維數組存儲明顯是不行的,我們存儲的目的是避免遞歸,存儲的數值要滿足,一定是當前位置往后查找遞增子序列時的最優解,如果用一維數組存儲:
(錯誤示例)

var lengthOfLIS = function(nums) {
    let memo = []
    return recursive(0, -Infinity)
    function recursive(pos, value) {
        // 邊界處理
        if(pos >= nums.length) return 0
        // add
        if(memo[pos]) return memo[pos]
         ...
        return memo[pos] = Math.max(path1, path2)
    }
};

以[5, 7, -24,12,13,2,3,12,5,6,35]為例,記憶數組memo的值是[5, 4, 3, 3, 2, 1, 1, 1, 1, 1, 1]。
其值存在1個問題:僅能記憶從位置0開始的遞增子序列,當從位置0開始計算一輪之后,memo的每一項都有了值,下一次再次嘗試調用遞歸,就滿足了if(memo[pos])return memo[pos]。也就是,記憶數組并沒有記住最優解,其值就不可用。

考慮用二維數組,memo[i][j]表示前一次取的數的位置是num[i],當前這一次取的數的位置是num[j]。再次訪問某個位置時,若其前一個位置也相同,則返回memo[i][j]。

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    let memo = []
    for(let i = 0; i < nums.length; i++) {
        memo[i] = []
    }
    return recursive(-1, 0, -Infinity)
    function recursive(prePos, pos, value) {
        // 邊界處理
        if(pos >= nums.length) return 0
        if(memo[prePos+1][pos]) {
            return memo[prePos+1][pos]
        }
        let path1 = 0, path2 = 0;
        if(nums[pos] > value) {
            path1 = 1 + recursive(pos, pos+1, nums[pos])
        }
        path2 = recursive(prePos, pos+1, value)
        return memo[prePos+1][pos] = Math.max(path1, path2)
    }
};

時間復雜度:O(n^2)
空間復雜度:O(n^2)

方法三:動態規劃

dp[]存儲包含當前位置的最大遞增子序列長度。
舉例:[5, 7, -24, 2, 3,12, 5, 6, 35]
(1)確定初始狀態: dp[0] = 1, 即長度為1的數組,最長遞增子序列長度也為1。(或者初始態也可以設置為,任意位置都為1,其意義是每個位置的最長遞增子序列是至少包含其自身的。)

截屏2021-02-03 14.51.40.png

(2)狀態分析,這一步是最難的,找到規律之前,不要失去耐心,一個位置一個位置地考察。
考慮dp[1]的值,很明顯,若 nums[1] > nums[0],則dp[1] = dp[0] + 1,否則dp[1] = 1(初始狀態,不改變)。

截屏2021-02-03 15.47.39.png

考慮dp[2]的值,是不是nums[2] > nums[1],也同樣可以得出dp[2] = dp[1] + 1,否則,dp[2] = 1呢?這里是可行的,按這個等式填上dp[2]的值。

截屏2021-02-03 15.44.33.png

考慮dp[3]的值,nums[3] > nums[2],但不能得出dp[3] = dp[2] + 1 = 3,其實你應該發現了,要滿足dp[i] = dp[i-1] + 1,一定是嚴格遞增數組才可以。
nums[3]僅僅和nums[2]比較大小是不夠的,它還需要和前面位置的所有元素都做比較,那比較出大小之后如何記錄dp[3]呢?dp[0]~dp[2]上的值就派上用場了。以i表示位置3,j從0走到i-1,若滿足nums[i] > nums[j],則dp[i] = dp[j] + 1,否則保持不變。

截屏2021-02-03 16.28.43.png

截屏2021-02-03 16.29.08.png

截屏2021-02-03 16.29.37.png

用此思路嘗試填入余下的值,并校驗正確性


截屏2021-02-03 16.47.12.png

同時,也要校驗dp[1]和dp[2]是否也可以按此狀態轉移得出。若不行,則考慮特殊處理。

代碼實現:

/**
 * @param {number[]} nums
 * @return {number}
 *  */
var lengthOfLIS = function(nums) {
    let size = nums.length;
    if(size <= 1) return size
    let dp = []
    let path = 0
    for(let i = 0; i < size; i++) {
        dp[i] = 1
        for(let j = 0; j < i; j++) {
            if(nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j]+1)
            }
        }
        path = Math.max(path, dp[i])
    }
    return path;
};

時間復雜度:O(n^2)
空間復雜度:O(n)


屏幕快照 2021-03-28 下午3.02.30.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容