leetcode算法刷题的第三十二天

1.完全背包问题---二维dp数组

题目描述

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。

小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

输入描述

第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量

接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值

输出描述

输出一个整数,表示最大价值。

输入示例
复制代码
4 5
1 2
2 4
3 4
4 5
输出示例
复制代码
10
提示信息

第一种材料选择五次,可以达到最大值。

数据范围:

1 <= n <= 10000;

1 <= v <= 10000;

1 <= wi, vi <= 10^9.

cpp 复制代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main(){

    int n,bagWeight;
    int w,v;
    cin>>n>>bagWeight;
    vector<int> weight(n);
    vector<int> value(n);

    for(int i=0;i<n;i++){
        cin>>weight[i]>>value[i];
    }

    vector<vector<int>> dp(n,vector<int>(bagWeight+1,0));

    //初始化
    for(int j=weight[0];j<=bagWeight;j++){
        dp[0][j]=dp[0][j-weight[0]]+value[0];
    }

    for(int i=1;i<n;i++){//遍历物品
        for(int j=0;j<=bagWeight;j++){//遍历背包容量
            if(j<weight[i]) dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);
        }
    }
    cout<<dp[n-1][bagWeight]<<endl;


    return 0;
}

思路总结:

第一,确定dp数组以及下标的含义

dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少

第二,确定递推公式

递推公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])

第三,dp数组如何初始化

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

j >= weight[0]时,dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个

dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?

其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

但只不过一开始就统一把dp数组统一初始为0,更方便一些。

第四,确定遍历顺序

可以先遍历物品再遍历背包

也可以先遍历背包再遍历物品

第五,举例推导dp数组

2.leetcode 518.零钱兑换II

题目链接

cpp 复制代码
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int bagSize=amount;
        vector<vector<uint64_t>> dp(coins.size(),vector<uint64_t>(bagSize+1,0));

        //初始化最上面那一行
        for(int j=0;j<=bagSize;j++){
            if(j%coins[0]==0) dp[0][j]=1;
        }

        //初始化最左列
        for(int i=0;i<coins.size();i++){
            dp[i][0]=1;
        }

        //以下遍历顺序行列可以颠倒
        for(int i=1;i<coins.size();i++){//行,遍历物品
            for(int j=0;j<=bagSize;j++){//列,遍历背包
                if(coins[i]>j) dp[i][j]=dp[i-1][j];
                else dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]];
            }
        }
        return dp[coins.size()-1][bagSize];
    }
};

思路总结:

第一,确定dp数组以及下标的含义

定义二维dp数值 dp[i][j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。

第二,确定递推公式

本题递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]] ,区别依然是 dp[i - 1][j - nums[i]]dp[i][j - nums[i]]

第三, dp数组如何初始化

那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础

这里首先要关注的就是 dp[0][0] 应该是多少?

背包空间为0,装满「物品0」 的组合数有多少呢?

应该是 0 个, 但如果 「物品0」 的 数值就是0呢? 岂不是可以有无限个0 组合 和为0!

题目描述中说了1 <= coins.length <= 300 ,所以不用考虑 物品数值为0的情况。

那么最上行dp[0][j] 如何初始化呢?

dp[0][j]的含义:用「物品0」(即coins[0]) 装满 背包容量为j的背包,有几种组合方法。

如果 j 可以整除 物品0,那么装满背包就有1种组合方法。

最左列如何初始化呢?

dp[i][0] 的含义:用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。

都有一种方法,即不装。

所以 dp[i][0] 都初始化为1

第四,确定遍历顺序

二维DP数组的完全背包的两个for循环先后顺序是无所谓的。

先遍历背包,还是先遍历物品都是可以的。

第五,打印dp数组

dp数组应该是这样的:

复制代码
1 0 1 0 1 0
1 0 1 1 1 1
1 0 1 1 1 2

3.leetcode 377.组合总和IV

题目链接

