动态规划(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]);
}
}