动态规划典型问题分析

动态规划(dynamic programming)通过组合子问题的解来求解原问题,动态规划应用于重叠子问题的情况,即公共子问题,把每个子问题的解记录在表格中,下次遇到相同子问题时就不需要再重新计算。
举个很简单的例子理解动态规划,如果问1+1+1+1等于多少,显而易见是4,那么如果在左边写上1+,答案又是多少,你会很快的得出5,因为你把4记录下来了。

斐波那契数列(入门)

已知一个数列F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2),设计一个算法求F(n)。

递归法

首先可以想到递归法

	public static void main(String[] args) {
		//n是输入的值
		int n=5;
        System.out.println(Fi(n));
    }
    
    //递归计算斐波那契数
	private static int Fi(int n) {
        if (n == 1 || n == 2) {
            return 1;
        }
        return Fi(n - 1) + Fi(n - 2);
    }

推荐使用二叉树分析递归算法。
二叉树分析递归算法
从上图可以看出,递归算法在计算时有大量的冗余,即F3被重复计算。那么,可以应用动态规划的思路,记录中间的数据,从而避免重复计算。

动态规划

	public static void main(String[] args) {
		//n为输入的数字
        int n = 5;
        //使用数组记录
        int[] fi = new int[n + 1];
        fi[1] = 1;
        fi[2] = 1;
        for (int i = 3; i <= n; i++) {
            fi[i] = fi[i - 1] + fi[i - 2];
        }
        System.out.println(fi[5]);
    }

爬台阶问题(简单)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:爬到第一个台阶只有1种方法,爬到第二个台阶,可以从第一个台阶出发,也可以从零开始,及可以得到状态转移方程dp(n)=dp(n-1)+dp(n-2),和斐波那契数列一样。

	 public static void main(String[] args) {
        int n = 5;
        int[] fi = new int[n + 2];
        fi[1] = 1;
        fi[2] = 2;
        for (int i = 3; i <= n; i++) {
            fi[i] = fi[i - 1] + fi[i - 2];
        }
        System.out.println(fi[5]);
    }

对于这个问题,还可以再进行优化,只需要记录n-1与n-2的值就可以了,能降低空间复杂度至O(1)。

	public static void main(String[] args) {
        int n = 3;
        int a=0;
        int b=1;
        int sum=0;
        for (int i = 0; i < n; i++) {
            sum=a+b;
            a=b;
            b=sum;
        }
        System.out.println(sum);
    }

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/three-steps-problem-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:同上,只不过多了一种上台阶方式。状态转移方程为dp[n]=dp[n-1]+dp[n-2]+dp[n-3]。改题为了避免超出int上限,可以用long存储数据

	public int waysToStep(int n) {
        long[] dp = new long[n + 2];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % 1000000007;
        }
        return (int)dp[n];
    }

也可以对空间复杂度进行优化,降为O(1)

不同路径(中等)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-paths
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:这是一个二维DP问题,每一个位置,只能从该位置的上一个位置往下移,或者从该位置的左边的位置往右移动。得出状态转移方程dp[n][n]=dp[n-1][n]+dp[n][n-1]

	//输入m,n
	public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        dp[0][0] = 1;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
            	//注意一下边界条件,当i=0或者j=0的时候,不存在i-1或者j-1
                if (i == 0 && j > 0) {
                    dp[i][j] = dp[i][j - 1];
                } else if (j == 0 && i > 0) {
                    dp[i][j] = dp[i - 1][j];
                } else if (i > 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m-1][n-1];
    }

买卖股票(中等)

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/gu-piao-de-zui-da-li-run-lcof/
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:只能买卖一次股票,那么对于每次股票,都可以选择卖或者不卖,如果卖了,那么利润为现在出售的价格减去之前的最低价格,如果不卖,利润为之前的利润。因此得出状态转移方程dp[n]=max(dp[n-1],prices[n]-min)

	public int maxProfit(int[] prices) {
        int n = prices.length;
        if(n==0){
            return 0;
        }
        int[] dp = new int[n + 1];
        dp[0] = 0;
        int min = prices[0];
        for (int i = 1; i < n; i++) {
            min = Math.min(prices[i], min); 
            dp[i] = Math.max(dp[i - 1], prices[i] - min);
        }
        return dp[n - 1];
    }

背包问题

参考分析
参考视频
题目来源

01背包

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

分析:对于每一件物品,都可以选或者不选。得到状态转移方程
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])

import java.util.*;
import java.io.*;

public class Main{
    public static void main(String[] args){
        Scanner s=new Scanner(System.in);
        int N=s.nextInt();
        int V=s.nextInt();
        int[] w=new int[N+1];
        int[] v=new int[N+1];
        for(int i=1;i<=N;i++){
            v[i]=s.nextInt();
            w[i]=s.nextInt();
        }
        int[][] dp=new int[N+1][V+1];
        dp[0][0]=0;
        for(int i=1;i<=N;i++){
            for(int j=0;j<=V;j++){
                dp[i][j]=dp[i-1][j];
                if(j>=v[i]){
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
                }
            }
        }
        System.out.println(dp[N][V]);
    }
}

