目录
leetcode70-爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
思路:使用动态规划,自底向上。
爬i阶台阶的可能结果等于,爬i-1阶台阶的可能结果+i-2阶台阶的可能结果。
class Solution {
private int[] memo;
public int climbStairs(int n) {
memo = new int[n+1];
return calcWays(n);
}
//n表示爬n阶台阶
private int calcWays(int n){
// if(n==0||n==1){
// return 1;
// }
// if(memo[n]==0){
// return calcWays(n-1)+calcWays(n-2);
// }
// return memo[n];
memo[0] = 1;
memo[1] = 1;
for(int i=2;i<=n;i++){
//爬i阶台阶的可能结果等于,爬i-1阶台阶的可能结果+i-2阶台阶的可能结果。
memo[i] = memo[i-1]+memo[i-2];
}
return memo[n];
}
}
leetcode343-整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。
思路:对于一个数i,遍历其可能的分解为两个数的结果,然后比较直接将两个数相乘,和将一个数与将后面一个数分解后的最大乘积相乘,取较大的数。
class Solution {
public int integerBreak(int n) {
//memo[i]表示整数i拆分可得到的最大乘积
int[] memo = new int[n+1];
memo[0] = 1;
memo[1] = 1;
for(int i=2;i<=n;i++){
//将i拆分成j和i-j,比较两个数直接相乘和j和i-j的最大乘积相乘,还有原来memo[i]的指哪个比较大,取最大值
for(int j=1;j<=i-1;j++){
memo[i] = max(memo[i],j*(i-j),j*memo[i-j]);
}
}
return memo[n];
}
private int max(int a,int b,int c){
return Math.max(a,Math.max(b,c));
}
}
leetcode198-打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
思路:偷盗到第i家的时候,比较不偷盗这家所能获取的最多的钱和偷盗所能获取最多的钱,返回最大值。
class Solution {
public int rob(int[] nums) {
if(nums.length==0){
return 0;
}
//memo[i]表示偷盗到第i家所能获得的最大金额
int[] memo = new int[nums.length+1];
memo[0] = 0;
memo[1] = nums[0];
//i表示偷盗到第i家,取不偷盗这家和偷盗这家两种情况下所能获得的最大值
for(int i=2;i<=nums.length;i++){
memo[i] = Math.max(memo[i-1],nums[i-1]+memo[i-2]);
}
return memo[nums.length];
}
}
0-1背包问题
题目:https://www.acwing.com/problem/content/description/2/
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
- DFS解法
B(k,w)表示考虑前k个物品在背包容量为w的情况下,能取得的最大价值。当考虑第k个物品的时候,比较加入这个物品后背包获取的价值和不加入这个物品背包获得的最大价值。取两个值中的最大值作为返回结果,为了减少递归过程的重复计算,使用memo数组来记录已经计算出的结果,若对应的memo不为0,则直接返回memo值。
//memo[i][j],表示放第i件物品时,背包剩余的容量,若第i件物品的重量大于背包剩余的容量,则不能放入。memo[i][j]=memo[i-1][j]
//若第i件物品的重量小于等于背包剩余的容量,那说明可以放入,比较放入这件物品取得的值v[i]+memo[i-1][j-w[i]]和不放入这件
//物品取得的值memo[i-1][j].memo[i][j]就等于这两个值中的最大值
private int[][] memo;
/**
* @param w 商品的重量
* @param v 商品的价值
* @param c 背包的容量
* @return
*/
public int knapsack01(int[] w, int[] v, int c){
int n = w.length;
memo = new int[n][c+1];
//从最后一个物品开始放
return bestValue(w,v,n-1,c);
}
//DFS-自顶向下
private int bestValue(int[] w,int[] v,int index,int c){
//当没有物品和背包容量为0的时候,返回0
if(index<0||c<=0){
return 0;
}
if(memo[index][c]!=0){
return memo[index][c];
}
//不放第index元素所能取得的最大结果
int res = bestValue(w,v,index-1,c);
if(w[index]<=c){
res = Math.max(res,v[index]+bestValue(w,v,index-1,c-w[index]));
}
memo[index][c] = res;
return res;
}
- 动态规划解法
核心就是上面这个数组,横向为背包的容量,纵向为可以放进去的元素。memo[i][j]表示当背包容量为j的时候放入前i个物品所能获得的最大值。在计算新的位置的值的时候,还是考虑放入目标物品和不放目标物品哪个值比较大。
private int[][] memo;
public int knapsack01(int[] w, int[] v, int c){
if(w.length!=v.length||c<=0){
return 0;
}
memo = new int[w.length][c+1];
//初始化memo[0][j],如果j大于第1个物品的重量,则可以放入
for(int j=0;j<c;j++){
if(j>=w[0]){
memo[0][j] = v[0];
}else{
memo[0][j] = 0;
}
}
//从第一个物品开始接着放入背包
for(int i=1;i<w.length;i++){
for(int j=0;j<=c;j++){
//考虑不讲第i个物品放入背包
memo[i][j] = memo[i-1][j];
if(w[i]<=j){
memo[i][j] = Math.max(memo[i][j],v[i]+memo[i-1][j-w[i]]);
}
}
}
return memo[w.length-1][c];
}
- 一维数组动态规划解法
我们在用二维动态规划解决背包问题的时候,对于放入第i个元素会依赖第i-1个元素memo[i-1]和memo[i-1][c-w[i]]的值。第i个元素只依赖其左边的值。我们让背包的容量从最大开始减小,内层循环倒过来,当我们计算memo[j]的时候,就是计算前i个元素在背包容量为j的况下的最大价值。这时的memo[j]就是上一层最大的memo[j],memo[j-w[i]]就为加入后的最大价值。总的来说就是不要破坏左边的参数。
private int[] memo;
public int knapsack01(int[] w, int[] v, int c){
if(w.length!=v.length||c<=0){
return 0;
}
memo = new int[c+1];
//初始化memo
for(int j=0;j<=c;j++){
if(j>=w[0]){
memo[j] = v[0];
}else{
memo[j] = 0;
}
}
memo = new int[c+1];
for(int i=1;i<w.length;i++){
for(int j=c;j>=w[i];j--){
memo[j] = Math.max(memo[j],v[i]+memo[j-w[i]]);
}
}
return memo[c];
}
总结,只要明白了上图中二维数组的含义,就可以进行递推。
完全背包问题
https://blog.csdn.net/w8253497062015/article/list/1?
题目:https://www.acwing.com/problem/content/3/
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
- 基于01问题的动态规划解法。增加一重循环。来表示第i个元素取k次所有的可能结果,取最大值。
重点部分
for(int i=1;i< w.length;i++){
for(int j=0;j<=c;j++){
//不加入第i个元素的时候的价值
memo[i][j] = memo[i-1][j];
int k=1;
//第i个元素可以取k次
while (k*w[i]<=j){
memo[i][j] = Math.max(memo[i][j],v[i]*k+memo[i-1][j-k*w[i]]);
k++;
}
}
}
完整代码
/**
* 基于01的动态规划解法,多加一重循环
* @param w
* @param v
* @param c
* @return
*/
private int[][] memo;
public int fullKnapsack(int[] w, int[] v, int c){
if(w.length!=v.length||c<=0){
return 0;
}
memo = new int[w.length][c+1];
for(int j=0;j<=c;j++){
if(j>=w[0]){
memo[0][j] = v[0];
}else{
memo[0][j] = 0;
}
}
for(int i=1;i< w.length;i++){
for(int j=0;j<=c;j++){
//不加入第i个元素的时候的价值
memo[i][j] = memo[i-1][j];
int k=1;
//第i个元素可以取k次
while (k*w[i]<=j){
memo[i][j] = Math.max(memo[i][j],v[i]*k+memo[i-1][j-k*w[i]]);
k++;
}
}
}
return memo[w.length-1][c];
}
- 基于动态规划的改进
重点在于:memo[i][j] = Math.max(memo[i][j],memo[i][j-w[i]]+v[i]);后面的不是memo[i-1][j-w[i]]+v[i]而是memo[i][j-w[i]]+v[i],是因为处理完这个物品之后,接下来可能会加上相同的物品,所以寻找装入这个物品后的价值的时候,i不减1,表示还可以再装入这个物品。
for(int i=1;i< w.length;i++){
for(int j=0;j<=c;j++){
//不加入第i个元素的时候的价值
memo[i][j] = memo[i-1][j];
if(j>=w[i]){
memo[i][j] = Math.max(memo[i][j],memo[i][j-w[i]]+v[i]);
}
}
}
return memo[w.length-1][c];
- 同样可以转换为一维数组
for(int i=1;i<w.length;i++){
for(int j=w[i];j<=c;j++){
memo1[j] = Math.max(memo1[j],v[i]+memo1[j-w[i]]);
}
}
根据状态转移公式:f[i][j] = Math.max(f[i-1][j],f[i][j-v[i]]+w[i]),当我们走到第i次循环的第j个点时,f[j]在赋值之前就是f[i-1][j],且因为j-v[i]< j,因此f[j-v[i]]必然在f[j]之前被计算,所以f[j-v[i]]等价于f[i][j-v[i]],这样就清楚了,我们其实可以直接把二维动态规划换成一维就行了,不用向01背包问题一样倒序。
倒序的物理意义代表只能选择一次,顺序的物理意义代表可以选择多次。
leetcode416-分割等和字串
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
思路:先遍历数组求出其所有元素的和,然后使用动态规划,看是否能找到其中的元素让其和为数组和的一半。
在动态规划的时候,我们对于目标和,memo[i]表示能否找到元素使其和为i。对于当前的元素,尝试不放入该元素和放入该元素,有一个能满足memo[i]等于true,说明能找到和为i的几个元素。
class Solution {
public boolean canPartition(int[] nums) {
if(nums.length==0){
return false;
}
int sum = 0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
if(sum%2!=0){
return false;
}
int C = sum/2;
boolean[] memo = new boolean[C+1];
//memo[j]表示容量为j时是否可以完全填满
for(int j=0;j<=C;j++){
memo[j] = (nums[0]==j);
}
for(int i=1;i<nums.length;i++){
for(int j=C;j>=nums[i];j--){
memo[j] = memo[j]||memo[j-nums[i]];
}
}
return memo[C];
}
}
leetcode300-最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
思路:memo[i]代表[0...i]区间内能获得的最长上升子序列。memo[i]取不将第i个元素放入这个序列中和将第i个元素放入序列中(第i个元素大于前一个元素)的最大值。第i个元素放入序列中的最大值-遍历一遍memo[i],对于从[0...i-1]的数,取memo[j]+1的最大值。
public int lengthOfLIS(int[] nums) {
if(nums.length==0){
return 0;
}
//memo[i]代表,在[0...i]区间内所取得的最长上升子序列
int[] memo = new int[nums.length];
//当只有一个数字的时候,memo[i]都为1
for(int i=0;i<nums.length;i++){
memo[i] = 1;
}
for(int i=1;i<nums.length;i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
memo[i] = Math.max(memo[i],1+memo[j]);
}
}
}
//最后遍历memo,取出最大值
int res = 1;
for(int k=0;k<nums.length;k++){
res = Math.max(res,memo[k]);
}
return res;
}