cpp 复制代码
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target + 1, 0);
        dp[0] = 1;
        for (int i = 0; i <= target; i++) { // 遍历背包
            for (int j = 0; j < nums.size(); j++) { // 遍历物品
                if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
};

思路总结:这道题的答案是这样的,但是力扣里面过不了,是因为C++测试用例有两个数相加超过int的数据。

第一,确定dp数组以及下标的含义

dp[i]: 凑成目标正整数为i的排列个数为dp[i]

第二,确定递推公式

dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。

因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。

求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];

本题也一样。

第三,dp数组的初始化

因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。

至于dp[0] = 1 有没有意义呢?

其实没有意义,所以我也不去强行解释它的意义了,因为题目中也说了:给定目标值是正整数! 所以dp[0] = 1是没有意义的,仅仅是为了推导递推公式。

至于非0下标的dp[i]应该初始为多少呢?

初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。

第四,确定遍历顺序

个数可以不限使用,说明这是一个完全背包。

得到的集合是排列,说明需要考虑元素之间的顺序。

本题要求的是排列,那么这个for循环嵌套的顺序可以有说法了。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!

所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历

第五,举例推导dp数组

求装满背包有几种方法,递归公式都是一样的,没有什么差别,但关键在于遍历顺序!

如果对遍历顺序没有深度理解的话,做这种完全背包的题目会很懵逼,即使题目刷过了可能也不太清楚具体是怎么过的。

4.爬楼梯(进阶版)

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

输入描述

输入共一行,包含两个正整数,分别表示n, m

输出描述

输出一个整数,表示爬到楼顶的方法数。

输入示例
复制代码
3 2
输出示例
复制代码
3
提示信息

数据范围:

1 <= m < n <= 32;

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

1 阶 + 1 阶 + 1 阶段

1 阶 + 2 阶

2 阶 + 1 阶

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
int main(){

    int n,m;
    while(cin>>n>>m){
        vector<int> dp(n+1,0);
        dp[0]=1;
        for(int i=1;i<=n;i++){//遍历背包
            for(int j=1;j<=m;j++){//遍历物品
                if(i-j>=0) dp[i]+=dp[i-j];
            }
        }
        cout<<dp[n]<<endl;
    }

    return 0;
}

思路总结:

第一,确定dp数组以及下标的含义

dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法

第二,确定递推公式

本题呢,dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]

那么递推公式为:dp[i] += dp[i - j]

第三,dp数组如何初始化

既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。

下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果

第四,确定遍历顺序

这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!

所以需将target放在外循环,将nums放在内循环。

每一步可以走多次,这是完全背包,内循环需要从前向后遍历。

第五,举例推导dp数组

本题看起来是一道简单题目,稍稍进阶一下其实就是一个完全背包!

如果我来面试的话,我就会先给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。

顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。

这就能考察对背包问题本质的掌握程度,候选人是不是刷题背公式,一眼就看出来了。

这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。

本题代码不长,题目也很普通,但稍稍一进阶就可以考察完全背包,而且题目进阶的内容在leetcode上并没有原题,一定程度上就可以排除掉刷题党了,简直是面试题目的绝佳选择!

相关推荐
Greedy Alg2 小时前
LeetCode 142. 环形链表 II
算法
先做个垃圾出来………2 小时前
残差连接的概念与作用
人工智能·算法·机器学习·语言模型·自然语言处理
乔宕一4 小时前
stm32 链接脚本没有 .gcc_except_table 段也能支持 C++ 异常
c++·stm32·嵌入式硬件
SuperCandyXu4 小时前
P3205 [HNOI2010] 合唱队-普及+/提高
c++·算法·洛谷
_OP_CHEN4 小时前
数据结构(C语言篇):(十二)实现顺序结构二叉树——堆
c语言·数据结构·算法·二叉树·学习笔记··顺序结构二叉树
_君落羽_4 小时前
ARM寄存器以及异常处理
c++
Yingjun Mo5 小时前
1. 统计推断-基于神经网络与Langevin扩散的自适应潜变量建模与优化
人工智能·神经网络·算法·机器学习·概率论
free5 小时前
基于librdkafa C++客户端生产者发送数据失败问题处理#2
c++·kafka
小柯J桑_5 小时前
Linux:线程封装
linux·运维·c++