动态规划算法:
动态规划算法和回溯算法很像,需要明确basecase、状态、选择。
动态规划的暴力穷举求解阶段就是回溯算法。只是有的问题可以构造最优子结构,找到重叠子问题,可以用dp table优化,将递归树大幅剪枝。
一方面,动态规划问题的一般形式就是求最值。核心是穷举,但其存在“重叠子问题”,如果暴力穷举效率会极其低下,所以需要使用dp数组来优化穷举过程,避免不必要的计算。
另一方面,动态规划问题一定会具备“最优子结构”(子问题必须相互独立),这样才能通过子问题的最值得到原问题的最值。
最后,只有列出正确的“状态转移方程”,才能正确的穷举。写出正确的状态转移方程,一定要思考basecase、状态、选择、dp数组的定义(表示状态和选择)
思考的流程:
自顶向下的递归 (已经需要basecase、状态、选择。“状态”用函数参数存储,“选择”需要遍历)
-》 自顶向下的递归+备忘录(空间换时间)
-》 自下向上的循环+dp数组(和自顶向下复杂度差不多,状态用dp数组下标存储)
-》 dp数组的缩小(状态压缩)
例题1:斐波那契数列
递归实现斐波那契数列:
时间复杂度:O(2^N)
空间复杂度:O(N)
dp:
时间复杂度:O(N)
空间复杂度:O(N)
class Solution {
public int fib(int n) {
if(n==0||n==1){
return n;
}
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;++i){
dp[i]=(dp[i-1]+dp[i-2])%1000000007;
}
return dp[n];
}
}
dp+状态压缩:
class Solution {
public int fib(int n) {
if(n==0||n==1){
return n;
}
int dp1=0,dp2=1;
for(int i=2;i<=n;++i){
int temp=(dp1+dp2)%1000000007;
dp1=dp2;
dp2=temp;
}
return dp2;
}
}
例题2:青蛙跳台阶问题
class Solution {
public int numWays(int n) {
int dp1=1,dp2=1;
for(int i=2;i<=n;++i){
int temp=(dp1+dp2)%1000000007;
dp1=dp2;
dp2=temp;
}
return dp2;
}
}
例题3:把数字翻译成字符串
青蛙跳台阶问题拓展
class Solution {
public int translateNum(int num) {
String str=String.valueOf(num);
int dp1=1,dp2=1;
for(int i=2;i<=str.length();++i){
//此题加的限制
int temp=dp2;
int tempNum=Integer.valueOf(str.substring(i-2,i));
if(tempNum>=10&&tempNum<=25){
temp=dp1+dp2;
}
//回到青蛙跳楼梯
dp1=dp2;
dp2=temp;
}
return dp2;
}
}
例题4:跳台阶扩展问题
青蛙跳台阶问题的数量拓展,时间空间复杂度都是o(1)。
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner in=new Scanner(System.in);
int n=in.nextInt()-1;
System.out.println(1<<n);
}
}
例题5:最小花费爬楼梯
青蛙跳台阶问题的数量拓展,时间空间复杂度都是o(1)。
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner in=new Scanner(System.in);
int n=in.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i){
arr[i]=in.nextInt();
}
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=0;
for(int i=2;i<=n;++i){
dp[i]=Math.min(dp[i-2]+arr[i-2],dp[i-1]+arr[i-1]);
}
System.out.println(dp[n]);
}
}
一、01背包
无状态压缩版本:
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int V=in.nextInt();
int[] v=new int[n+1]; //volume体积
int[] c=new int[n+1]; //cost价值
for(int i=0;i<n;++i){
v[i+1]=in.nextInt();
c[i+1]=in.nextInt();
}
//逻辑开始
f(n,V,v,c);
justF(n,V,v,c);
}
//求这个背包至多能装多大价值的物品。dp[i][v]表示前i个物品装入容量为v的背包中能获得最大价值
static void f(int n,int V,int[] v,int[] c){
int[][] dp=new int[n+1][V+1];
for(int i=1;i<=n;++i){
for(int j=1;j<=V;++j){
if(j>=v[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-v[i]]+c[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
System.out.println(dp[n][V]);
}
//若背包恰好装满,求至多能装多大价值的物品。dp[i][v]表示前i个物品 恰好 装入容量为v的背包中能获得最大价值。就是初始化的不一样。
static void justF(int n,int V,int[] v,int[] c){
int[][] dp=new int[n+1][V+1];
for(int i=0;i<=n;++i){
for(int j=0;j<=V;++j){
if(j==0){
dp[i][j]=0;
}else{
dp[i][j]=Integer.MIN_VALUE;
}
}
}
for(int i=1;i<=n;++i){
for(int j=1;j<=V;++j){
if(j>=v[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-v[i]]+c[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
int result=dp[n][V]>0?dp[n][V]:0;
System.out.println(result);
}
}
状态压缩版本:
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int V=in.nextInt();
int[] v=new int[n+1]; //volume体积
int[] c=new int[n+1]; //cost价值
for(int i=0;i<n;++i){
v[i+1]=in.nextInt();
c[i+1]=in.nextInt();
}
//逻辑开始
f(n,V,v,c);
justF(n,V,v,c);
}
//求这个背包至多能装多大价值的物品。dp[i][v]表示前i个物品装入容量为v的背包中能获得最大价值
static void f(int n,int V,int[] v,int[] c){
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]]+c[i]);
}
}
System.out.println(dp[V]);
}
//若背包恰好装满,求至多能装多大价值的物品。dp[i][v]表示前i个物品 恰好 装入容量为v的背包中能获得最大价值。就是初始化的不一样。
static void justF(int n,int V,int[] v,int[] c){
int[] dp=new int[V+1];
Arrays.fill(dp,Integer.MIN_VALUE);
dp[0]=0;
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]]+c[i]);
}
}
int result=dp[V]<0?0:dp[V];
System.out.println(result);
}
}
例题1: 分割等和子集
例题2: 一和零
例题3: 目标和
解法1:回溯算法
时间复杂度太高
class Solution {
int count=0;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums,0,target,0);
return count;
}
void dfs(int[] nums,int index,int target,int sum){
if(index>=nums.length){
if(sum==target){
++count;
}
return;
}
dfs(nums,index+1,target,sum+nums[index]);
dfs(nums,index+1,target,sum-nums[index]);
}
}
解法2:动态规划算法
1 我们要消除target可能为负数的影响,所以要进行转换,将原题变为01背包问题(恰好装满的版本),而不是现在的这个-11背包问题
这里实际上找的是nums[]选哪些数字可以让其和为(sum-target)/2,推导如下:
设被减去的数字和为neg,所有数字和为sum。有sum-2neg=target,则neg=(sum-target)/2
2 由于 nums[i]可以等于0 ,而且+0,-0算两种方法,所以不能让dp[i][0]都为1,而只能让dp[0][0]为1
像是青蛙跳台阶问题的二阶拓展,或者说背包问题像青蛙跳台阶问题的二阶扩展。
class Solution {
public int findTargetSumWays(int[] nums, int target) {
//真正的目标数字realTarget,这样才是01背包问题
int sum=0;
for(int i=0;i<nums.length;++i){
sum+=nums[i];
}
if(sum-target<0||(sum-target)%2!=0){
return 0;
}
int realTarget=(sum-target)/2;
//开始动态规划:dp[i][j]表示前i个数,和为j的不同选择数目
int[][] dp=new int[nums.length+1][realTarget+1];
dp[0][0]=1;
for(int i=1;i<dp.length;++i){
for(int j=0;j<dp[0].length;++j){
if(j>=nums[i-1]){
dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]];
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[dp.length-1][dp[0].length-1];
}
}
例题4: 盈利计划
例题5: 最后一块石头的重量 II
二、完全背包
大佬题解中的程序猿大队长:0-1背包是完全背包的特殊情况
无状态压缩版本:
import java.util.*;
public class Main{
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int V=in.nextInt();
int[] v=new int[n+1]; //volume体积
int[] c=new int[n+1]; //cost价值
for(int i=0;i<n;++i){
v[i+1]=in.nextInt();
c[i+1]=in.nextInt();
}
//逻辑开始
f(n,V,v,c);
justF(n,V,v,c);
}
//求这个背包至多能装多大价值的物品。dp[i][v]表示前i个物品装入容量为v的背包中能获得最大价值(每个物品可以选无数次)
static void f(int n,int V,int[] v,int[] c){
int[][] dp=new int[n+1][V+1];
for(int i=1;i<=n;++i){
for(int j=1;j<=V;++j){
int max=dp[i-1][j];
for(int k=0;k*v[i]<=j;++k){
max=Math.max(max,dp[i-1][j-k*v[i]]+k*c[i]);
}
dp[i][j]=max;
}
}
System.out.println(dp[n][V]);
}
//若背包恰好装满,求至多能装多大价值的物品。dp[i][v]表示前i个物品 恰好 装入容量为v的背包中能获得最大价值。就是初始化的不一样(每个物品可以选无数次)
static void justF(int n,int V,int[] v,int[] c){
int[][] dp=new int[n+1][V+1];
for(int i=0;i<=n;++i){
for(int j=0;j<=V;++j){
if(j==0){
dp[i][j]=0;
}else{
dp[i][j]=Integer.MIN_VALUE;
}
}
}
for(int i=1;i<=n;++i){
for(int j=1;j<=V;++j){
int max=dp[i-1][j];
for(int k=0;k*v[i]<=j;++k){
max=Math.max(max,dp[i-1][j-k*v[i]]+k*c[i]);
}
dp[i][j]=max;
}
}
int result=dp[n][V]>0?dp[n][V]:0;
System.out.println(result);
}
}
状态压缩版本:
import java.util.*;
public class Main{
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int V=in.nextInt();
int[] v=new int[n+1]; //volume体积
int[] c=new int[n+1]; //cost价值
for(int i=0;i<n;++i){
v[i+1]=in.nextInt();
c[i+1]=in.nextInt();
}
//逻辑开始
f(n,V,v,c);
justF(n,V,v,c);
}
//求这个背包至多能装多大价值的物品。dp[i][v]表示前i个物品装入容量为v的背包中能获得最大价值(每个物品可以选无数次)
static void f(int n,int V,int[] v,int[] c){
int[] dp=new int[V+1];
for(int i=1;i<=n;++i){
for(int j=v[i];j<=V;++j){
//状态转移,要么选择当前物品,要么不选(选较大的情况)
dp[j]=Math.max(dp[j],dp[j-v[i]]+c[i]);
}
}
System.out.println(dp[V]);
}
//若背包恰好装满,求至多能装多大价值的物品。dp[i][v]表示前i个物品 恰好 装入容量为v的背包中能获得最大价值。就是初始化的不一样(每个物品可以选无数次)
static void justF(int n,int V,int[] v,int[] c){
int[] dp=new int[V+1];
Arrays.fill(dp,Integer.MIN_VALUE);
dp[0]=0;
for(int i=1;i<=n;++i){
for(int j=v[i];j<=V;++j){
dp[j]= Math.max(dp[j],dp[j-v[i]]+c[i]);
}
}
int result=dp[V]<0?0:dp[V];
System.out.println(result);
}
}
例题1: 数位成本和为目标值的最大数字
例题2:零钱兑换
贪心是不行的,比如coins=[1,51,100],target=102用贪心就是错的。
假设我们知道 F(S),即组成金额 S 最少的硬币数,最后一枚硬币的面值是 C。那么由于问题的最优子结构,转移方程应为:F(S)=F(S−C)+1
状态是目标金额(dp的下标),所需结果是目标金额的最少硬币数(dp的内容),选择是每种硬币的面值
class Solution {
public int coinChange(int[] coins, int amount) {
//dp下标表示状态:目标金额;dp内容需要的最少硬币个数
int[] dp=new int[amount+1];
//初始化base case(基本情况)
dp[0]=0;
for(int i=1;i<=amount;++i){
dp[i]=(amount+1);
}
//i用来遍历状态
for(int i=1;i<=amount;++i){
//coin用来遍历选择,即选择最后一枚硬币的面值
for(int coin:coins){
int temp=i-coin;
if(temp<0){
continue;
}
dp[i]=Math.min(dp[temp]+1,dp[i]);
}
}
return dp[amount]==(amount+1)?-1:dp[amount];
}
}
例题3:零钱兑换 II
例题4:完全平方数
三、分组背包
例题1:掷骰子的N种方法
四、石子游戏
https://blog.csdn.net/hang_ning/article/details/121120671
例题1:石子游戏
dp[i][j]:表示先手玩家与后手玩家在区间 [i, j][i,j] 之间互相拿,先手玩家比后手玩家多的最大石子个数。这是个差值,而且是个最大差值
class Solution {
public boolean stoneGame(int[] piles) {
int len=piles.length;
//dp[i][j]:表示先手玩家(亚历克斯)与后手玩家(李)在区间 [i, j][i,j] 之间互相拿,先手玩家比后手玩家多的最大石子个数。这是个差值,而且是个最大差值
int[][] dp=new int[len][len];
for(int i=0;i<len;++i){
dp[i][i]=piles[i];
}
for(int i=len-2;i>=0;--i){
for(int j=i+1;j<len;++j){
dp[i][j]=Math.max(piles[i]-dp[i+1][j],piles[j]-dp[i][j-1]);
}
}
return dp[0][len-1]>0;
}
}
例题2:石子游戏 II
dp[i][j]表示剩余[i : len - 1]堆时,M = j的情况下,先取的人能获得的最多石子数
class Solution {
public int stoneGameII(int[] piles) {
int len=piles.length;
//dp[i][j]表示piles[i:len-1]堆,M为j时的先手最大值
int[][] dp=new int[len][len+1];
int sum=0;
for(int i=len-1;i>=0;--i){
sum+=piles[i];
for(int M=1;M<=len;++M){
if(i+2*M>=len){ //是>= len,而不是>=len-1。因为如i=0,M=1,len=3时是不能一次性取完的
dp[i][M]=sum;
}else{
for(int X=1;X<=2*M;++X){
dp[i][M]=Math.max(dp[i][M],sum-dp[i+X][Math.max(M,X)]);
}
}
}
}
return dp[0][1];
}
}
例题3:石子游戏 III
此题相对于例题2的难点在于有了负数,简单的点在于没有了M。
class Solution {
public String stoneGameIII(int[] stoneValue) {
int len=stoneValue.length;
//dp[i]表示stoneValue[i:len-1]下先手者最多能获得石子数
int[] dp=new int[len+1];
dp[len]=0; //没有石子的时候结果为0
int sum=0;
for(int i=len-1;i>=0;--i){
sum+=stoneValue[i];
dp[i]=Integer.MIN_VALUE;
for(int x=1;x<=3&&i+x<=len;++x){
dp[i]=Math.max(dp[i],sum-dp[i+x]);
}
}
if(dp[0]>sum-dp[0]){
return "Alice";
}else if(dp[0]<sum-dp[0]){
return "Bob";
}else{
return "Tie";
}
}
}
例题4:石子游戏 IV
class Solution {
public boolean winnerSquareGame(int n) {
boolean[] dp=new boolean[n+1];
for(int i=1;i<=n;++i){
for(int k=1;i-k*k>=0;++k){
if(dp[i-k*k]==false){
dp[i]=true;
break;
}
}
}
return dp[n];
}
}
例题5:石子游戏 V
O(N^3)的动态规划
class Solution {
public int stoneGameV(int[] stoneValue) {
int len=stoneValue.length;
int[][] dp=new int[len][len];
int[] sum=new int[len];
sum[0]=stoneValue[0];
for(int i=1;i<len;++i){
sum[i]=sum[i-1]+stoneValue[i];
}
for(int i=len-1;i>=0;--i){
for(int j=i;j<len;++j){
if(i==j){
dp[i][j]=0;
}else{
//k为分隔点
for(int k=i;k<j;++k){
int sum1;
if(i-1<0){
sum1=sum[k];
}else{
sum1=sum[k]-sum[i-1];
}
int sum2=sum[j]-sum[k];
if(sum1<sum2){
dp[i][j]=Math.max(dp[i][j],sum1+dp[i][k]);
}else if(sum1>sum2){
dp[i][j]=Math.max(dp[i][j],sum2+dp[k+1][j]);
}else{
dp[i][j]=Math.max(dp[i][j],sum1+dp[i][k]);
dp[i][j]=Math.max(dp[i][j],sum2+dp[k+1][j]);
}
}
}
}
}
return dp[0][len-1];
}
}
O(N^2)的动态规划
class Solution {
public int stoneGameV(int[] stoneValue) {
int len=stoneValue.length;
int[][] dp=new int[len][len];
int[][] maxl = new int[len][len];
int[][] maxr = new int[len][len];
for (int left = len - 1; left >= 0; --left) {
maxl[left][left] = maxr[left][left] = stoneValue[left];
int sum = stoneValue[left], suml = 0;
for (int right = left + 1, i = left - 1; right < len; ++right) {
sum += stoneValue[right];
while (i + 1 < right && (suml + stoneValue[i + 1]) * 2 <= sum) {
suml += stoneValue[i + 1];
++i;
}
if (left <= i) {
dp[left][right] = Math.max(dp[left][right], maxl[left][i]);
}
if (i + 1 < right) {
dp[left][right] = Math.max(dp[left][right], maxr[i + 2][right]);
}
if (suml * 2 == sum) {
dp[left][right] = Math.max(dp[left][right], maxr[i + 1][right]);
}
maxl[left][right] = Math.max(maxl[left][right - 1], sum + dp[left][right]);
maxr[left][right] = Math.max(maxr[left + 1][right], sum + dp[left][right]);
}
}
return dp[0][len-1];
}
}
例题6:石子游戏 VI
此题不是动态规划
class Solution {
public int stoneGameVI(int[] aliceValues, int[] bobValues) {
int len=aliceValues.length;
Node[] values=new Node[len];
for(int i=0;i<len;++i){
values[i]=new Node();
values[i].index=i;
values[i].val=aliceValues[i]+bobValues[i];
}
Arrays.sort(values,(e1, e2) -> e2.val - e1.val);
int numA=0,numB=0;
for(int i=0;i<len;++i){
if(i%2==0){
numA+=aliceValues[values[i].index];
}else{
numB+=bobValues[values[i].index];
}
}
if(numA>numB){
return 1;
}else if(numA<numB){
return -1;
}else{
return 0;
}
}
}
class Node{
int index;
int val;
}
例题7:石子游戏 VII
sum[i][j]:表示从 i 到 j 的石头价值总和
dp[i][j]:表示剩下的石头堆为 i 到 j 时,当前玩家与另一个玩家得分差的最大值,当前玩家不一定是先手Alice
class Solution {
public int stoneGameVII(int[] stones) {
int n=stones.length;
//sum[i][j]表示stones[i:j]的和
int[][] sum=new int[n][n];
for(int i=0;i<n;++i){
for(int j=i;j<n;++j){
if(j==i){
sum[i][j]=stones[i];
}else{
sum[i][j]=sum[i][j-1]+stones[j];
}
}
}
//dp[i][j]表示对stones[i:j]先手玩家能得到的最大差值
int[][] dp=new int[n][n];
for(int i=n-1;i>=0;--i){
for(int j=i+1;j<n;++j){
dp[i][j]=Math.max(sum[i+1][j]-dp[i+1][j],sum[i][j-1]-dp[i][j-1]);
}
}
return dp[0][n-1];
}
}
例题8:石子游戏 VIII
设 f(i)表示先手可以选择的下标 u在[i,n) 范围内时,先手和后手分数的最大差值
class Solution {
public int stoneGameVIII(int[] stones) {
int len=stones.length;
int[] sum=new int[len];
sum[0]=stones[0];
for(int i=1;i<len;++i){
sum[i]=sum[i-1]+stones[i];
}
int[] dp=new int[len];
dp[len-1]=sum[len-1];
for(int i=len-2;i>=1;--i){
//dp[i+1]为先手选到了[i:i+]的情况,sum[i]-dp[i+1]为先手选了[i:i]的情况。
dp[i]=Math.max(dp[i+1],sum[i]-dp[i+1]);
}
return dp[1];
}
}
例题9:石子游戏 IX
class Solution {
public boolean stoneGameIX(int[] stones) {
int len=stones.length;
int num0=0,num1=0,num2=0;
for(int i=0;i<len;++i){
if(stones[i]%3==0){
++num0;
}else if(stones[i]%3==1){
++num1;
}else{
++num2;
}
}
if(num0%2==0){
return num1>0&&num2>0;
}else{
return num1-num2>2||num2-num1>2;
}
}
}
扩展:两堆石子的游戏(威佐夫博弈)
威佐夫博弈(Wythoff’s game)是指的这样一个问题:有两堆各若干个物品,两个人轮流从任意一堆中取出至少一个或者同时从两堆中取出同样多的物品,规定每次至少取一个,至多不限,最后取光者胜利
https://blog.csdn.net/weixin_44413364/article/details/114820135
在这里插入代码片
五、其他
例题1:最长回文子串
很明显求解出所有的字符串自然能够找到最大的,与例题2做好比较。大佬题解
解法1:动态规划法
时间复杂度:O(n^2)
空间复杂度:O(n^2)
class Solution {
public String longestPalindrome(String s) {
boolean[][] dp=new boolean[s.length()][s.length()];
int left=-1,right=-2;
for(int j=0;j<dp.length;++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;
if(right-left<j-i){
right=j;
left=i;
}
}
}
}
return s.substring(left,right+1);
}
}
解法2:中心扩散双指针
时间复杂度:O(n^2)
空间复杂度:O(1)
class Solution {
public String longestPalindrome(String s) {
int needLeft=-1,needRight=-2;
for (int center = 0; center< 2*s.length()-1; center++) {
int left=center/2;
int right=left+center%2;
while(left>=0&&right<s.length()&&s.charAt(left)==s.charAt(right)){
if(right-left>needRight-needLeft){
needRight=right;
needLeft=left;
}
--left;
++right;
}
}
return s.substring(needLeft,needRight+1);
}
}
例题2:回文子串
解法1:动态规划法
状态:dp[i][j] 表示字符串s在[i,j]区间的子串是否是一个回文串。
时间复杂度:O(n^2)
空间复杂度:O(n^2)
class Solution {
public int countSubstrings(String s) {
// 动态规划
boolean[][] dp = new boolean[s.length()][s.length()];
int result = 0;
for (int j = 0; j < s.length(); 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;
result++;
}
}
}
return result;
}
}
解法2:中心扩散双指针
时间复杂度:O(n^2)
空间复杂度:O(1)
class Solution {
public int countSubstrings(String s) {
int result = 0;
for (int center = 0; center< 2*s.length()-1; center++) {
int left=center/2;
int right=left+center%2;
while(left>=0&&right<s.length()&&s.charAt(left)==s.charAt(right)){
--left;
++right;
++result;
}
}
return result;
}
}
例题3:最少回文分割
dp[i]的含义为字符串长度从0到i,最少可以分为几个回文串
class Solution {
public int minCut(String s) {
int n = s.length();
int[] dp = new int[n+1];
dp[0]=0;
for(int i=1;i<=n;i++){
dp[i]=i;
for(int j=0;j<i;j++){
if(valid(s,j,i-1)){
dp[i]=Math.min(dp[i],dp[j]+1);
}
}
}
return dp[n]-1;
}
public boolean valid(String s,int l,int r){
while(l<r){
if(s.charAt(l)!=s.charAt(r)){
return false;
}
l++;
r--;
}
return true;
}
}
例题4:连续子数组的最大和
状态:dp[i]表示以A[i]作为末尾的连续序列的最大和。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
while(in.hasNextInt()){
int n=in.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i){
arr[i]=in.nextInt();
}
//逻辑开始
int[] dp=new int[n];
dp[0]=nums[0];
int result=nums[0];
for(int i=1;i<n;++i){
dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);
result=Math.max(result,dp[i]);
}
System.out.println(result);
}
}
}
例题5:乘积最大子数组
状态:dp[i]表示以A[i]作为末尾的连续序列的最大乘积。
class Solution {
public int maxProduct(int[] nums) {
int maxNum=nums[0],minNum=nums[0],ans=nums[0];
int length=nums.length;
for(int i=1;i<length;++i){
int maxn=maxNum,minn=minNum;
maxNum=Math.max(nums[i],Math.max(nums[i]*maxn,nums[i]*minn));
minNum=Math.min(nums[i],Math.min(nums[i]*maxn,nums[i]*minn));
ans=Math.max(ans,maxNum);
}
return ans;
}
}
例题5:最长上升子序列
给定一个长度为 n 的数组 arr,求它的最长严格上升子序列的长度。
解法1:动态规划
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int n=in.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i){
arr[i]=in.nextInt();
}
int[] dp=new int[n];
int result=-1;
for(int i=0;i<n;++i){
dp[i]=1;
for(int j=0;j<i;++j){
if(arr[j]<arr[i]&&dp[j]+1>dp[i]){
dp[i]=dp[j]+1;
}
}
result=Math.max(result,dp[i]);
}
System.out.println(result);
}
}
解法2:动态规划+二分搜索算法
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int n=in.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i){
arr[i]=in.nextInt();
}
//逻辑开始
int[] value = new int[n + 1];
// 初始化第一个数
int maxLength = 1;
value[1] = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > value[maxLength]) {
// 大于目前最大长度的子序列的最后一位,给value[]后边续上
maxLength++;
value[maxLength] = arr[i];
} else {
// 小于目前最大长度的子序列的最后一位,查找前边部分第一个大于自身的位置
// 更新它
int t = find(value, maxLength, arr[i]);
value[t] = arr[i];
}
}
System.out.println(maxLength);
}
// 二分查找
private static int find(int[] value, int maxindex, int i) {
int l = 1, r = maxindex;
while (l <= r) {
int mid = (l + r) / 2;
if (i > value[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return l;
}
}
例题6:最长上升子序列扩展
小强现在有个物品,每个物品有两种属性和。他想要从中挑出尽可能多的物品满足以下条件:对于任意两个物品和,满足或者。问最多能挑出多少物品。
输入例子1:
2
3
1 3 2
0 2 3
4
1 5 4 2
10 32 19 21
输出例子1:
2
3
import java.util.*;
class Node implements Comparable{ //不能直接在Arrays.sort()里面用lamda函数,会超时。必须要类实现Comparable接口,重写compareTo()
int x;
int y;
public Node(int x,int y) {
this.x = x;
this.y = y;
}
@Override
public int compareTo(Object o) {
Node node = (Node) o;
return this.x==node.x?node.y-this.y:this.x-node.x;
}
}
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
for (int i = 0; i < num; i++) {
int goodsNum = sc.nextInt();
int[] linearr1 = new int[goodsNum];
int[] linearr2 = new int[goodsNum];
for (int j = 0; j < linearr1.length; j++) {
linearr1[j] = sc.nextInt();
}
for (int j = 0; j < linearr2.length; j++) {
linearr2[j] = sc.nextInt();
}
Node[] list = new Node[goodsNum];
for (int j = 0; j < list.length; j++) {
list[j] = new Node(linearr1[j], linearr2[j]);
}
Arrays.sort(list);
//动态规划+二分法
binarySearch(list);
}
}
private static void binarySearch(Node[] arr) {
int[] value = new int[arr.length +1];
int maxLength = 1;
value[1] = arr[0].y;
for (int i = 1; i < arr.length; i++) {
if (arr[i].y > value[maxLength]) {
value[++maxLength] = arr[i].y;
} else {
int t = find(value, maxLength, arr[i].y);
value[t] = arr[i].y;
}
}
System.out.println(maxLength);
}
// 二分查找
private static int find(int[] value, int maxindex, int i) {
int l = 1, r = maxindex;
while (l <= r) {
int mid = (l + r) / 2;
if (i > value[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return l;
}
}
例题7:最长公共子序列
其中dp[i][j] 表示 text1[0:i] 和text2[0:j] 的最长公共子序列的长度
解法1:动态规划
空间复杂度 O(mn),时间复杂度 O(mn)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int m=in.nextInt();
String a=in.next();
String b=in.next();
//逻辑开始
int[][] dp=new int[n+1][m+1];
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
if(a.charAt(i-1)==b.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
System.out.println(dp[n][m]);
}
}
解法2:优化空间复杂度
空间复杂度 O(min(m,n)),时间复杂度 O(mn)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int m=in.nextInt();
String a=in.next();
String b=in.next();
//逻辑开始
if(n<m){
String temp=a;
a=b;
b=temp;
int tempNum=n;
n=m;
m=tempNum;
}
int[] dp=new int[m+1];
for (int i = 1; i <=n; ++i) {
int temp1 = 0;
int temp2 = 0;
for (int j = 1; j <=m; ++j) {
temp2 = dp[j];
if (a.charAt(i-1) == b.charAt(j-1)) {
dp[j] = temp1 + 1;
}else {
dp[j] = Math.max(dp[j-1], dp[j]);
}
temp1 = temp2;
}
}
System.out.println(dp[m]);
}
}
例题8:DAG最长路
DAG是有向无环图。
(1)求整个DAG中的最长路径
dp[i]表示从i号顶点出发能获得的最长路径长度。图用邻接矩阵存储。
class Solution {
int DP(int i){
if(dp[i]>0)return dp[i];
for(int j=0;j<n;++j){ //遍历i的所有出边
if(G[i][j]!=Integer.MAX_VALUE){
int temp=DP(j)+G[i][j]; //单独计算,防止if中调用DP函数两次
if(temp>dp[i]){ //可以获得更长的路径
dp[i]=temp; //覆盖dp[i]
choice[i]=j; //i号顶点的后继顶点是j
}
}
}
return dp[i];
}
//调用printPath前需要先得到最大的dp[i],然后将i作为路径起点传入
void printPath(int i){
System.out.print(i);
while(choice[i]!=-1){ //choice数组初始化为-1
i=choice[i];
System.out.println("->"+i);
}
}
}
(2)固定终点,求DAG的最长路径
dp[i]表示从i号顶点出发到达重点T能获得的最长路径长度。图用邻接矩阵存储。
class Solution {
int DP(int i){
if(vis[i])return dp[i];
vis[i]=true;
for(int j=0;j<n;++j){ //遍历i的所有出边
if(G[i][j]!=Integer.MAX_VALUE){
dp[i]= Math.max(dp[i],DP(j)+G[i][j]);
}
}
return dp[i];
}
}
例题9:数塔问题
import java.util.*;
public class T5 {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int[][] num=new int[n+1][n+1];
int[][] dp=new int[n+1][n+1];
for(int i=1;i<=n;++i){
for(int j=1;j<=i;++j){
num[i][j]=in.nextInt();
}
}
//逻辑开始
for(int j=1;j<=n;++j){
dp[n][j]=num[n][j];
}
for(int i=n-1;i>=1;--i){
for(int j=1;j<=i;++j){
dp[i][j]=Math.max(dp[i+1][j],dp[i+1][j+1])+num[i][j];
}
}
System.out.println(dp[1][1]);
}
}
例题10:有多少个不同的二叉搜索树
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner in=new Scanner(System.in);
int n=in.nextInt();
//core code
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;++i){
for(int j=1;j<=i;++j){
dp[i]+=dp[j-1]*dp[i-j];
}
}
System.out.println(dp[n]);
}
}
例题10:二叉树方案数
小强现在有个节点,他想请你帮他计算出有多少种不同的二叉树满足节点个数为且树的高度不超过的方案.因为答案很大,所以答案需要模上1e9+7后输出.
树的高度: 定义为所有叶子到根路径上节点个数的最大值。
import java.util.*;
public class T9 {
public static void main(String[] args) {
Scanner in =new Scanner(System.in);
int n=in.nextInt();
int m=in.nextInt();
//逻辑开始
//[i][j]表示i个节点最大深度为j的树数量
int mod= (int)Math.pow(10,9)+7;
long[][] dp=new long[n+1][m+1]; //必须是long,不然只能通过部分用例
Arrays.fill(dp[0], 1);
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
for(int k=0;k<i;++k){
// 左子树节点数为k,右子树节点数为i-k-1,且左右子树都要求小于等于j-1
dp[i][j]=(dp[i][j]+dp[k][j-1]*dp[i-k-1][j-1]%mod)%mod;
}
}
}
System.out.println(dp[n][m]);
}
}
例题13:无重复字符的最长子串
解法1:滑动窗口算法
window存储子串中字符以及对应数量
时间复杂度:O(n)
空间复杂度:O(1)。字符的 ASCII 码范围为 0 ~ 127 ,哈希表 window最多使用 O(128) = O(1)大小的额外空间。
class Solution {
public int lengthOfLongestSubstring(String s) {
//window统计此窗口中出现的字符与出现的次数
Map<Character,Integer> window=new HashMap<>();
int left=0,right=0;
int len=0;
while(right<s.length()){
char r=s.charAt(right);
++right;
window.put(r,window.getOrDefault(r,0)+1);
while(window.get(r)>1){
char l=s.charAt(left);
++left;
window.put(l,window.get(l)-1);
}
if(right-left>len){
len=right-left;
}
}
return len;
}
}
解法2:动态规划算法
map统计各字符最后一次出现的索引位置。也可以不使用map,而是线性搜索。这样时间复杂度就是O(n^2)
时间复杂度:O(n)
空间复杂度:O(n)。
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length()==0){
return 0;
}
//dp[i]表示以s.charAt(i)结尾的最长不重复字符子串长度
int[] dp=new int[s.length()];
dp[0]=1;
//map统计各字符最后一次出现的索引位置
Map<Character,Integer> map=new HashMap<>();
map.put(s.charAt(0),0);
//构建过程中得到最大值result
int result=1;
//开始构建dp数组
for(int right=1;right<s.length();++right){
int left = map.getOrDefault(s.charAt(right), -1);
map.put(s.charAt(right), right); // 更新哈希表
//核心逻辑:就是选择right-left或者dp[right-1]+1中较小的一个
if(dp[right-1]<right-left){ //当有重复字符的子串长度-1大于以s.charAt(right-1)为结尾的最长不重复子串长度时,为dp[right-1]+1
dp[right]=dp[right-1]+1;
}else{ //否则,为有重复字符的子串长度-1
dp[right]=right-left;
}
//记录最大值
result = Math.max(result, dp[right]);
}
return result;
}
}
解法3:动态规划算法+状态压缩
map统计各字符最后一次出现的索引位置。也可以不使用map,而是线性搜索。这样时间复杂度就是O(n^2)
时间复杂度:O(n)
空间复杂度:O(1)。字符的 ASCII 码范围为 0 ~ 127 ,哈希表map最多使用 O(128) = O(1)大小的额外空间。
class Solution {
public int lengthOfLongestSubstring(String s) {
//dp[i]表示以s.charAt(i)结尾的最长不重复字符子串长度。其状态压缩后为temp变量
int temp=0;
//map统计各字符最后一次出现的索引位置
Map<Character,Integer> map=new HashMap<>();
//构建过程中得到最大值result
int result=0;
//开始构建dp数组
for(int right=0;right<s.length();++right){
int left = map.getOrDefault(s.charAt(right), -1);
map.put(s.charAt(right), right); // 更新哈希表
if(right-left>temp){
temp=temp+1;
}else{
temp=right-left;
}
//记录最大值
result = Math.max(result, temp);
}
return result;
}
}
例题14:正则表达式匹配
解法1:动态规划版本(从下到上)
dp[n][m]表示字符串A前n个字符和正则表达式B前m个字符是否能够匹配。
1 空正则表达式:字符串为空时为true,不为空为false。
2 非空正则表达式:需要实际情况进行计算
2.1 正则表达式B的第j位为非*:如果 A第i位与B第j位相同 或 B第j位为. ,则dp[i][j]=dp[i-1][j-1],否则dp[i][j]不变,还是false。
2.2 正则表达式B的第j位为*:
(1)不看*和其前面一位:dp[i][j]|=dp[i][j-2]
(2)看前面一位:如果 A第i位与B第j-1位相同 或B第j-1位为. ,则dp[i][j]|=dp[i-1][j],否则dp[i][j]不变,还是false
大佬题解
时间复杂度:O(m*n)
空间复杂度:O(m*n)
class Solution {
public boolean isMatch(String s, String p) {
int n=s.length();
int m=p.length();
boolean[][] dp=new boolean[n+1][m+1];
for(int i=0;i<=n;++i){
for(int j=0;j<=m;++j){
if(j==0){ //边界条件
dp[i][j]=(i==0);
}else{
if(p.charAt(j-1)!='*'){ //j-1位置不等于*时
if(i>=1&&(p.charAt(j-1)==s.charAt(i-1)||p.charAt(j-1)=='.')){
dp[i][j]=dp[i-1][j-1];
}
}else{ //j-1位置等于*时
if(j>=2){
dp[i][j]=dp[i][j-2];
}
if(i>=1&&j>=2&&(p.charAt(j-2)==s.charAt(i-1)||p.charAt(j-2)=='.')){
dp[i][j]|=dp[i-1][j];
}
}
}
}
}
return dp[n][m];
}
}
解法2:递归版本(从上到下)
时间复杂度高
class Solution {
public boolean isMatch(String s, String p) {
if(s.length()==0){
if(p.length()%2==0){
for(int i=1;i<p.length();i+=2){
if(p.charAt(i)!='*'){
return false;
}
}
return true;
}
return false;
}
if(p.length()==0){
return false;
}
char a=s.charAt(0);
char b=p.charAt(0);
char c='a';
if(p.length()>1){
c=p.charAt(1);
}
if(c!='*'){
if(a==b||b=='.'){
return isMatch(s.substring(1),p.substring(1));
}
return false;
}else{
if(a==b||b=='.'){
return isMatch(s.substring(1),p)||isMatch(s,p.substring(2));
}
return isMatch(s,p.substring(2));
}
}
}
例题12:礼物的最大价值
可以将原矩阵 grid 用作 dp 矩阵。所以必须 grid[i][j]+=Math.max(grid[i-1][j],grid[i][j-1]);
因为其实他的本质是dp[i][j]=grid[i][j]+Math.max(dp[i-1][j],dp[i][j-1]);
class Solution {
public int maxValue(int[][] grid) {
//grid即为dp,dp[i][j]表示此位置获得最大礼物价值
for(int i=1;i<grid.length;++i){
grid[i][0]+=grid[i-1][0];
}
for(int i=1;i<grid[0].length;++i){
grid[0][i]+=grid[0][i-1];
}
for(int i=1;i<grid.length;++i){
for(int j=1;j<grid[0].length;++j){
grid[i][j]+=Math.max(grid[i-1][j],grid[i][j-1]);
}
}
return grid[grid.length-1][grid[0].length-1];
}
}
例题15:最小路径和
class Solution {
public int minPathSum(int[][] grid) {
int n=grid.length,m=grid[0].length;
int[][] dp=new int[n][m];
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(i==0&&j==0){
dp[0][0]=grid[0][0];
}else if(i==0){
dp[i][j]=dp[i][j-1]+grid[i][j];
}else if(j==0){
dp[i][j]=dp[i-1][j]+grid[i][j];
}else{
dp[i][j]=Math.min(dp[i][j-1],dp[i-1][j])+grid[i][j];
}
}
}
return dp[n-1][m-1];
}
}
例题16:编辑距离
本质不同的操作实际上只有三种:
在单词 A 中插入一个字符;
在单词 B 中插入一个字符;
修改单词 A 的一个字符。
D[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离
class Solution {
public int minDistance(String word1, String word2) {
int n=word1.length(),m=word2.length();
//当n和m有一个为0时,优美的解决方案
if(n*m==0){
return n+m;
}
//dp[i][j]代表word1前i个字符和word2前j个字符的编辑距离
int[][] dp=new int[n+1][m+1];
for(int i=0;i<=n;++i){
for(int j=0;j<=m;++j){
//边界
if(i==0){
dp[i][j]=j;
}else if(j==0){
dp[i][j]=i;
}else{
//非边界
int left=dp[i][j-1]+1;
int up=dp[i-1][j]+1;
int left_up=dp[i-1][j-1]+1;
if(word1.charAt(i-1)==word2.charAt(j-1)){
left_up=dp[i-1][j-1];
}
dp[i][j]=Math.min(left,Math.min(up,left_up));
}
}
}
return dp[n][m];
}
}
例题17:单词拆分
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> set=new HashSet<>();
for(String word:wordDict){
set.add(word);
}
int n=s.length();
boolean[] dp=new boolean[n+1];
dp[0]=true;
for(int j=1;j<=n;++j){
for(int i=0;i<j;++i){
if(dp[i]&&set.contains(s.substring(i,j))){
dp[j]=true;
break;
}
}
}
return dp[n];
}
}
例题18:K个逆序对数组
class Solution {
public int kInversePairs(int n, int k) {
final int MOD = 1000000007;
int[][] f = new int[2][k + 1];
f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= k; ++j) {
int cur = i & 1, prev = cur ^ 1;
f[cur][j] = (j - 1 >= 0 ? f[cur][j - 1] : 0) - (j - i >= 0 ? f[prev][j - i] : 0) + f[prev][j];
if (f[cur][j] >= MOD) {
f[cur][j] -= MOD;
} else if (f[cur][j] < 0) {
f[cur][j] += MOD;
}
}
}
return f[n & 1][k];
}
}