动态规划讲解

https://tangshusen.me/2019/11/24/knapsack-problem/

 

https://mp.weixin.qq.com/s?src=11&timestamp=1596210639&ver=2494&signature=ebLRWzj10GYY8W5wWUipHYsbdDQIO7q-NwsWE7QNR52R67ZrtYBrX7oaXp3bvDtR3xXCoqpJbAc9NooN0fijlMufdXxT-MhpIRfAm30g1Fek7gVDFGO1t6s0LuFYtWiV&new=1

 

0/1背包

//01背包问题伪代码(空间优化版)
dp[0,...,W]=0;
for i=1,...,N
   for j=W,...w[i]//必须逆向枚举!!!
       dp[j]=max(dp[j],dp[j-w[i]]+v[i])
       
//完全背包问题思路伪代码(空间优化)
dp[0,...,W]=0;
for i=1,...,N
   for j=w[i],...,W//必须正向枚举!!!
       dp[j]=max(dp[j],dp[j-w[i]]+v[i])
//恰好装满要注意初始化值,重量为0时最大价值为0,dp[0]=0;而当重量不为0时,暂时不知道最大价值,但是由于后面要max,所以初始化Integer.MIN_VALUE;
//求方案数总和

 

时间复杂度O(NW),空间复杂度O(W)

package com.dj;


import java.util.Arrays;

public class test {
   // 所有的物品
   private Knapsack[] bags;
   // 物品的数量
   private int n;
   // 背包总承重
   private int totalWeight;
   // 第一维:当前第几个物品;第二维:当前的背包承重;值:当前背包最大价值
   private int[][] bestValues;
   private int[] res;
   // 最终背包中最大价值
   private int bestValue;

   public test(Knapsack[] bags, int totalWeight) {
       this.bags = bags;
       this.totalWeight = totalWeight;
       this.n = bags.length;
       if (bestValues == null) {
           // 考虑0的状态+1,防止数组角标越界
           bestValues = new int[n + 1][totalWeight + 1];
      }
       if(res==null){
           res=new int[totalWeight+1];
      }
  }
   public void solve2(){
       //对一维数组F进行初始化,即当背包不放入物品,其最大价值均为0
       for(int i=0;i<=totalWeight;i++){
           res[i]=0;
      }
       for(int i = 1; i <= n; i++){
           for(int j=totalWeight;j>=0;j--){
               if(j>=bags[i-1].getWeight()){
                   res[j]=Math.max(res[j],res[j-bags[i-1].getWeight()]+bags[i-1].getValue());
              }else{//可省略
                   res[j]=res[j];
              }
          }
      }
       System.out.println(res[totalWeight]);
  }
   public void solve() {
       // 遍历背包的承重
       for (int j = 0; j <= totalWeight; j++) {
           // 遍历指定物品
           for (int i = 0; i <= n; i++) {
               // 当背包不放入物品或承重为0时,其最大价值均为0
               if (i == 0 || j == 0) {
                   bestValues[i][j] = 0;
              } else {
                   // 如果第 i个物品重量大于总承重,则最优解存在于前 i-1 个背包中
                   if (j < bags[i - 1].getWeight()) {
                       bestValues[i][j] = bestValues[i - 1][j];
                  } else {
                       // 如果第 i个物品不大于总承重,则最优解要么是包含第 i个背包的最优解,
                       // 要么是不包含第 i个背包的最优解, 取两者最大值
                       int weight = bags[i - 1].getWeight();
                       int value = bags[i - 1].getValue();
                       bestValues[i][j] = Math.max(bestValues[i - 1][j], value
                               + bestValues[i - 1][j - weight]);
                  }
              }
          }
      }

       bestValue = bestValues[n][totalWeight];
  }

   public int getBestValue() {
       return bestValue;
  }

   public static void main(String[] args) {
       Knapsack[] bags = new Knapsack[] { new Knapsack(2, 13),
               new Knapsack(1, 10), new Knapsack(3, 24), new Knapsack(2, 15),
               new Knapsack(4, 28), new Knapsack(5, 33), new Knapsack(3, 20),
               new Knapsack(1, 8) };
       int totalWeight = 12;
       test problem = new test(bags, totalWeight);
       problem.solve();
       System.out.println(problem.getBestValue());
       problem.solve2();
  }
}

class Knapsack{
   /** 物品重量 */
   private int weight;
   /** 物品价值 */
   private int value;

   public Knapsack(int weight, int value) {
       this.weight = weight;
       this.value = value;
  }

   public int getWeight() {
       return weight;
  }

   public void setWeight(int weight) {
       this.weight = weight;
  }

   public int getValue() {
       return value;
  }

   public void setValue(int value) {
       this.value = value;
  }

}

石头碰撞问题

链接:https://www.nowcoder.com/questionTerminal/9dd19c9305704138bdf83e2dffdcb4f4?f=discussion 来源:牛客网

 