该算法还可以进行优化,经观察,dp中的i只与i-1有关。可以用一维数组进行存储,降低空间复杂度。

import java.util.*;
import java.io.*;

public class Main{
    public static void main(String[] args){
        Scanner s=new Scanner(System.in);
        int N=s.nextInt();
        int V=s.nextInt();
        int[] w=new int[N+1];
        int[] v=new int[N+1];
        for(int i=1;i<=N;i++){
            v[i]=s.nextInt();
            w[i]=s.nextInt();
        }
        int[] dp=new int[V+1];
        for(int i=1;i<=N;i++){
            for(int j=V;j>=v[i];j--){
                    dp[j]=Math.max(dp[j],dp[j-v[i]]+w[i]);
            }
        }
        System.out.println(dp[V]);
    }
}

分割等和子集(中等)

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:每个数只能选一次,相当于01背包问题。与01背包的区别在于需要凑出背包容量一半的物品。

class Solution {
    public boolean canPartition(int[] nums) {
        int len = nums.length;
        int total=0;
        if(len==0){
            return false;
        }
        for(int i=0;i<len;i++){
            total+=nums[i];
        }
        if(total%2==1){
            return false;
        }else{
            total/=2;
        }  
        boolean[][] dp=new boolean[len][total+1];
        for(int i=0;i<len;i++){
            if(nums[i]<=total)
            dp[i][nums[i]]=true;
        }
        dp[0][0]=true;
        for(int i=1;i<len;i++){
            for(int j=0;j<=total;j++){
                dp[i][j]=dp[i-1][j];
                if(j-nums[i]==0){
                    dp[i][j]=dp[i-1][j-nums[i]];
                    continue;
                }
                if(j-nums[i]>0){
                    dp[i][j]=dp[i-1][j] || dp[i-1][j-nums[i]];
                }
            }
        }
        return dp[len-1][total];
    }
}                                              

完全背包

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10

import java.util.*;
import java.io.*;

public class Main{
    public static void main(String[] args){
        Scanner s=new Scanner(System.in);
        int N=s.nextInt();
        int V=s.nextInt();
        int v,w;
        int[] dp=new int[V+1];
        dp[0]=0;
        for(int i=1;i<=N;i++){
            v=s.nextInt();
            w=s.nextInt();
            for(int j=v;j<=V;j++){
                dp[j]=Math.max(dp[j],dp[j-v]+w);
            }
        }
        System.out.println(dp[V]);
    }
}

零钱兑换问题(本质上是完全背包问题)

零钱兑换问题1(中等)

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change-2
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:每个硬币无限选择,凑出满足背包的物品。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp=new int[amount+1];
        dp[0]=0;
        for(int i=1;i<=amount;i++){
            dp[i]=amount+1;
            for(int coin: coins){
                if(i-coin>=0){
                    dp[i]= Math.min(dp[i],dp[i-coin]+1);
                }
            }
        }
        return (dp[amount]==amount+1)? -1 : dp[amount]; 
    }
}
零钱兑换问题2(中等)

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change-2
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:与爬楼梯问题的区别在于,该题目是组合,不是排列,如1,2,2与2,1,2算同一种情况。

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp=new int[amount+1];
        dp[0]=1;
        for(int coin : coins){
            for(int i=1;i<amount+1;i++){
                if(coin>i) continue;
                dp[i]+=dp[i-coin];
            }
        }
        return dp[amount];
    }
}
完全平方数(中等,可以视作零钱兑换问题)

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/perfect-squares
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:可以把完全平方数看成硬币,那么题目就变成了,用指定的硬币拼出指定的金额,且数量要最少。

	class Solution {
    public int numSquares(int n) {
        int[] dp=new int[n+1];
        for(int i=0;i<=n;i++){
            dp[i]=i;
            for(int j=1;i-j*j>=0;j++){
                dp[i]=Math.min(dp[i],dp[i-j*j]+1);
            }
        }
        return dp[n];
    }
}

多重背包

有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10

import java.util.*;
import java.io.*;

public class Main{
    public static void main(String[] args){
        Scanner s=new Scanner(System.in);
        int N=s.nextInt();
        int V=s.nextInt();
        int[] dp=new int[V+1];
        for(int i=0;i<N;i++){
            int v=s.nextInt();
            int w=s.nextInt();
            int t=s.nextInt();
            for(int j=V;j>=0;j--){
                for(int k=1;k<=t && k*v<=j;k++){
                    dp[j]=Math.max(dp[j],dp[j-k*v]+w*k);
                }
            }
        }
        System.out.println(dp[V]);
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值