2019 算法面试相关(leetcode)--动态规划之背包问题

2019 iOS面试题大全---全方面剖析面试
2018 iOS面试题---算法相关
1、七种常见的数组排序算法整理(C语言版本)
2、2019 算法面试相关(leetcode)--数组和链表
3、2019 算法面试相关(leetcode)--字符串
4、2019 算法面试相关(leetcode)--栈和队列
5、2019 算法面试相关(leetcode)--优先队列
6、2019 算法面试相关(leetcode)--哈希表
7、2019 算法面试相关(leetcode)--树、二叉树、二叉搜索树
8、2019 算法面试相关(leetcode)--递归与分治
9、2019 算法面试相关(leetcode)--贪心算法
10、2019 算法面试相关(leetcode)--动态规划(Dynamic Programming)
11、2019 算法面试相关(leetcode)--动态规划之背包问题


背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkel和Hellman提出的。
常见的背包问题:01背包完全背包多重背包

一、01背包问题

有M个物品,每个物品的重量为W[i],每个物品的价值为V[i]。
现在有一个背包,它所能容纳的重量为N
问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?


每个物品无非是装入背包或者不装入背包,即每个物品的取值无非就是0或者1,故而这题被称为01背包问题。
我们可以用动态规划的思路,在循环过程中比较第i个物品取与不取的大小,取其中大的一个,依次循环即可。
其状态转移方程为:dp[j] = Math.max(V[i] + dp[j - W[i]],dp[j]) ,表示背包已装容量为j时,背包里所装物品的最大总价值
其约束条件是:∑(num[i]*w[i]) <= N,其中num指第i个物品取的数量,取值为0或1,所以我们要有一个嵌套遍历。
需要注意的是,因为每个物品数量只有一个,即最多只能取一次,所以在遍历背包容量N的时候需倒序遍历,这样能保证不会重复添加物品

var zeroOneBag = function(W,V,N){

    let dp = Array(N + 1).fill(0)

    for(let i = 0; i < W.length; i++){
        
        for(let j = N; j >= w[i]; j--){

            dp[j] = Math.max(V[i] + dp[j - W[i]],dp[j]) 
        }
    }

    return dp[N]
}

二、完全背包问题

有M个物品,每种物品有无限个,每个物品的重量为weight[i],每个物品的价值为value[i]。
现在有一个背包,它所能容纳的重量为N
问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?


完全背包问题和01背包问题很像,解决思路也和上边的差不多,区别就在于每种物品不限,即约束条件有变化:∑(num[i]*w[i]) <= N,其中num指第i个物品取的数量,取值为非负整数
所以我们只要正序遍历即可

var completelyBag = function(W,V,N){

    let dp = Array(N + 1).fill(0)

    for(let i = 0; i < W.length; i++){
        
        for(let j = W[i]; j <= N; j++){
            
            dp[j] = Math.max(V[i] + dp[j - W[i]],dp[j])
        }
    }
    return dp[N]
}

这种方法的时间复杂度:O(M*N),空间复杂度:O(N)

  • 因为每个背包数量不限制,这里其实没必要用动态规划,还有一种排序解法:V[i]/W[i]表示每点重量所提供的价值,即价重比。按价重比从大到小排序,然后依次遍历,直至背包不能再塞
    这种方法的时间复杂度则是O(MlogM),空间复杂度是O(M),是要优于动态规划的
/**
 * 物品对象
 *
 * @param {*} w 物品重量
 * @param {*} v 物品价值
 * @param {*} r 物品价重比
 */
function Goods(w,v,r) {
    this.w = w;
    this.v = v;
    this.r = r;
}
var completelyBag2 = function(W,V,N){

    let GoodsArr = []

    for(let i = 0; i < W.length; i++){

        GoodsArr[i] = new Goods(W[i],V[i],V[i]/W[i])
    }

    GoodsArr.sort((a,b) => b.r - a.r)  //将物品按价重比从大到小排序

    let res = 0

    for(let i = 0; i < GoodsArr.length; i++){
        
        let goods = GoodsArr[i]

        res += Math.floor(N/goods.w)*goods.v

        N = N%goods.w

        if(N == 0) return res
    }

    return res
}