给定一组石头,每个石头有一个正数的重量。每一轮开始的时候,选择两个石头一起碰撞,假定两个石头的重量为x,y,x<=y,碰撞结果为

  1. 如果x==y,碰撞结果为两个石头消失

  2. 如果x != y,碰撞结果两个石头消失,生成一个新的石头,新石头重量为y-x

    最终最多剩下一个石头为结束。求解最小的剩余石头质量的可能性是多少。

输入描述:
第一行输入石头个数(<=100)

第二行输入石头质量,以空格分割,石头质量总和<=10000
输出描述:
最终的石头质量

示例1

输入
6
2 7 4 1 8 1
输出
1
链接:https://www.nowcoder.com/questionTerminal/9dd19c9305704138bdf83e2dffdcb4f4?f=discussion
来源:牛客网

package com.dj;

import java.util.*;


public class test {

   public static void main(String[] args){
       Scanner sc = new Scanner(System.in);
       int n = sc.nextInt();
       sc.nextLine();
       int[] stones = new int[n] ;
       for(int i = 0; i < n;i++){
           stones[i] = sc.nextInt();
      }
       int ans = lastStoneWeightII(stones);
       System.out.println(ans);
       int res=helper(stones);
       System.out.println(res);
  }

   public static int helper(int[] stones){
       int len = stones.length;
       /* 获取石头总重量 */
       int totalStone = 0;
       for (int i : stones) {
           totalStone += i;
      }

       int maxCapacity = totalStone / 2;
       int[] dp = new int[totalStone+1];
       for(int i = 1; i < len + 1; i++){
           for (int j = maxCapacity; j >= 1; j--){
               if (j - stones[i - 1] < 0){
                   dp[j]=dp[j];
              }else{
                   dp[j]=Math.max(dp[j],dp[j-stones[i-1]]+stones[i-1]);
              }
          }
      }
       return totalStone - 2 * dp[maxCapacity];
  }
   public static int lastStoneWeightII(int[] stones) {
       /*
        1.换一种想法,就是将这些数字分成两拨,使得他们的和的差最小
        2.因此可以简单地把所有石头看作两堆,假设总重量为 sum,
        则问题转化为背包问题:如何使两堆石头总重量接近 sum / 2
        3.两堆石头的价值(重量)都只能接近
        */
       int len = stones.length;
       /* 获取石头总重量 */
       int totalStone = 0;
       for (int i : stones) {
           totalStone += i;
      }

       int maxCapacity = totalStone / 2;
       /*
        1.这个是0-1背包的dp定义,dp[i][w]: 前i个商品,当容量为w时,最大价值为dp[i][w]
        2.对照一下此题dp数组定义,dp[i][j]: 前i个石头,当容量为j时,最大重量为dp[i][j]
       */
       int[][] dp = new int[len + 1][totalStone + 1];


       for (int i = 1; i < len + 1; i++) {
           for (int j = 1; j < maxCapacity + 1; j++) {
               if (j - stones[i - 1] < 0) {
                   dp[i][j] = dp[i - 1][j];
              } else {
                   dp[i][j] = Math.max(dp[i - 1][j - stones[i - 1]] + stones[i - 1], dp[i - 1][j]);
              }
          }
      }
       return totalStone - 2 * dp[len][maxCapacity];

  }
}

二维01背包问题求最大价值

474. 一和零

难度中等185

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

现在,假设你分别支配着 m0n1。另外,还有一个仅包含 01 字符串的数组。

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

注意:

  1. 给定 01 的数量都不会超过 100

  2. 给定字符串数组的长度不会超过 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"

分析:题目给定一个仅包含0和1字符串的数组。任务是从数组中选取尽可能多的字符串,使这些字符串包含的0和1的数目分别不超过m和n。

我们把每个字符串看做是一件物品,把字符串中0的数目和1的数目看做是两种“重量”,所以就变成了一个二维01背包问题,书包的两个限重分别是m和n,要求书包能装下的物品的最大数目(也相当于价值最大,设每个物品的价值为1).

我们提前把每个字符串的两个“重量”w0,w1算出来用数组存放,但是注意到只需要用一次这两个值,所以我们只需在用到的时候计算w0和w1就行了,这样就不用例外的数组存放。

class Solution {
   public int findMaxForm(String[] strs, int m, int n) {
       if(strs==null) return 0;
       int num=strs.length;
       int w0,w1;
       int[][] dp=new int[m+1][n+1];
       for(int i=1;i<=num;i++){
           w0=0;
           w1=0;
           for(char ch:strs[i-1].toCharArray()){
               if(ch=='0') w0++;
               else w1++;
          }

           for(int j=m;j>=w0;j--){
               for(int k=n;k>=w1;k--){
                   dp[j][k]=Math.max(dp[j][k],1+dp[j-w0][k-w1]);
              }
          }
      }
       return dp[m][n];
  }
}

 

恰好装满0/1背包问题计算方案数

dp[i][j]=sum(dp[i-1][j],dp[i][j-w[i]])

