談一談動態規劃和dfs

Runningfyy發表於2022-01-09

前言

前端學演算法還是有必要性的,不說現在大廠面試都考演算法,就是做許可權模組的時候,如果不懂點演算法,寫樹結構的自定義遍歷都寫不出來。
今天把最近刷題的遇到的動態規劃和dfs做一個總結,希望對大家學習演算法能有所幫助。

DP和dfs關係

動態規劃(dp):

  • 是從下往上的一個遍歷。
  • 需要一個遞推公式比如f(n)=f(n-1)+f(n-2)這種。
  • 不需要遞迴只需要普通的迭代,所以效能好。

深度優先搜尋(dfs):

  • 是從上往下的一個遞迴。
  • 也需要一個遞推公式比如f(n)=f(n-1)+f(n-2)這種。
  • 從上往下遞迴遇到終止條件後又會不斷的從下往上取值(遞迴本質),過程類似dp
  • 由於遍歷所有可能性,所以效能特別差,一般會剪枝
  • 一般剪枝手法就是按條件提前終止或者記憶化

聯絡

  • 記憶化後的dfs其實本質上和dp差不多
  • 因為兩者都要遞推公式,所以理論上dfs能解的題dp都能解
  • 不過有的題適合dfs,比如求解可能性的題目

一道經典題目雙解

leetcode322. 兌換零錢

dp

var coinChange = function(coins, amount) {
/**
    遍歷i
    dp(n) = min( dp(n-conin[i]) + 1) 
    */
    coins.sort((a,b)=>b-a)
    const arr = new Array(amount+1).fill(0)
    for(let i =1;i<=amount;i++){ 
        let curAmount = i //總額
        for(let j=0;j<coins.length;j++){
            let curConin = coins[j] //當前兌換硬幣
            if(curConin<=curAmount){
                let res = arr[curAmount-curConin]+1 //這個數必然大於等於0
                if(res===0) continue //等於0則這種找零不可取
                arr[i] = arr[i]===0?res:Math.min(arr[i],res) //等於0還沒有初始化,先初始化一下
            }
        }
        if(!arr[i]) arr[i]=-1
    }
    // console.log(arr)
    return arr[amount]
    
};

dfs

不剪枝會超時,用快取去剪枝,剪枝了一般效率還是打不過Dp,除非有非常好的剪枝策略

var coinChange = function(coins, amount) {
        coins.sort((a,b)=>b-a)
        let cache = {}
        const dfs = (restamount)=>{

            if(restamount===0) return 0
            if(restamount<coins[coins.length-1])  return -1 //剩餘比最小面額還小則 無法成功兌換
            if(cache[restamount])return cache[restamount]
            if(restamount<coins[coins.length-1]) return 
            for(var i=0;i<coins.length;i++){
                if(coins[i]<=restamount){
                    let res = dfs(restamount-coins[i])+1
                    if(res===0) continue
                    cache[restamount] = cache[restamount]?Math.min(cache[restamount],res):res
                }
            }
            // 當走到這裡的時候cache[restamount]實際已經確定了   自頂向下深度優先遍歷實際預設有一個自底向上的過程,所以天生比dp慢
            if(!cache[restamount]) cache[restamount]=-1
            return cache[restamount]
        }
        return dfs(amount)
};

適合dsf的題目

leetcode46. 全排列

求解可能性的題目,非常適合用dfs.
如果用dp,程式碼會很難組織

var permute = function(nums) {
    //dfs  
    /**
        dp(n) = dp(n-1)陣列中每個元素在i位置(0<=i<=n)新增n
     */
    const dfs=(nums)=>{ // [1,2]
        if(nums.length===1) return [nums.slice(0,1)]
        const arr = dfs(nums.slice(0,-1))
        const res = []
            arr.forEach(item=>{
                for(var i=0;i<=item.length;i++){
                    let addedItem = item.slice()
                    addedItem.splice(i,0,nums[nums.length-1])
                    res.push(addedItem)

                }
            })
        return res
    }
    return dfs(nums)
};

適合dp的題目

leetcode198. 打家劫舍

這個題目用dp解就很清爽,當然遞推公式包含貪心思想。

var rob = function(nums) {
    //dp(n) = max(dp(n-2)+nums[n] , dp(n-1))    總共有N家,貪心。從左往右輪到第N家的時候的收益

    const dp = new Array(nums.length).fill(0)
    dp[1] = nums[0]
    for(let i=2;i<=nums.length;i++){
        dp[i] = Math.max(dp[i-2]+nums[i-1] , dp[i-1]) //輪到第i家的收益
    }

    return dp[nums.length]
};

相關文章