三、多重背包问题

有N个物品,每种物品有nums[i]个,每个物品的重量为weight[i],每个物品的价值为value[i]。
现在有一个背包,它所能容纳的重量为V,
问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?


多重背包问题多了一个变量nums[i],但状态转移方程还是和上边差不多的
dp[j] = Math.max(kV[i] + dp[j - kW[i]],dp[j]),k即是指第i个物品取的数量,取值为0-nums[i]
因为不是无限,所以我们还是需要和01背包问题一样,去倒序遍历背包容量N
约束条件:∑(num[i]*w[i]) <= N,其中num指第i个物品取的数量,取值为0-nums[i]
所以我们在遍历的时候就需要再嵌套一层,添加变量:Math.min(~~(j/W[i]),nums[i]) ,表示第i件物品在容量为j的背包里可以带走的最大数量

var multipleBag = function(W,V,N,nums){

    let dp = Array(N + 1).fill(0)

    for(let i = 0; i < W.length; i++){
        
        for(let j = N; j >= w[i]; j--){
            
            let count = Math.min(~~(j/W[i]),nums[i])    //第i件物品在容量为j的背包里可以带走的最大数量

            for(let k = 0; k <= count; k++){
                
                dp[j] = Math.max(k*V[i] + dp[j - k*W[i]],dp[j])
            }
        }
    }

    return dp[N]
 }
w = [5 ,4 ,7 ,2 ,6]

v = [12 ,3 ,10, 3 ,6]

nums = [2,4,1,5,3]

console.log(zeroOneBag(w,v,15))

console.log(completelyBag(w,v,15))

console.log(completelyBag2(w,v,15))

console.log(multipleBag(w,v,15,nums))

输出结果分别是:25,36,36,30


leetcode上并没有背包问题的题目,但有其类似的题目。

一、零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:

输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。


比较类似完全背包问题,只不过这种变成求最少数量。
其状态转移方程为dp[i] = Math.min(dp[i - c] + 1,dp[i]),dp[i]表示总金额为i时所需硬币的最小数量,如果为Infinity即无穷大时则说明无法凑成i;c为硬币数组中的元素,表示如果i比c大,可以使用c这个硬币加上dp[i - c]的数量凑成金额i,取其最少数量即可
和完全背包问题一样,这里也是正序遍历的

var coinChange = function(coins, amount) {
    
    let dp = Array(amount + 1).fill(Infinity)

    dp[0] = 0

    for (const c of coins) {

        for(let i = c; i < dp.length; i++){
        
            dp[i] = Math.min(dp[i - c] + 1,dp[i]) 
        }
    }

    return dp[amount] == Infinity ? -1 : dp[amount]
};
二、 一和零

在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

注意:

给定 0 和 1 的数量都不会超过 100。
给定字符串数组的长度不会超过 600。
示例 1:

输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4

解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
示例 2:

输入: Array = {"10", "0", "1"}, m = 1, n = 1
输出: 2

解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。


这是一道比较典型的01背包问题。
只不过约束条件变成了m和n两个,那么dp就会是一个二维数组dp[m][n]
其动态转移方程为:dp[i][j] = Math.max(dp[i - zeros][j - ones] + 1,dp[i][j])

var findMaxForm = function(strs, m, n) {
  
    let dp = Array(m + 1)

    for(let i = 0; i < m + 1; i++){
        
        dp[i] = Array(n + 1).fill(0)
    }

    for (const str of strs) {
        
        let zeros = ones = 0

        for(let i = 0; i < str.length; i++){
            
            if(str[i] == '0') zeros++

            else ones++
        }

        for(let i = m; i >= zeros; i--){
            
            for(let j = n; j >= ones; j--){
                
                dp[i][j] = Math.max(dp[i - zeros][j - ones] + 1,dp[i][j])
            }
        }
    }

    return dp[m][n]
};

你可能感兴趣的:(2019 算法面试相关(leetcode)--动态规划之背包问题)