动态规划
- 最优子结构
- 边界
- 状态转移公式
1.最大子序和
题目:
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:
因为是要求的连续子序列,从前往后遍历,最大和一定为当前元素值加上之前的最大和,dp[n]表示为到下标n为止的最大和,则dp[n]=num[n]+Math.max(dp[n-1],0)
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length==1)
return nums[0];
int[] dp=new int[nums.length];
dp[0]=nums[0];
int max=nums[0];
for(int i=1;i<nums.length;i++){
dp[i]=nums[i]+(dp[i-1]>0?dp[i-1]:0);
max=max>dp[i]?max:dp[i];
}
return max;
}
}
2.路径问题
最小路径和
链接
题目:
思路:
class Solution {
public int minPathSum(int[][] grid) {
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(i==0&&j==0)
continue;
else if(i==0){
grid[i][j]=grid[i][j-1]+grid[i][j];
}
else if(j==0){
grid[i][j]=grid[i-1][j]+grid[i][j];
}
else{
grid[i][j]=Math.min(grid[i-1][j],grid[i][j-1])+grid[i][j];
}
}
}
return grid[grid.length-1][grid[0].length-1];
}
}
求共有多少不同路径
题目:
此题和上题解法类似,使用二维数组dp[i][j]表示到达[i][j]网格处的路径数目,那么当i>0&&j>0时,dp[i][j]=dp[i-1][j]+dp[i][j-1]
public int uniquePaths(int m, int n) {
int[][] dp=new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0&&j==0)dp[i][j]=1;
else if(i==0)dp[i][j]=dp[i][j-1];
else if(j==0)dp[i][j]=dp[i-1][j];
else{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
3.最长回文字符串
思路
dp[j][k]表示从j到k行程的子字符串是否为回文字符串,首先易得如果首尾字符串不相等,此字符串必定不是回文
public String longestPalindrome(String s) {
int len=s.length();
if(len<2)
return s;
int maxLen=1;
int begin=0;
boolean [][] dp=new boolean[len][len];
//遍历字符串每一个区间
for(int k=0;k<len;k++){
for(int j=0;j<=k;j++){
//如果不相等必定不是回文
if(s.charAt(j)!=s.charAt(k))
dp[j][k]=false;
else{
if(k<j+3)
dp[j][k]=true;
else
dp[j][k]=dp[j+1][k-1];
}
//记录最长的回文长度和起始位置
if(dp[j][k]&&k-j+1>maxLen){
maxLen=k-j+1;
begin=j;
}
}
}
return s.substring(begin,begin+maxLen);
}
回文子串
用dp[i][j]代表从i到j的子串是否为回文子串
public int countSubstrings(String s) {
boolean [][] dp=new boolean[s.length()][s.length()];
int ans=0;
int len=s.length();
for(int j=0;j<len;j++){
for(int i=0;i<=j;i++){
if(s.charAt(i)==s.charAt(j)&&(j-i<2||dp[i+1][j-1])){
dp[i][j]=true;
ans++;
}
}
}
return ans;
}
4.股票问题
public int maxProfit(int[] prices) {
if(prices==null||prices.length==0)
return 0;
int max=0;
int min=prices[0];
for(int i=0;i<prices.length;i++){
max=Math.max(max,prices[i]-min);
min=Math.min(min,prices[i]);
}
return max;
}
买卖股票的最佳时机II
用贪心算法,因为可以做无限次交易,那么我们可以将每次可能的利润都获取
public int fun4(int[] prices){
int res=0;
for (int i = 0; i <prices.length-1 ; i++) {
int temp=prices[i+1]-prices[i];
if(temp>0)res+=temp;
}
return res;
}
public int maxProfit(int[] prices) {
int[][] dp=new int[prices.length][5];
dp[0][1]=-prices[0];
dp[0][3]=-prices[0];
for(int i=1;i<prices.length;i++){
dp[i][0]=dp[i-1][0];
dp[i][1]=Math.max(dp[i-1][0]-prices[i],dp[i-1][1]);
dp[i][2]=Math.max(dp[i-1][1]+prices[i],dp[i-1][2]);
dp[i][3]=Math.max(dp[i-1][2]-prices[i],dp[i-1][3]);
dp[i][4]=Math.max(dp[i-1][3]+prices[i],dp[i-1][4]);
}
return dp[prices.length-1][4];
}
最佳买卖股票时机含冷冻期
public int maxProfit(int[] prices) {
if(prices.length==0)return 0;
int[][] dp=new int[prices.length][3];
dp[0][0]=-prices[0];
//dp[i][0]:手上持有股票的最大收益
//dp[i][1]:手上不持有股票且第i+1天是冷冻期的最大收益
//dp[i][2]:手上不持有股票且第i+1天不是冷冻期的最大收益
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
dp[i][1]=dp[i-1][0]+prices[i];
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]);
}
int max=Math.max(dp[prices.length-1][1],dp[prices.length-1][2]);
return max;
}
JZ67
题目:
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1,m<=n),每段绳子的长度记为k[1],…,k[m]。请问k[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
思路:
这题目用动态规划。首先题目要求至少切一次。当target<=3时可以明确得出最大乘积,当target>3后,dp[n]=max(dp[n-i]*dp[i])。假设n为10,第一刀之后分为了4-6,而6也可能再分成2-4(6的最大是3-3,但过程中还是要比较2-4这种情况的),而上一步4-6中也需要求长度为4的问题的最大值,可见,各个子问题之间是有重叠的,所以可以先计算小问题,存储下每个小问题的结果,逐步往上,求得大问题的最优解
public class Solution {
public int cutRope(int target) {
if(target<2)
return 0;
if(target==2)
return 1;
if(target==3)
return 2;
int[]dp=new int[target+1];
dp[1]=1;
dp[2]=2;
dp[3]=3;
int max=0;
for(int i=4;i<=target;i++){
for(int j=1;j<=i/2;j++){
max=max>(dp[j]*dp[i-j])?max:(dp[j]*dp[i-j]);
}
dp[i]=max;
}
return dp[target];
}
}
最长公共子串
import java.util.*;
public class Solution {
public String LCS (String str1, String str2) {
int m = str1.length(), n = str2.length();
//dp[i][j]代表str1[0~m-1]和str2[0~n-1]的最大公共子串的长度
int[][] dp = new int[m + 1][n + 1];
int max = 0, index = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (str1.charAt(i) == str2.charAt(j)) {
dp[i + 1][j + 1] = dp[i][j] + 1;
if (max < dp[i + 1][j + 1]) {
max = dp[i + 1][j + 1];
index = i + 1;
}
}
}
}
return max == 0 ? "-1" : str1.substring(index-max,index);
}
}
dp[i][j]代表str1[0~m]和str2[0-n]的最大公共子串的长度,这里要求公共子串必须以str1[m]=str2[n]结尾
背包问题********************************************
问题背景
有一个空间有限(设空间为W)的背包和一堆物品,每个物品有两个属性,一个是占据的空间w,一个是价值v。我们要解决的问题就是在有限的空间W内,将物品装入背包,并且使总价值尽可能的大。背包问题按大类分为三大类:
- 01背包
- 多重背包
- 完全背包
最常见的问题问法有求方案数,求最优方案数,求最大价值,求能不能满足某一条件等等。接下来让我们先分别来看看这些问题的原型
01背包
01背包是指物品的个数都为1,只能使用一次。这类背包首先建立一个二维数组,dp[i][j],表示前i件物品在体积不超过j的前提下的最大价值。其中对于每一个物品的体积用一维数组表示,每一个物品的价值也用一维数组表示。对于每一个物品有两个状态,一个是添加到背包,一个是 没有添加到背包,两种情况满足以下条件:
- 第i件物品没有添加到背包,前i件物品体积不超过j的最大价值就是体积不超过j的前i-1物品的最大价值,即dp[i][j]=dp[i-1][j]
- 第i件物品添加进了背包,直接在前i-1件物品在空间不超过j-w[i]的最大价值上加上v[i],即dp[i][j]=dp[i-1][j-w]+v;
以上两种情况的选择取决于谁的价值更大,用公式表示就是
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w]+v)
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//背包总重
int cap =Integer.parseInt(sc.nextLine().trim());
String str1 = "1,2,3";
String str2 = "6,10,12";
String[] weights = str1.trim().split(",");
String[] values = str2.trim().split(",");
int len = weights.length;
int[] weight = new int[len + 1];
int[] value = new int[len + 1];
for (int i = 0; i < len; i++) {
weight[i + 1] = Integer.parseInt(weights[i]);
}
for (int i = 0; i < len; i++) {
value[i + 1] = Integer.parseInt(values[i]);
}
int[][] dp = new int[len + 1][cap + 1];
for (int i = 1; i <= len; i++) {
for (int j = 1; j <= cap; j++) {
if (weight[i] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
}
完全背包
完全背包和01背包不同点在于,完全背包中的物品可以无限次的取
/**
*
* @param W 背包的总体积
* @return
*/
public int func(int W, int N) {
int[] weight = {1, 3, 3, 6};
int[] value = {2, 5, 3, 1};
int N=weight.length;
//dp[i][j]表示前i件物品在体积不超过j的情况下的最大价值
int[][] dp = new int[N + 1][W + 1];
for (int i = 0; i < N; i++) {
int w = weight[i-1], v = value[i-1];
for (int j = 1; j <= W; j++) {
if(j>=w){
//完全背包和01背包的差别在此,请注意
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-w]+v);
}else
dp[i][j]=dp[i-1][j];
}
}https://leetcode-cn.com/problems/coin-change/
}
零钱兑换
题目链接
dp[i]表示组成面值i所需要的最小硬币数
import java.util.*;
class Solution {
public int coinChange(int[] coins, int amount) {
int max=amount+1;
int[] dp=new int[amount+1];
//给数组所有元素都赋值max
Arrays.fill(dp,max);
dp[0]=0;
for(int i=1;i<=amount;i++){
for(int j=0;j<coins.length;j++){
if(i>=coins[j]){
dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
}
}
}
//如果大于account就说明硬币无法凑成account,返回-1
return dp[amount]>amount?-1:dp[amount];
}
}
完全平方数
public int numSquares(int n) {
if(n<=2)return n;
int[] dp=new int[n+1];
for(int i=1;i<=n;i++){
//最坏情况为i个,即全为1
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];
}
打家劫舍三部曲*********************************************
public int rob(int[] nums) {
if(nums.length<=1)return nums.length==0?0:nums[0];
int[] dp=new int[nums.length];
int max=Math.max(nums[0],nums[1]);
for(int i=0;i<nums.length;i++){
if(i==0){dp[i]=nums[i];continue;}
if(i==1){dp[i]=Math.max(nums[0],nums[1]);continue;}
//状态转移方程
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
max=Math.max(max,dp[i]);
}
return max;
}
此题为环装,将其分为三种情况后,和上题用一样的方法即可
public int rob(int[] nums) {
if(nums.length<=1)return nums.length==0?0:nums[0];
/**
1.不偷头,不偷尾巴
2.偷头,不偷尾
3.不偷头,偷尾
*/
int[] nums1=Arrays.copyOfRange(nums,1,nums.length-1);
int[] nums2=Arrays.copyOfRange(nums,0,nums.length-1);
int[] nums3=Arrays.copyOfRange(nums,1,nums.length);
return Math.max(helper(nums1),Math.max(helper(nums2),helper(nums3)));
}
public int helper(int[] nums){
if(nums.length<=1)return nums.length==0?0:nums[0];
int max=Math.max(nums[1],nums[0]);
int[] dp=new int[nums.length];
for(int i=0;i<nums.length;i++){
if(i==0){dp[i]=nums[i];continue;}
if(i==1){dp[i]=Math.max(nums[1],nums[0]);continue;}
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
max=Math.max(max,dp[i]);
}
return max;
}
public int rob(TreeNode root) {
/**
res数组
res[0] 表示抢劫当前节点的最大值
res[1] 表示不抢劫当前节点的最大值
*/
int[] res=dp(root);
return Math.max(res[1],res[0]);
}
public int[] dp(TreeNode root){
if(root==null)return new int[]{0,0};
int[] left=dp(root.left);
int[] right=dp(root.right);
int select=root.val+left[1]+right[1];
int notSelect=Math.max(left[1],left[0])+Math.max(right[1],right[0]);
return new int[]{select,notSelect};
}
public int translateNum(int num) {
if(num==0)return 1;
String s=String.valueOf(num);
int[] dp=new int[s.length()];
dp[0]=1;
for(int i=1;i<s.length();i++){
if(s.substring(i-1,i+1).compareTo("10")>=0&&s.substring(i-1,i+1).compareTo("25")<=0){
if(i==1)
dp[i]=2;
else
dp[i]=dp[i-2]+dp[i-1];
}
else
dp[i]=dp[i-1];
}
return dp[s.length()-1];
}
环形数组的最大和
如果使用到了环,则求出nums[1]-nums[len-2]的最小值
用sum-min即可得到使用环情况下的最大值
public int maxSubarraySumCircular(int[] nums) {
if(nums.length==1)return nums[0];
int sum=0;
for(int i=0;i<nums.length;i++)
sum+=nums[i];
int[] dp1=new int[nums.length];
int[] dp2=new int[nums.length];
dp1[0]=nums[0];
dp2[0]=nums[0];
int min=dp1[0];
int max=dp2[0];
for(int i=1;i<nums.length;i++){
dp2[i]=Math.max(dp2[i-1]+nums[i],nums[i]);
max=Math.max(max,dp2[i]);
}
//如果使用到了环,则求出nums[1]-nums[len-2]的最小值
//用sum-min即可得到使用环情况下的最大值
for(int i=1;i<nums.length-1;i++){
dp1[i]=Math.min(dp1[i-1]+nums[i],nums[i]);
min=Math.min(min,dp1[i]);
}
return Math.max(max,sum-min);
}