目录
1.连续数组最大和&最长递增子序列
都是定义的单维数组,计算完dp之后,再遍历一遍dp数组,寻找最大值
A.最大子序和
class Solution {
public int maxSubArray(int[] nums) {
//动态规划:(1)状态:和(2)选择:是否将当前位置值加入和中
//dp[i]表示到达位置i时的最大和
int n=nums.length;
int[] dp=new int[n];
dp[0]=nums[0];
for(int i=1;i<n;i++){
dp[i]=Math.max(nums[i],nums[i]+dp[i-1]);
}
//需要遍历整个dp数组,找到最大和
int res=Integer.MIN_VALUE;
for(int i=0;i<n;i++){
res=Math.max(res,dp[i]);
}
return res;
}
}
B.最长递增子序列
a.简单版本
class Solution {
public int lengthOfLIS(int[] nums) {
//动态规划:定义dp数组:dp[i]表示以i索引位结尾时最长递增子序列的长度
int n=nums.length;
int[] dp=new int[n];
//初始化:每个位置子序列最短是1,因为有本身元素的长度
Arrays.fill(dp,1);
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
}
//由于dp数组每个位置的值代表的是到nums当前位置的最长递增子序列的长度,所以要找到最长递增子序列长度,需要遍历dp数组每个位置的元素,找到最大值
int res=1;
for(int i:dp){
res=Math.max(res,i);
}
return res;
}
}
b.复杂版本
需要先进行排序处理,处理之后得到的一维数组再使用最长递增子序列的方法求得最终结果。
class Solution {
public int maxEnvelopes(int[][] envelopes) {
//首先进行排序,然后求最长递增子序列
Arrays.sort(envelopes,new Comparator<int[]>(){
public int compare(int[] a,int[] b){
//先按宽度排序,如果宽度相等,按高度逆序排序
return a[0]==b[0]?b[1]-a[1]:a[0]-b[0];
}
});
//然后提取出所有高度值作为一个数组
int[] h=new int[envelopes.length];
for(int i=0;i<envelopes.length;i++){
h[i]=envelopes[i][1];
}
int ans=0;
ans=getMaxLength(h);
return ans;
}
//单独定义一个函数求最长递增子序列
public int getMaxLength(int[] nums){
int n=nums.length;
int[] dp=new int[n];
//将dp数组每个位置初始化为1,因为本身就可以是长度为1的递增序列
Arrays.fill(dp,1);
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
//做选择,是否将j所处位置元素添加到递增子序列
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
}
int res=0;
for(int val:dp){
res=Math.max(res,val);
}
return res;
}
}
2.两个字符串求最长公共子序列
两个字符串的操作一般使用动态规划+双指针
A 原型
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
//动态规划,
//(1)确定状态:两个字符串的索引位置i和j
//(2)确定函数的定义:dp[i][j]表示text1在位置i和text2在位置j的最长公共子序列的长度
//(3)确定「选择」并择优
int m=text1.length();
int n=text2.length();
int[][] dp=new int[m+1][n+1];
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(text1.charAt(i-1)==text2.charAt(j-1)){
dp[i][j]=1+dp[i-1][j-1];
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
}
B.变种1
class Solution {
public int minDistance(String word1, String word2) {
//动态规划
//(1)定义dp[i][j]:word1到达索引i,word2到达索引j时所需的最小步数
//(2)选择并择优:如果两个字符串对应字符相等就跳过,否则选取两个中最小值
int m=word1.length();
int n=word2.length();
//因为后面循环时需要用到i-1和j-1,所以这里定义时候维度要加1
int[][] dp=new int[m+1][n+1];
//初始化:base case,当一个字符串长度为0时,另一个字符串需要删除字符串中所有字符来和另一个字符串保持一致
for(int i=1;i<=m;i++){
dp[i][0]=i;
}
for(int j=1;j<=n;j++){
dp[0][j]=j;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+1;
}
}
}
return dp[m][n];
}
}
C.变种2
class Solution {
public int minimumDeleteSum(String s1, String s2) {
int m=s1.length();
int n=s2.length();
int[][]dp=new int[m+1][n+1];
//初始化,base case
int sum1=0;
for(int i=1;i<=m;i++){
sum1+=s1.charAt(i-1);
dp[i][0]=sum1;
}
int sum2=0;
for(int j=1;j<=n;j++){
sum2+=s2.charAt(j-1);
dp[0][j]=sum2;
}
//遍历
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(s1.charAt(i-1)==s2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(dp[i-1][j]+s1.charAt(i-1),dp[i][j-1]+s2.charAt(j-1));
}
}
}
return dp[m][n];
}
}
3.最长回文子序列
class Solution {
public int longestPalindromeSubseq(String s) {
int n=s.length();
int[][] dp=new int[n][n];
for(int i=0;i<n;i++){
dp[i][i]=1;
}
for(int i=n-2;i>=0;i--){
for(int j=i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)){
dp[i][j]=dp[i+1][j-1]+2;
}else{
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
}
4.零钱兑换问题
class Solution {
public int coinChange(int[] coins, int amount) {
//动态规划:dp[i]表示凑成金额为i需要最少的硬币个数为dp[i]
//amount可以取到,所以长度应该为amount+1
int[] dp=new int[amount+1];
//初始化:后面要去最小值,所以初始化为一个最大值
Arrays.fill(dp,amount+1);
dp[0]=0;//金额为0时,所需的硬币数为0
for(int coin:coins){
for(int j=coin;j<=amount;j++){
//做选择:是否使用当前硬币
dp[j]=Math.min(dp[j],dp[j-coin]+1);
}
}
return dp[amount]==amount+1?-1:dp[amount];
}
}
5.编辑距离
class Solution {
public int minDistance(String word1, String word2) {
int m=word1.length();
int n=word2.length();
int[][] dp=new int[m+1][n+1];
//base case
for(int i=1;i<=m;i++){
dp[i][0]=i;
}
for(int j=1;j<=n;j++){
dp[0][j]=j;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
//插入:dp[i][j-1];删除:dp[i-1][j];替换:dp[i-1][j-1]
dp[i][j]=min(
dp[i][j-1]+1,//向word1插入,则i不变,j-1
dp[i-1][j]+1,//word1删除,则j不变,i-1
dp[i-1][j-1]+1//word1替换,i和j都减1
);
}
}
}
return dp[m][n];
}
public int min(int a,int b,int c){
int res=Math.min(a,b);
res=Math.min(res,c);
return res;
}
}
6.背包问题
1.0-1背包
class Solution {
public boolean canPartition(int[] nums) {
//可以使用动态规划的0-1背包解决问题,即是否可以将数组中的某些数正好装进sum/2的背包里
//求数组所有元素的和
int sum=0;
for(int num:nums){
sum+=num;
}
//判断是否可以整除2,如果不可以,说明不能分成两个元素和相等的子集,就直接返回false
if(sum%2!=0) return false;
sum=sum/2;
//状态有两个,元素、元素和
boolean[][] dp=new boolean[nums.length+1][sum+1];
//base case,和为0的时候为true
for(int i=0;i<=nums.length;i++){
dp[i][0]=true;
}
for(int i=1;i<=nums.length;i++){
for(int j=1;j<=sum;j++){
if(nums[i-1]>j){
dp[i][j]=dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i-1]];
}
}
}
return dp[nums.length][sum];
}
}
2.完全背包
class Solution {
public int change(int amount, int[] coins) {
//二维dp
// int n=coins.length;
// int[][] dp=new int[n+1][amount+1];
// //base case
// for(int i=0;i<=n;i++){
// dp[i][0]=1;//凑成总金额为0永远有一种办法:就是什么都不放
// }
// for(int i=1;i<=n;i++){
// for(int j=1;j<=amount;j++){
// if(coins[i-1]>j){
// dp[i][j]=dp[i-1][j];
// }else{
// dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];
// }
// }
// }
// return dp[n][amount];
//一维dp
int[] dp=new int[amount+1];
dp[0]=1;
for(int coin:coins){
for(int j=1;j<=amount;j++){
if(coin<=j){
dp[j]=dp[j]+dp[j-coin];
}
}
}
return dp[amount];
}
}