494. 目标和

难度中等338

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 +-。对于数组中的任意一个整数,你都可以从 +-中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

  • 数组非空,且长度不会超过 20 。

  • 初始的数组的和不会超过 1000 。

  • 保证返回的最终结果能被 32 位整数存下。

class Solution {
   public int findTargetSumWays(int[] nums, int S) {
       int sum=0;
       for(int x:nums){
           sum+=x;
      }
       if(S>sum||S<-sum) return 0;
       if(((S+sum)&1)==1) return 0;
       int target=(S+sum)>>1;
       int[] dp=new int[target+1];
       dp[0]=1;
       for(int i=1;i<=nums.length;i++){
           for(int j=target;j>=nums[i-1];j--){
               dp[j]=dp[j]+dp[j-nums[i-1]];
          }
      }
       return dp[target];
  }
}

恰好装满完全背包问题求最小价值

322. 零钱兑换

难度中等724

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

示例 1:

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

示例 2:

输入: coins = [2], amount = 3
输出: -1

说明: 你可以认为每种硬币的数量是无限的。

//完全背包问题思路伪代码(空间优化)
dp[0,...,W]=0;
for i=1,...,N
   for j=w[i],...,W//必须正向枚举!!!
       dp[j]=max(dp[j],dp[j-w[i]]+v[i])

分析:如果我们把面值看做物品,面值金额看成是物品的重量,每件物品的价值均为1,这样此题就是一个恰好装满的完全背包问题了。不过这里不是求最多装入多少物品而是求最少,可以将每个物品的价值看成1,求最少价值。

class Solution {
   public int coinChange(int[] coins, int amount) {
       int len=coins.length;
       int[] dp=new int[amount+1];
       Arrays.fill(dp,Integer.MAX_VALUE);
       dp[0]=0;
       
       for(int i=0;i<len;i++){
           for(int j=coins[i];j<=amount;j++){
               if(dp[j-coins[i]]!=Integer.MAX_VALUE){
                   dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
              }
          }
      }
       return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
  }
}

恰好装满0/1完全背包问题求方案数

518. 零钱兑换 II

难度中等207

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

输入: amount = 10, coins = [10] 
输出: 1

 

注意**:**

你可以假设:

  • 0 <= amount (总金额) <= 5000

  • 1 <= coin (硬币面额) <= 5000

  • 硬币种类不超过 500 种

  • 结果符合 32 位符号整数

class Solution {
   public int change(int amount, int[] coins) {
       int len=coins.length;
       int[] dp=new int[amount+1];
       
       dp[0]=1;
       
       for(int i=0;i<len;i++){
           for(int j=coins[i];j<=amount;j++){
               dp[j]=dp[j]+dp[j-coins[i]];
          }
      }
       return dp[amount];
  }
}

恰好01背包问题

416. 分割等和子集

难度中等341

给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

  1. 每个数组中的元素不会超过 100

  2. 数组的大小不会超过 200

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

https://mp.weixin.qq.com/s?src=11&timestamp=1596211005&ver=2494&signature=cLm--axb39CcQxxgdQSwLgGzsZE5pHVZe0QXYdkTIk7XWDLEJgosT-RKJbp19fgD9XvfAokmbuBfjQR6UKWvnGrc77lxwWTKgfJA4JQf3xVHoAHsr-gPh7wD*rDlpOdf&new=1

class Solution {
   public boolean canPartition(int[] nums) {
       int sum=0;
       for(int i=0;i<nums.length;i++){
           sum+=nums[i];
      }
       if((sum&1)==1) return false;
       sum=sum>>1;
       boolean[] dp=new boolean[sum+1];
       Arrays.fill(dp,false);//装不满
       dp[0]=true; //装满了
       for(int i=1;i<=nums.length;i++){
           for(int j=sum;j>=nums[i-1];j--){
               dp[j]=dp[j]||dp[j-nums[i-1]];
          }
      }
       return dp[sum];
  }
}

面试题 08.11. 硬币

难度中等155

硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)

示例1:

 输入: n = 5
输出:2
解释: 有两种方式可以凑成总金额:
5=5
5=1+1+1+1+1

示例2:

 输入: n = 10
输出:4
解释: 有四种方式可以凑成总金额:
10=10
10=5+5
10=5+1+1+1+1+1
10=1+1+1+1+1+1+1+1+1+1

说明:

注意:

你可以假设:

  • 0 <= n (总金额) <= 1000000

class Solution {
   public int waysToChange(int n) {
       int[] nums=new int[]{1,5,10,25};
       int[] dp=new int[n+1];
       dp[0]=1;
       for(int i=0;i<4;i++){
           for(int j=nums[i];j<=n;j++){
               dp[j]=(dp[j]+dp[j-nums[i]])% 1000000007;
          }
      }
       return dp[n];
  }
}

 

posted @ 2020-08-01 11:22  打屎也不熬夜  阅读(174)  评论(0编辑  收藏  举报