基础递推
fibo
dp[i]表示第i个数是dp[i]
class Solution {
public int fib(int n) {
if(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];
}
return dp[n];
}
}
爬楼梯的方法数
dp[i]表示爬到第i阶有dp[i]种方法,dp[i]=dp[i-1]+dp[i-2],爬到第i阶的方法数是爬到第i-1阶和第i-2阶的方法数之和
class Solution {
public int climbStairs(int n) {
if(n<=1)return n;
int []dp=new int [n+1];
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
最少体力爬楼梯
dp[i]表示爬到第i阶楼梯花费的最少体力
class Solution {
public int minCostClimbingStairs(int[] cost) {
int len=cost.length;
int []dp=new int [len+1];
//到达下标0或1不消耗体力
dp[0]=0;
dp[1]=0;
for(int i=2;i<=len;i++){
dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[len];
}
}
左上角到右下角的路径数量
dp[i][j]表示到达(i,j)时的路径数,(i,j)可由(i-1,j)和(i,j-1)推出,dp[i][j]=dp[i-1][j]+dp[i][j-1]
class Solution {
public int uniquePaths(int m, int n) {
int [][]dp=new int[m][n];
for(int i=0;i<n;i++)dp[0][i]=1;//第一行
for(int j=0;j<m;j++)dp[j][0]=1;//第一列
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
走迷宫的路径总数
初始化不同,第一行第一列只有不存在障碍物的
前面部分才会被初始化为1,(i,j)如果有障碍物,则跳过不推导
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m=obstacleGrid.length;
int n=obstacleGrid[0].length;
int[][]dp=new int[m][n];
if(obstacleGrid[m-1][n-1]==1||obstacleGrid[0][0]==1){
return 0;//终点有障碍物
}
for(int i=0;i<m&&obstacleGrid[i][0]==0;i++)dp[i][0]=1;//第一列初始化
for(int j=0;j<n&&obstacleGrid[0][j]==0;j++)dp[0][j]=1;//第一行初始化
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=(obstacleGrid[i][j]==0)?dp[i-1][j]+dp[i][j-1]:0;
}
}
return dp[m-1][n-1];
}
}
整数拆分的最大乘积
dp[i]表示将i拆分的最大乘积,取 j*(i-j)和 j*dp[i-j]的最大值
class Solution {
public int integerBreak(int n) {
int []dp=new int[n+1];
dp[2]=1; //2=1+1,1*1=1
for(int i=3;i<=n;i++){
for(int j=1;j<=i/2;j++){
dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
}
}
return dp[n];
}
}
n个结点的二叉搜索树总数
二叉搜索树的概念
例如
递推公式
dp[i]表示由i个结点形成的二叉树的数量=1~i个结点分别作为头节点形成二叉树的数量总和
class Solution {
public int numTrees(int n) {
int []dp=new int[n+1];
dp[0]=1; //0个结点的二叉树
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
dp[i]+=dp[j-1]*dp[i-j]; //左子树小于j,为j-1个右子树大于j,为i-j个
}
}
return dp[n];
}
}
零一背包
二维数组
分析:
递推公式:
dp[i][j]表示从0~i个物品中选,放进体积不超过j的背包中,最大价值
public class bag01{
public static void main(String[] args) {
int []weight={1,3,4};
int []value={15,20,30};
int bagsize=4;
bag01(weight,value,bagsize);
}
public static void bag01(int []weight,int []value,int bagsize){
int wlen=weight.length;
int[][]dp=new int[wlen+1][bagsize+1];
for(int j=weight[0];j<=bagsize;j++){
dp[0][j]=value[0]; //装得下第一个物品的背包才有价值
}
for(int i=1;i<wlen;i++){
for(int j=0;j<=bagsize;j++){
if(j<weight[i])
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]);//取最大值
}
}
//打印物品和背包的二维数组
for(int i=0;i<wlen;i++){
for(int j=0;j<=bagsize;j++){
System.out.print(dp[i][j]+" ");
}
System.out.println();
}
System.out.println(dp[wlen-1][bagsize]);
}
}
滚动优化
逆序遍历背包的原因:
物品和背包遍历顺序:
dp[j]表示体积为j的背包能装下物品的最大价值
public static void bag02(int[]weight,int value[],int bagsize){
int []dp=new int[bagsize+1];
for(int i=0;i<weight.length;i++){
for(int j=bagsize;j>=weight[i];j--){
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
for(int i=0;i<=bagsize;i++){
System.out.print(dp[i]+" ");
}
}
价值与体积的乘积最大
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=30010;
static int []v=new int[N];
static int []w=new int[N];
static int []dp=new int[N];
public static void main(String[]args){
int n=sc.nextInt(); //背包容积
int m=sc.nextInt(); //物品种数
for(int i=0;i<m;i++){
v[i]=sc.nextInt();
w[i]=sc.nextInt();
}
for(int i=0;i<m;i++){
for(int j=n;j>=v[i];j--){
dp[j]=Math.max(dp[j],dp[j-v[i]]+v[i]*w[i]);
}
}
System.out.println(dp[n]);
}
}
装箱问题
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int V=sc.nextInt();
int n=sc.nextInt();
int N=20010;
int[]v=new int[N];
int[]dp=new int[N];
for(int i=0;i<n;i++){
v[i]=sc.nextInt();
}
for(int i=0;i<n;i++){
for(int j=V;j>=v[i];j--){
dp[j]=Math.max(dp[j],dp[j-v[i]]+v[i]);
}
}
System.out.println(V-dp[V]);
}
}
分割成两个子集和相等
背包体积为sum/2,dp[j]表示体积不超过sum/2的背包能背的物品的最大价值,此题价值和体积相同,dp[j]=Math.max(dp[j],dp[j-[nums[i]]+nums[i]),如果最大值刚好装满背包,则返回true
class Solution {
public boolean canPartition(int[] nums) {
if(nums==null||nums.length==0)
return false;
int sum=0;
for(int num:nums){
sum+=num;
}
if(sum%2!=0)
return false;
int halfSum=sum/2;
int []dp=new int[halfSum+1];
for(int i=0;i<nums.length;i++){
for(int j=halfSum;j>=nums[i];j--){
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[halfSum]==halfSum;
}
}
子集和抵消的最小值
尽量将石头分为重量相等的两堆,dp[j]表示容积不超过j的背包所能装下的最大重量
class Solution {
public int lastStoneWeightII(int[] stones) {
int len=stones.length;
int sum=0;
for(int stone:stones){
sum+=stone;
}
int target=sum>>1;
int []dp=new int[target+1];
for(int i=0;i<len;i++){
for(int j=target;j>=stones[i];j--){
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
//将sum-dp[target] 和dp[target}两堆相减
return sum-2*dp[target];
}
}
添加+和-得到目标和的方法
l-r=target;
l+r=sum;
l=(sum+target)/2;
问题转化为容积不超过(sum+target)/2的背包刚好能装满的方法数,dp[j]表示装满容积为j的背包的方法数,特别地要考虑初始化dp[0]
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum=0;
for(int num:nums){
sum+=num;
}
int bagSize=(sum+target)/2;
if(Math.abs(target)>sum)
return 0;
if((sum+target)%2==1)//不要整除后再取余
return 0;
int []dp=new int[bagSize+1];
dp[0]=1; //初始值不能是0!
for(int i=0;i<nums.length;i++){
for(int j=bagSize;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[bagSize];
}
}
最大子集长度包含1和0
dp[i][j]表示0的个数不超过i,1的个数不超过j,的字串的最多数量,dp[i][j]由dp[i-zero][j-one]转移过来
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][]dp=new int [m+1][n+1];
for(String str:strs){
int zero=0;
int one=0;
//得到每个字串0 1数量
for(char c:str.toCharArray()){
if(c=='0') zero++;
else one++;
}
//倒序遍历两个背包
for(int i=m;i>=zero;i--){
for(int j=n;j>=one;j--){
dp[i][j]=Math.max(dp[i][j],dp[i-zero][j-one]+1);
}
}
}
return dp[m][n];
}
}
目标和组合总数
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int sum=sc.nextInt();
int[]a=new int[110];
int[]dp=new int[100010];
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
/*
dp[i]表示凑成和为i的方案数,
将集合划分为dp[i-a[i]],dp[i]+=dp[i-a[i]],将所有子方案数相加
*/
dp[0]=1;
for(int i=0;i<n;i++){ //先遍历物品
for(int j=sum;j>=a[i];j--){ //再遍历背包
dp[j]+=dp[j-a[i]];
}
}
System.out.println(dp[sum]);
}
}
背包最优解方案数
import java.util.*;
public class Main{
/*
dp[i] 用来存储背包容积为 i 时的最佳方案的总价值,
cnt[i]为背包容积为 i 时总价值为最佳的方案数
求出装新物品时的总价值,与不装新物品时作对比
如果装新物品的方案总价值更大,那么用 dp[j−v]+w 来更新 dp[j],用cnt[j−v]更新cnt[j]
如果总价值相等,那么最大价值的方案数就多了cnt[j−v] 种
*/
public static void main(String[]args){
int N=1010,res=0,mod=(int)1e9+7;
int []v=new int[N];
int []w=new int[N];
int []dp=new int[N];
int []cnt=new int[N];
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int V=sc.nextInt();
for(int i=0;i<n;i++){
v[i]=sc.nextInt();
w[i]=sc.nextInt();
}
//先初始化所有的cnt[i] 为 11,因为背包里什么也不装也是一种方案
Arrays.fill(cnt,1);
for(int i=0;i<n;i++){
for(int j=V;j>=v[i];j--){
int value=dp[j-v[i]]+w[i];
if(value>dp[j]){
dp[j]=value; //状态转移
cnt[j]=cnt[j-v[i]]; //状态转移
}else if(value==dp[j]){
cnt[j]=(cnt[j]+cnt[j-v[i]])%mod; //累加得到方案数
}
}
}
System.out.println(cnt[V]);
}
}
背包问题具体方案
题解:
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int N=1010;
int [][]dp=new int[N][N];
int []v=new int[N];
int []w=new int[N];
int n=sc.nextInt();
int V=sc.nextInt();
for(int i=1;i<=n;i++){
v[i]=sc.nextInt();
w[i]=sc.nextInt();
}
for(int i=n;i>=1;i--){
for(int j=0;j<=V;j++){
if(j<v[i])
dp[i][j]=dp[i+1][j];
else
dp[i][j]=Math.max(dp[i+1][j],dp[i+1][j-v[i]]+w[i]);
}
}
int cur_v=V;
for(int i=1;i<=n;i++){
//如果是最后一个元素,特判一下,防止越界即可
if(i==n&&cur_v>=v[i]){
System.out.println(i+" ");
break;
}else if(cur_v<=0){
break;
}else if(cur_v>=v[i]&&dp[i][cur_v]==dp[i+1][cur_v-v[i]]+w[i]){
System.out.print(i+" ");
cur_v-=v[i];
}
}
}
}
完全背包
class multibag{
public static void main(String[] args) {
int[]weight={1,3,4};
int[]value={15,20,30};
int bagWeight=4;
int[]dp=new int[bagWeight+1];
for(int i=0;i<weight.length;i++){
for(int j=weight[i];j<=bagWeight;j++){
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
System.out.println(dp[bagWeight]);
}
}
目标和排列数
class Solution {
public int combinationSum4(int[] nums, int target) {
int []dp=new int[target+1];
dp[0]=1;
for(int i=0;i<=target;i++){ //先遍历背包
for(int j=0;j<nums.length;j++){ //再遍历物品
if(i>=nums[j])
dp[i]+=dp[i-nums[j]];
}
}
return dp[target];
}
}
爬楼梯的排列数
假设要爬上n级台阶,每次可以跳1阶,2阶…最多可以跳m阶,求爬上n级台阶有多少种方法
//完全背包问题
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
int[] weight = {1,2};
dp[0] = 1;
for (int i = 0; i <= n; i++) {
for (int j = 0; j < weight.length; j++) {
if (i >= weight[j])
dp[i] += dp[i - weight[j]];
}
}
return dp[n];
}
}
零钱兑换的组合数
先遍历物品再遍历背包,计算的是组合数
先遍历背包再遍历物品计算的是排列数
求组合数:dp[j]表示装满容积为j的背包的方法数,dp[j]+=dp[j-value[i]]
class Solution {
public int change(int amount, int[] coins) {
int []dp=new int[amount+1];
dp[0]=1;
for(int i=0;i<coins.length;i++){ //先遍历物品
for(int j=coins[i];j<=amount;j++){ //再遍历背包
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
}
零钱兑换最少个数
class Solution {
public int coinChange(int[] coins, int amount) {
//dp[i]表示凑成总金额为i所需的最少硬币个数
int []dp=new int[amount+1];
int max=Integer.MAX_VALUE;
//注意初始化范围是dp的长度,求最小值一般是初始化为最大值
for(int i=0;i<dp.length;i++){
dp[i]=max;
}
dp[0]=0;
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
if(dp[j-coins[i]]!=max)
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount]==max?-1:dp[amount];
}
}
完全平方数最少个数
class Solution {
public int numSquares(int n) {
//dp[i]:完全平方和为i所需的最少平方数
//dp[i]=Math.min(dp[i],dp[i-j*j]+1)
int []dp=new int[n+1];
int max=Integer.MAX_VALUE;
for(int i=0;i<dp.length;i++){
dp[i]=max;
}
dp[0]=0;
for(int i=0;i<=n;i++){
for(int j=1;j*j<=i;j++){
dp[i]=Math.min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
}
买书
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int amount=sc.nextInt();
int []money={10,20,50,100};
int []dp=new int[1010];
//初始化
dp[0]=1;
for(int i=0;i<money.length;i++){
for(int j=money[i];j<=amount;j++){
dp[j]+=dp[j-money[i]];
}
}
System.out.println(dp[amount]);
}
}
货币系统
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int sum=sc.nextInt();
int []money=new int[20];
long []dp=new long[3010];
for(int i=0;i<n;i++){
money[i]=sc.nextInt();
}
dp[0]=1;
for(int i=0;i<n;i++){
for(int j=money[i];j<=sum;j++){
dp[j]+=dp[j-money[i]];
}
}
System.out.println(dp[sum]);
}
}
极大线性无关组个数–货币系统
题目分析:
dp:
import java.util.*;
public class Main{
static int N=110,M=25010;
static int[]v=new int[N];
static boolean []dp=new boolean[M];
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int T=sc.nextInt();
while(T-->0){
int n=sc.nextInt();
for(int i=0;i<n;i++){
v[i]=sc.nextInt();
}
/*
从小到大,先查看当前数有没有被晒掉,
1)如果没有就把它加入到最大无关向量组中,并把他以及他和此前的硬币的线性组合都筛掉
2)如果有就不理会
即就是在完全背包求方案数的过程中,统计那些初始没有方案数的物品
*/
Arrays.sort(v,0,n);
Arrays.fill(dp,false); //每次记得重制数组
/*
我们只需统计所有物品的体积是否能被其他的线性表出
因此背包的体积只需设置为最大的物品体积即可
res用来记录最大无关向量组的个数
*/
int max=v[n-1],res=0;
dp[0]=true;
for(int i=0;i<n;i++){
//如果当前物品体积被之前的物品组合线性筛掉了,则它是无效的
if(dp[v[i]]==true)
continue;
//否则线性无关组个数递增
res++;
/*筛掉当前最大无关向量组能线性表示的体积
完全背包中能被表示的体积置为false*/
for(int j=v[i];j<=max;j++){
dp[j]|=dp[j-v[i]];
}
}
System.out.println(res);
}
}
}
单词拆分
完全背包问题的转换:
字符串s视为背包,字典wordDict作为物品,本题判断在物品数量不限的情况下,能否将背包充满.valid[i]==true,表示考虑截取s中长度为i的子字符串,在字典wordDict中出现的情况.
如果valid[s.length()]==true,则说明s可以分割为在字典中的单词.
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
//放入集合
HashSet<String>set=new HashSet<>(wordDict);
//valid[i]==true,表示长度为i的字符串可以分割为字典中的单词
boolean []valid=new boolean[s.length()+1];
//初始化
valid[0]=true;
//先遍历背包再遍历物品
for(int i=1;i<=s.length();i++){ //遍历背包
for(int j=0;j<i;j++){ //遍历物品
if(set.contains(s.substring(j,i))&&valid[j]){
valid[i]=true;
}
}
}
return valid[s.length()];
}
}
多重背包
import java.util.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
//读入物品种数和背包体积
int N=sc.nextInt();
int V=sc.nextInt();
//物品的体积,价值,数量
int[]v=new int[110];
int[]w=new int[110];
int[]num=new int[110];
for(int i=1;i<=N;i++){
v[i]=sc.nextInt();
w[i]=sc.nextInt();
num[i]=sc.nextInt();
}
int[]dp=new int[110];
for(int i=1;i<=N;i++){ //遍历物品
for(int j=V;j>=v[i];j--){ //逆序遍历背包
for(int k=1;k<=num[i]&&(j-k*v[i])>=0;k++){ //遍历数量
dp[j]=Math.max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
}
System.out.println(dp[V]);
}
}
分组背包
import java.util.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
//读入物品组数和背包体积
int N=sc.nextInt();
int V=sc.nextInt();
//读入每组物品的种数,每个物品的体积,价值
int[]num=new int[110];
int[][]v=new int[110][110];
int[][]w=new int[110][110];
for(int i=1;i<=N;i++){
num[i]=sc.nextInt(); //每组物品种数
for(int j=1;j<=num[i];j++){
v[i][j]=sc.nextInt(); //第i组第j个物品的体积
w[i][j]=sc.nextInt(); //第i组第j个物品的价值
}
}
int[]dp=new int[110];
for(int i=1;i<=N;i++){
for(int j=V;j>=0;j--){
for(int k=1;k<=num[i];k++){
if((j-v[i][k])>=0)
dp[j]=Math.max(dp[j],dp[j-v[i][k]]+w[i][k]);
}
}
}
System.out.println(dp[V]);
}
}
机器分配
分析:
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=20;
static int [][]w=new int[N][N];
static int []dp=new int[N];
static int [][]select=new int[N][N];
public static void main(String[]args){
int n=sc.nextInt();
int m=sc.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
w[i][j]=sc.nextInt(); //第i家公司分配j台机器的价值
}
}
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=1;k<=j&&(j-k)>=0;k++){
//dp[j]=Math.max(dp[j],dp[j-k]+w[i][k]);
int res=dp[j-k]+w[i][k];
if(res>dp[j]){
select[i][j]=k; //第i个公司分配的机器数量
dp[j]=res;
}
}
}
}
System.out.println(dp[m]); //最大盈利
print(n,m); //各个公司分配情况
}
//打印第i个公司分配k台机器
public static void print(int n,int m){
if(n==0)
return;
int k=select[n][m];
print(n-1,m-k);
System.out.println(n+" "+k);
}
}
混合背包
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=1010;
static int []dp=new int[N];
public static void main(String[]args){
int n=sc.nextInt();
int V=sc.nextInt();
for(int i=0;i<n;i++){
int v=sc.nextInt();
int w=sc.nextInt();
int num=sc.nextInt();
//01背包问题
if(num==-1){
for(int j=V;j>=v;j--){
dp[j]=Math.max(dp[j],dp[j-v]+w);
}
}
//完全背包问题
else if(num==0){
for(int j=v;j<=V;j++){
dp[j]=Math.max(dp[j],dp[j-v]+w);
}
}
//多重背包问题
else if(num>0){
for(int j=V;j>=v;j--){
for(int k=1;k<=num&&(j-k*v)>=0;k++){
dp[j]=Math.max(dp[j],dp[j-k*v]+k*w);
}
}
}
}
System.out.println(dp[V]);
}
}
二维费用背包问题
第二维的最小价值–捕获精灵
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt(); //初始球数量
int M=sc.nextInt(); //初始体力
int K=sc.nextInt(); //精灵数量
int[]v1=new int[1010]; //第一费用,消耗的精灵球
int[]v2=new int[1010]; //第二费用,消耗的体力
int[][]dp=new int[1010][1010];
for(int i=0;i<K;i++){
v1[i]=sc.nextInt();
v2[i]=sc.nextInt();
}
/*
dp[i][j]表示第一费用不超过i,第二费用不超过j的
捕获的最大数量
*/
for(int i=0;i<K;i++){
for(int j=N;j>=v1[i];j--){ //第一费用
for(int k=M;k>=v2[i];k--){ //第二费用
dp[j][k]=Math.max(dp[j][k],dp[j-v1[i]][k-v2[i]]+1);
}
}
}
System.out.println(dp[N][M-1]);
/*
在满足数量最大情况下的最小第二费用
*/
int cost_health=M;
for(int i=0;i<M;i++){
if(dp[N][i]==dp[N][M-1]){ //在满足数量最大情况下
cost_health=Math.min(cost_health,i);
}
}
System.out.println(cost_health);
}
}
二维费用的最大价值–背包
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=1010;
static int []volume=new int[N];
static int []weight=new int[N];
static int []value=new int[N];
static int [][]dp=new int[N][N];
public static void main(String[]args){
int N=sc.nextInt();
int V=sc.nextInt();
int M=sc.nextInt();
for(int i=0;i<N;i++){
volume[i]=sc.nextInt();
weight[i]=sc.nextInt();
value[i]=sc.nextInt();
}
for(int i=0;i<N;i++){
for(int j=V;j>=volume[i];j--){
for(int k=M;k>=weight[i];k--){
dp[j][k]=Math.max(dp[j][k],dp[j-volume[i]][k-weight[i]]+value[i]);
}
}
}
System.out.println(dp[V][M]);
}
}
二维费用的最小重量–潜水员
对比:
dp[i,j,k]表示从1~n个物中选择,氧气体积至少是j,氮气体积至少是k的最小重量
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=1010,MAX=0x3f3f3f3f;
static int []o=new int[N];
static int []n=new int[N];
static int []weight=new int[N];
static long [][]dp=new long[N][N];
public static void main(String[]args){
int o_need=sc.nextInt();
int n_need=sc.nextInt();
int T=sc.nextInt();
for(int i=0;i<N;i++){
Arrays.fill(dp[i],MAX);
}
for(int i=0;i<T;i++){
o[i]=sc.nextInt();
n[i]=sc.nextInt();
weight[i]=sc.nextInt();
}
//初始化为0种方案
dp[0][0]=0;
//所有从前i个物品中选,总氧气至少是j,总氮气至少是k的方案的最小值
for(int i=0;i<T;i++){
for(int j=o_need;j>=0;j--){
for(int k=n_need;k>=0;k--){
/*
如果j < v1[i] ,那么j - v1[i]就会是负数 ,那就说明我们加上这个v1之后就会满了
那么我们就没必要从前i-1个物品中选了,因为加上第i个物品已经够了
所以直接将前面的负数置为0,刚好满了 跟 满溢出了是一种道理
都是没有必要在从前面的i-1个物品中选了,直接取0
*/
dp[j][k]=Math.min(dp[j][k],dp[Math.max(0,j-o[i])][Math.max(0,k-n[i])]+weight[i]);
}
}
}
System.out.println(dp[o_need][n_need]);
}
}
背包总结
递推公式:
能否装满背包:
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
装满背包的方法数:
dp[j] += dp[j - nums[i]]
装满背包的最大价值:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
装满背包所需最小物品个数:
dp[j] = min(dp[j - coins[i]] + 1, dp[j])
遍历顺序:
01背包:
二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历;
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历
完全背包:
纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
数字三角形模型
三角形路径最大值
import java.util.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
//dp[i][j]表示走到a[i][j]时最大路径和
int[][]dp=new int[510][510];
//保存路径
int[][]a=new int[510][510];
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
a[i][j]=sc.nextInt();
}
}
//初始化最后一层
for(int j=1;j<=n;j++){
dp[n][j]=a[n][j];
}
//从倒数第二层开始往上走
//a[i][j]可以来自两个点,a[i+1][j],a[i+1][j+1],取两者的最大值
for(int i=n-1;i>=1;i--){
for(int j=1;j<=i;j++){
dp[i][j]=Math.max(dp[i+1][j]+a[i][j],dp[i+1][j+1]+a[i][j]);
}
}
System.out.println(dp[1][1]);
}
}
摘花生
dp[i][j]表示从起点到(i,j)点的所有路径集合中,路径的最大和,dp[i][j]可由两种情况转移,从上方转移dp[i-1][j],从左方转移dp[i][j-1],取两种情况最大值即可
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int N=110;
int[][]w=new int[N][N];
int[][]dp=new int[N][N];
int T=sc.nextInt();
while(T-->0){
int n=sc.nextInt();
int m=sc.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
w[i][j]=sc.nextInt();
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j]=Math.max(dp[i-1][j]+w[i][j],dp[i][j-1]+w[i][j]);
}
}
System.out.println(dp[n][m]);
}
}
}
最少通行费
根据时间限制可以推出商人只能往下或者往左走,求最小值问题时要初始化数组为正无穷
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int N=110,INF=0x3f3f3f3f;
int[][]w=new int[N][N];
int[][]dp=new int[N][N];
int n=sc.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
w[i][j]=sc.nextInt();
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
//初始化起点
if(i==1&&j==1){
dp[i][j]=w[i][j];
}else{
//本题求最小值,要先初始化为无穷,否则默认为0开始
dp[i][j]=INF;
//当i>1时,才能从上面下来
if(i>1) dp[i][j]=Math.min(dp[i][j],dp[i-1][j]+w[i][j]);
//当j>1时,才能从左边过来
if(j>1)dp[i][j]=Math.min(dp[i][j],dp[i][j-1]+w[i][j]);
}
}
}
System.out.println(dp[n][n]);
}
}
方格取数
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int N=20;
int[][][]dp=new int[N+N][N][N];
int[][]w=new int[N][N];
int n=sc.nextInt();
while(true){
int x=sc.nextInt();
int y=sc.nextInt();
int z=sc.nextInt();
w[x][y]=z;
if(x==0&&y==0&z==0)break;
}
//两次同时走,则路径之和为2
for(int k=2;k<=n+n;k++){
for(int i1=1;i1<=n;i1++){ //第一次
for(int i2=1;i2<=n;i2++){ //第二次
//两次走的纵坐标
int j1=k-i1,j2=k-i2;
if(j1>=1&&j1<=n&&j2>=1&&j2<=n){
//权重
int t=w[i1][j1];
//走的点不重合则加两个权重
if(i1!=i2)t+=w[i2][j2];
//四种情况
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1-1][i2]+t);
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1][i2-1]+t);
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1][i2]+t);
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1-1][i2-1]+t);
}
}
}
}
System.out.println(dp[n+n][n][n]);
}
}
传纸条
首先考虑路径有交集该如何处理:
可以发现交集中的格子一定在每条路径的相同步数处。
因此可以让两个人同时从起点出发,每次同时走一步,这样路径中相交的格子一定在同一步内。状态表示: f[k, i, j] 表示两个人同时走了k步, 第一个人在 (i, k - i) 处,第二个人在 (j, k -
j)处的所有走法的最大路径和
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=55,n,m;
static int[][][]dp=new int[N*N][N][N];
static int[][]w=new int[N][N];
public static void main(String[]args){
n=sc.nextInt(); //行
m=sc.nextInt(); //列
for(int i = 1 ; i <= n; i ++ )
for(int j = 1; j <= m ; j ++ )
w[i][j] = sc.nextInt();
//两人同时走,则路径之和初始为2
for(int k=2;k<=n+m;k++){
for(int i1=1;i1<=n;i1++){ //第一条路线
for(int i2=1;i2<=n;i2++){ //第二条路线
//两次走的纵坐标
int j1=k-i1,j2=k-i2;
if(j1>=1&&j1<=m&&j2>=1&&j2<=m){
//默认权重
int t=w[i1][j1];
//走的点不重合则加两个权重
if(i1!=i2)t+=w[i2][j2];
//四种情况
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1-1][i2]+t);
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1][i2-1]+t);
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1][i2]+t);
dp[k][i1][i2]=Math.max(dp[k][i1][i2],dp[k-1][i1-1][i2-1]+t);
}
}
}
}
System.out.println(dp[n+m][n][n]);
}
}
最长上升子序列模型
最长上升子序列
数据范围:0~1000
import java.util.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int []a=new int[1001];
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
//dp[i]表示以a[i]结尾的序列的最大长度
int[]dp=new int[1001];
int max_len=1;
for(int i=0;i<n;i++){
dp[i]=1; //初始化长度为1
for(int j=0;j<i;j++){
if(a[i]>a[j]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
max_len=Math.max(max_len,dp[i]);
}
}
System.out.println(max_len);
}
}
class Solution {
public int lengthOfLIS(int[] nums) {
int len=nums.length;
int[]dp=new int[len+1];
int res=0;
for(int i=0;i<len;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
res=Math.max(res,dp[i]);
}
return res;
}
}
数据范围:0~1000000
import java.util.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[]a=new int[1000010];
int[]q=new int[1000010]; //每种长度的单增序列结尾的最小值
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
int len=0;
q[0]=(int)-2e9; //保证每一种长度结尾的最小值后面都能接数
for(int i=0;i<n;i++){
//将a[i]与q[i]比较,将a[i]接到q[i]后面,并更新长度
int l=0,r=len;
while(l<r){
int mid=l + r + 1 >>1;
if(q[mid]<a[i]){
l=mid;
}else{
r=mid-1;
}
}
len=Math.max(len,r+1);
q[r+1]=a[i];
}
System.out.println(len);
}
}
最长连续上升序列
class Solution {
public int findLengthOfLCIS(int[] nums) {
int len=nums.length;
//dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。
int[]dp=new int[len+1];
int res=1;
//初始化长度为1
for(int i=0;i<len;i++){
dp[i]=1;
}
for(int i=1;i<len;i++){
if(nums[i]>nums[i-1]){
dp[i]=Math.max(dp[i],dp[i-1]+1);
}
res=Math.max(res,dp[i]);
}
return res;
}
}
最大上升子序列和
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
final int N=1010;
int n=sc.nextInt();
int[]a=new int[N];
//dp[i]表示以nums[i]结尾的上升子序列和
int[]dp=new int[N];
int res=0;
for(int i=1;i<=n;i++){
a[i]=sc.nextInt();
dp[i]=a[i]; //初始化和
}
//状态转移dp[i]=Math.max(dp[i],dp[j]+a[i])
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
if(a[i]>a[j])
dp[i]=Math.max(dp[i],dp[j]+a[i]);
}
res=Math.max(res,dp[i]);
}
System.out.println(res);
}
}
最大连续子序列和
dp[i]表示以nums[i]结尾的子序列和
dp[i]要么是从nums[i]转移过来,代码从头开始计算;要么从dp[i-1]+nums[i]转移; dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);
初始化dp[0]=nums[0],res=nums[0];
class Solution {
public int maxSubArray(int[] nums) {
int len=nums.length;
if(len==0)return 0;
int[]dp=new int[len+1];
dp[0]=nums[0];
int res=nums[0];
for(int i=1;i<len;i++){
dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);
res=Math.max(dp[i],res);
}
return res;
}
}
最长公共子序列
import java.util.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
int a_len=sc.nextInt();
int b_len=sc.nextInt();
String A=sc.next();
String B=sc.next();
char[]a=new char[1001];
char[]b=new char[1001];
//字符串转化为字符数组,用于下标索引
for(int i=1;i<=a_len;i++)a[i]=A.charAt(i-1);
for(int j=1;j<=b_len;j++)b[j]=B.charAt(j-1);
//dp[i][j]表示既在a的前i个字符中出现,又在b的前j个字符中出现的长度
int[][]dp=new int[1001][1001];
for(int i=1;i<=a_len;i++){
for(int j=1;j<=b_len;j++){
if(a[i]==b[j]){ //相等则直接在dp[i-1][j-1]基础上长度+1
dp[i][j]=Math.max(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[a_len][b_len]);
}
}
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
本质上还是求公共子序列
dp[i][j]:既在A的前i-1个字符中出现又在B的前j-1个字符中出现的长度
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int len1=nums1.length;
int len2=nums2.length;
int[][]dp=new int[len1+1][len2+1];
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(nums1[i-1]==nums2[j-1]){
dp[i][j]=Math.max(dp[i][j],dp[i-1][j-1]+1);
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[len1][len2];
}
}
最长连续公共子序列
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int len1=nums1.length;
int len2=nums2.length;
//dp[i][j]表示既在a的前i-1个字符中出现,又在b的前j-1个字符中出现的长度
int[][]dp=new int[len1+1][len2+1];
int res=0;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(nums1[i-1]==nums2[j-1]){
dp[i][j]=Math.max(dp[i][j],dp[i-1][j-1]+1);
}
res=Math.max(res,dp[i][j]);
}
}
return res;
}
}
最长公共上升子序列
对于两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
状态表示:
集合:f[i, j]表示第一个序列前i个字母以及第二个序列前j个字母,并且以b[j]结尾的公共上升子序列的集合
属性:代表f[i, j]这个集合子序列中长度最大值
集合划分:
通过a[i] == b[j]成立与否,划分为两部分
a[i] != b[j] –> 无法构成新的公共子序列,f[i][j] = f[i - 1][j]
a[i] == b[j] –> 可以构成新的公共子序列,将其继续进行划分
划分依据:公共子序列倒数第二个元素在b[]序列中所对应的数
子序列只包含b[j]本身 –> f[i][j] = max(f[i][j], 1)
子序列倒数第二个数是b[1] –> f[i][j] = max(f[i][j], f[i - 1][1] + 1)
…
子序列倒数第二个数是b[j - 1] –> f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1)
若存在倒数第二个数,则有 b[j] > b[k] (0<=k<j) 方能构成上升子序列,由于a[i]==b[j],因此通过a[i]>b[j]进行单调性比较,用额外的变量maxv来表示满足a[i] > b[k]情况下所有的f[i - 1][k] + 1的最大值
import java.util.*;
public class Main{
static final int N=3010;
static int []a=new int[N];
static int []b=new int[N];
static int [][]dp=new int[N][N];
static Scanner sc=new Scanner(System.in);
public static void main(String[]args){
int n=sc.nextInt();
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
for(int j=0;j<n;j++){
b[j]=sc.nextInt();
}
for(int i=1;i<=n;i++){
int maxv=1;
for(int j=1;j<=n;j++){
//a[i]!=b[j]时,dp[i][j]由dp[i-1][j]转移过来
dp[i][j]=dp[i-1][j];
if(a[i]==b[j]){
dp[i][j]=Math.max(dp[i][j],maxv);
}
//maxv代表满足a[i]>b[j]的所有dp[i-1][0~j]的最大值
if(a[i]>b[j]){
maxv=Math.max(maxv,dp[i-1][j]+1);
}
}
}
int res=0;
for(int i=1;i<=n;i++){
res=Math.max(res,dp[n][i]);
}
System.out.println(res);
}
}
怪盗基德的滑翔翼
import java.util.*;
public class Main{
public static void main(String []args){
Scanner sc=new Scanner(System.in);
int T=sc.nextInt();
int[]a=new int[110];
int[]dp=new int[110];
while(T-->0){
int n=sc.nextInt();
for(int i=1;i<=n;i++){
a[i]=sc.nextInt();
}
int res=0;
//由于所求的子序列可以单增,也可以单调递减,因此要做两遍,取最大值
//正
for(int i=1;i<=n;i++){
dp[i]=1;
for(int j=1;j<i;j++){
if(a[j]<a[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
res=Math.max(res,dp[i]);
}
//反
for(int i=n;i>=1;i--){
dp[i]=1;
for(int j=n;j>i;j--){
if(a[i]>a[j]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
res=Math.max(res,dp[i]);
}
System.out.println(res);
}
}
}
登山
import java.util.*;
public class Main{
public static void main(String []args){
Scanner sc=new Scanner(System.in);
int res=0;
int[]a=new int[1100];
int[]dp=new int[1100]; //以a[i]结尾的单增子序列的长度最大值
int[]dp2=new int[1100]; //以a[i]结尾的单减子序列的长度最大值
int n=sc.nextInt();
for(int i=1;i<=n;i++){
a[i]=sc.nextInt();
}
//从左往右看单增
for(int i=1;i<=n;i++){
dp[i]=1;
for(int j=1;j<i;j++){
if(a[j]<a[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
}
//从右往左看单增
for(int i=n;i>=1;i--){
dp2[i]=1;
for(int j=n;j>i;j--){
if(a[i]>a[j]){
dp2[i]=Math.max(dp2[i],dp2[j]+1);
}
}
}
for(int i=1;i<=n;i++){
res=Math.max(res,dp[i]+dp2[i]-1);
}
System.out.println(res);
}
}
合唱队形
import java.util.*;
public class Main{
public static void main(String []args){
Scanner sc=new Scanner(System.in);
int res=0;
int[]a=new int[1100];
int[]dp=new int[1100];
int[]dp2=new int[1100];
int n=sc.nextInt();
for(int i=1;i<=n;i++){
a[i]=sc.nextInt();
}
//从左往右看单增
for(int i=1;i<=n;i++){
dp[i]=1;
for(int j=1;j<i;j++){
if(a[j]<a[i]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
}
//从右往左看单增
for(int i=n;i>=1;i--){
dp2[i]=1;
for(int j=n;j>i;j--){
if(a[i]>a[j]){
dp2[i]=Math.max(dp2[i],dp2[j]+1);
}
}
}
for(int i=1;i<=n;i++){
res=Math.max(res,dp[i]+dp2[i]-1);
}
System.out.println(n-res);
}
}
友好城市
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=5001;
static int[]dp=new int[N];
static int res=0;
static PII[]q=new PII[N*N];
public static void main(String []args){
int n=sc.nextInt();
for(int i=1;i<=n;i++){
int a=sc.nextInt();
int b=sc.nextInt();
q[i] = new PII(a,b);
}
//按照上坐标排序
Arrays.sort(q,1,1+n);
for(int i=1;i<=n;i++){
dp[i]=1;
for(int j=1;j<i;j++){
//下坐标序列的单调序列长度最大值
if(q[i].x>q[j].x){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
res=Math.max(res,dp[i]);
}
System.out.println(res);
}
}
//保存上下一对坐标
class PII implements Comparable<PII>{
int x,y;
public PII(int x,int y){
this.x=x;
this.y=y;
}
public int compareTo(PII o){
return Integer.compare(y,o.y);
}
}
拦截导弹
第一问计算拦截导弹数量
转化为求单调递减的子序列的最大长度
第二问计算系统组数
将导弹分为n个不单增的组,将每一组的最后一个元素作为分组依据,如果当前数大于当前组最后一个元素,则应该新开一个组,这样每一个组都能找到大于该组最后一个元素的数,将这些数连起来就形成单调递增序列,求这个序列的最大长度即是答案。
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
static int N=1010,n,res_first,res_second;
static int[]a=new int[N];
static int[]f=new int[N]; //单调序列长度最大值
static int[]g=new int[N]; //单调序列组数
public static void main(String[]args){
while(sc.hasNext()){
a[n++]=sc.nextInt();
}
for(int i=0;i<n;i++){
f[i]=1;
g[i]=1;
for(int j=0;j<i;j++){
if(a[i]<=a[j]){
f[i]=Math.max(f[i],f[j]+1);
}else{
g[i]=Math.max(g[i],g[j]+1);
}
}
}
for(int i=0;i<n;i++){
res_first=Math.max(res_first,f[i]);
res_second=Math.max(res_second,g[i]);
}
System.out.println(res_first);
System.out.println(res_second);
}
}
导弹防御系统
贪心的思想:
每次加入到一个末尾元素与当前数最接近的递增或递减序列(末尾元素小于h[u]且最大的或大于h[u]且最小的)
可以用两个数组up[i]和down[i]分别表示第i个递增序列和和递减序列的末尾元素,然后搜索时,每次按照以上原则尽可能加入到编号靠前的序列就是满足上述思想的方案。
dfs迭代:规定一个当前能搜索导弹拦截系统的最大个数max_cnt(即:答案cnt),从0开始,每次dfs一遍,如果当前cnt下无法拦截所有导弹,则cnt++(就是搜索的步长+1)再dfs,直到合法为止(这就是一个迭代加深的过程),则此时的cnt即为答案。
import java.util.*;
public class Main{
static Scanner sc=new Scanner(System.in);
/*
cnt:所需的导弹系统个数,通过枚举获得
h:导弹高度数组
up[i]第i组上升序列的末尾值
down[i]第i组下降序列的末尾值
*/
static int N=55,n;
static int []h=new int[N];
static int []up=new int[N];
static int []down=new int[N];
/*
u表示枚举到h数组的第u个数,
up_num表示上升序列的个数
down_num表示下降序列的个数
max_num表示通过迭代得到的当前的防御系统数量
*/
public static boolean dfs(int u,int up_num,int down_num,int max_num){
if(up_num+down_num>max_num) //当前所需的导弹系统数量大于当前数量max_num
return false; //无法拦截
if(u==n) //枚举u从1~n都没有return false
return true;
/*
将h[u]加入到末尾元素与h[u]最接近的上升序列
*/
boolean flag=false;
for(int i=1;i<=up_num;i++){
if(h[u]>up[i]){ //大于末尾的最小值
int temp=up[i];
up[i]=h[u];
if(dfs(u+1,up_num,down_num,max_num))
return true; //深搜成功返回true
up[i]=temp; //还原现场
flag=true;
break; //不用再查找了
}
}
/*
如果无法插入当前所有上升序列,则new一个新的
以h[u]开头的上升序列
*/
if(!flag){
up[up_num+1]=h[u];
if(dfs(u+1,up_num+1,down_num,max_num))
return true; //如果用上述方案可以达到最后要求,则return true
}
/*
将h[u]加入到末尾元素与h[u]最接近的下降序列
*/
flag=false;
for(int i=1;i<=down_num;i++){
if(h[u]<down[i]){ //小于末尾最大的数
int temp=down[i];
down[i]=h[u];
if(dfs(u+1,up_num,down_num,max_num))
return true; //深搜成功返回true
down[i]=temp; //还原现场
flag=true;
break; //不用再查找了
}
}
/*
如果无法插入当前所有下降序列,则new一个新的
以h[u]开头的下降序列
*/
if(!flag){
down[down_num+1]=h[u];
if(dfs(u+1,up_num,down_num+1,max_num))
return true; //如果用上述方案可以达到最后要求,则return true
}
return false;
}
public static void main(String []args){
while((n=sc.nextInt())!=0){
for(int i=0;i<n;i++){
h[i]=sc.nextInt();
}
int cnt=0;
while(!(dfs(0,0,0,cnt)))
cnt++;
System.out.println(cnt);
}
}
}
状态机模型
一次买卖的股票交易
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
//贪心:
//选择最小最左值和最大最右值,求差值
public int maxProfit(int[] prices) {
int low=Integer.MAX_VALUE,res=0;
for(int i=0;i<prices.length;i++){
low=Math.min(low,prices[i]);
res=Math.max(res,prices[i]-low);
}
return res;
}
/*
dp:
dp[0]表示第i天不持有股票的最大利润,dp[1]表示第
i天持有股票的最大利润
*/
public int maxProfit(int[]prices){
int[]dp=new int[2];
dp[1]=-prices[0];
dp[0]=0;
for(int i=1;i<=prices.length;i++){
//第i天持有:前一天也持有,前一天不持有
dp[1]=Math.max(dp[1],-prices[i-1]); //只能买卖一次,前一天不持有则买入后利润为负数
//第i天不持有
dp[0]=Math.max(dp[0],dp[1]+prices[i-1]);
}
return dp[0]; //第i天不持有的最大利润
}
多次买卖的股票交易
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
public int maxProfit(int[]prices){
int len=prices.length;
int[][]dp=new int[len+1][2];
/*
dp[i][0]:第i天不持有股票
dp[i][1]:第i天持有股票
*/
dp[0][1]=-prices[0];
dp[0][0]=0;
for(int i=1;i<prices.length;i++){
//持有
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]); //前一天不持有也可能有利润
//不持有
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
}
return dp[len-1][0];
}
含手续费的股票买卖
class Solution {
public int maxProfit(int[] prices, int fee) {
int len=prices.length;
int[][]dp=new int[len+1][2];
dp[0][1]=-prices[0];
dp[0][0]=0;
for(int i=1;i<len;i++){
//持有股票
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
//不持有股票,卖出时减去费用
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
}
return dp[len-1][0];
}
}
限制两次交易的股票买卖
class Solution {
public int maxProfit(int[] prices) {
int len=prices.length;
int [][]dp=new int[len+1][5];
/*
dp[i][j],0=<j<=4,表示第i天状态为j时持有的利润
j=0:没有操作
j=1:第一次持有股票
j=2:第一次卖出股票
j=3:第二次持有股票
j=4:第二次卖出股票
*/
dp[0][1]=-prices[0];
dp[0][2]=0;
dp[0][3]=-prices[0];
dp[0][4]=0;
for(int i=1;i<len;i++){
/*
dp[i][1]表示第i天第一次持有股票,来自两种状态转移,
延续前一天的持有状态,dp[i][1]=dp[i-1][1]
通过前一天买入所得,dp[i][1]=dp[i-1][0]+price[i]
dp[i][2],dp[i][3],dp[i][4]同理。
*/
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
}
return dp[len-1][4];
}
}
限制k次交易数的股票买卖
二维数组实现
class Solution {
public int maxProfit(int k, int[] prices) {
int len=prices.length;
int[][]dp=new int[len+1][2*k+1];
/*
dp[i][j],0=<j<=2*k,表示第i天状态为j时持有的利润
j=0:没有操作
j=1:第一次持有股票
j=2:第一次卖出股票
j=3:第二次持有股票
j=4:第二次卖出股票
.......
j是奇数时,就是持有股票的状态
j是偶数时,就是卖出股票的状态
*/
//奇数状态初始化为-price[0]
for(int i=1;i<2*k;i+=2){
dp[0][i]=-prices[0];
}
for(int i=1;i<len;i++){
for(int j=0;j<2*k-1;j+=2){
//奇数时持有
dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]-prices[i]);
//偶数时卖出
dp[i][j+2]=Math.max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
}
}
return dp[len-1][2*k];
}
}
三维数组实现
import java.util.*;
import java.io.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
int N=(int)1e5,M=110,res=0,INF=0x3f3f3f3f;
int[]w=new int[N];
int[][][]dp=new int[N][M][2];
/*
dp[i][j][0]所有走到了第i天,然后完成交易次数是j的,手中无货
dp[i][j][1]所有走到了第i天,然后当前交易到了或者说正在进行第j次的交易,手中有货
*/
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
String[] st = bf.readLine().split(" ");
int n = Integer.parseInt(st[0]);
int k = Integer.parseInt(st[1]);
String[] str = bf.readLine().split(" ");
for(int i = 1 ; i <= n ; i ++ ) w[i] = Integer.parseInt(str[i - 1]);
for(int i=0;i<=n;i++)
for(int j=0;j<=k;j ++)
Arrays.fill(dp[i][j],-INF);
//交易次数为零(j=0)时没有利润
for(int i=1;i<=n;i++) dp[i][0][0] = 0;
for(int i=1;i<=n;i++){
for(int j=1;j<=k;j++){
//状态机
//手中无货的情况可以从前一天的也是手中无货转移过来,也可以从前一天有货转移过来,所以两者取一个max
//如果是前一天有货,那么今天没货,就是卖掉了,所以收益增加w[i]
dp[i][j][0] = Math.max(dp[i - 1][j][0],dp[i - 1][j][1] + w[i]);
//手中有货的情况可以从前一天的也是手中有货转移过来,也可以从前一天手中没货转移过来,两者取一个max
//前一天没有货转移过来的话f[i - 1][j - 1][0] - w[i],因为前一天没有货,今天有货了,说明就是今天买票了,
//所以需要花钱买,然后这一次买了就会花费一次交易操作,所以前一天的交易次数比今天小1,
//所以就是写成f[i - 1][j - 1]
dp[i][j][1] = Math.max(dp[i - 1][j][1],dp[i - 1][j - 1][0] - w[i]);
}
}
for(int j=0;j<=k;j++){
res=Math.max(res,dp[n][j][0]);
}
System.out.println(res);
}
}
含冷冻期的股票买卖
class Solution {
public int maxProfit(int[] prices) {
/* dp[i][j]:第i天状态为j的利润
划分为4个状态
1.j=0,表示第i天持有股票
1.1前一天就持有,保持持有状态dp[i-1][0]
1.2前一天是卖出状态,dp[i-1][1]-prices[i]
1.3前一天是冷冻期,dp[i-1][3]-prices[i]
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i],dp[i-1][3]-prices[i]);
2.j=1,表示第i天是卖出状态(并非今天卖出),要么保持卖出状态,dp[i-1][1],
2.1前一天是冷冻期,dp[i-1][3]
2.2前一天也是卖出状态,保持卖出状态,dp[i-1][1]
dp[i][1]=max(dp[i-1][3],dp[i-1][1]);
3.j=2,表示第i天卖出股票
(因为冷冻期的前一天只能是卖出股票,如果只说前一天是不持有状态,
那么不一定是前一天卖出的,因此要单独把今天卖出股票的情况单独拎出来)
前一天持有,今天卖出dp[i-1][0]+prices[i]
4.j=3,表示第i天是冷冻期
冷冻期的前一天一定是卖出股票
dp[i-1][2]
*/
int len=prices.length;
if(len==0)return 0;
int[][]dp=new int[len+1][4];
//第0天持有股票
dp[0][0]-=prices[0];
for(int i=1;i<len;i++){
dp[i][0]=find_max(dp[i-1][0],dp[i-1][1]-prices[i],dp[i-1][3]-prices[i]);
dp[i][1]=Math.max(dp[i-1][3],dp[i-1][1]);
dp[i][2]=dp[i-1][0]+prices[i];
dp[i][3]=dp[i-1][2];
}
return find_max(dp[len-1][3],dp[len-1][1],dp[len-1][2]);
}
public int find_max(int a,int b,int c){
int max=a;
if(b>max)max=b;
if(c>max)max=c;
return max;
}
}
import java.util.*;
import java.io.*;
public class Main{
public static void main(String[]args)throws Exception{
Scanner sc=new Scanner(System.in);
int N=100010,res=0,INF=0x3f3f3f3f;
/* dp[i][j]考虑到第i天,目前手持股票状态为j的利润 */
int[][]dp=new int[N][3];
int[]w=new int[N];
int n=sc.nextInt();
for(int i=1;i<=n;i++){
w[i]=sc.nextInt();
}
dp[0][0]=0;
dp[0][1]=-INF;
dp[0][2]=-INF;
for(int i=1;i<=n;i++){
//如果第 i 天是 空仓 (j=0) 状态,则 i-1 天可能是 空仓 (j=0) 或 冷冻期 (j=2) 的状态
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]);
//如果第 i 天是 持仓 (j=1) 状态,则 i-1 天可能是 持仓 (j=1) 状态 或 空仓 (j=0) 的状态 (买入)
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-w[i]);
//如果第 i 天是 冷冻期 (j=2) 状态,则 i-1 天只可能是 持仓 (j=1) 状态,在第 i 天选择了 卖出
dp[i][2]=dp[i-1][1]+w[i];
}
res=Math.max(dp[n][0],dp[n][2]);
System.out.println(res);
}
}
打家劫舍
dp[i]表示抢劫前i家,可以偷窃的最高金额数。
分两种情况:
1.抢劫第i家,则dp[i]=dp[i-2]+nums[i]
2.不抢劫第i家,则dp[i]=dp[i-1]
取一个max
class Solution {
public int rob(int[] nums) {
if(nums.length==0) return 0;
if(nums.length==1)return nums[0];
int len=nums.length;
int[]dp=new int[len+1];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<=len-1;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[len-1];
}
}
// f[i]表示前 i 个店铺的最大收益
// 不抢第 i个店铺时的最大收益为f[i−1]
// 由于不能抢相邻的,所以抢第 i 个店铺时一定不能抢第i−1个店铺
// 抢第 i 个店铺时的最大收益位f[i−2]+w[i]
// 当i>1时,f[i]=max(f[i−1],f[i−2]+w[i])
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int N=(int)1e5,w=0;
int []dp=new int[N];
int T=sc.nextInt();
while(T-->0){
int n=sc.nextInt();
w=sc.nextInt();
dp[1]=w;
for(int i=2;i<=n;i++){
w=sc.nextInt();
dp[i]=Math.max(dp[i-1],dp[i-2]+w);
}
System.out.println(dp[n]);
}
}
}
打家劫舍–首尾成环
第一家和最后一家连成环状,因此分两种情况考虑:
1.考虑头不考虑尾
robRange(nums,0,len-2)
2.考虑尾不考虑头
robRanger(nums,1,len-1)
取两者的最大值
class Solution {
public int rob(int[] nums) {
int len=nums.length;
if(len==0) return 0;
if(len==1)return nums[0];
//考虑头不考虑尾
int res1=robRange(nums,0,len-2);
//考虑尾不考虑头
int res2=robRange(nums,1,len-1);
//返回两种情况的最大值
return Math.max(res1,res2);
}
public int robRange(int[]nums,int start,int end){
if(start==end)return nums[start];
int len=nums.length;
int[]dp=new int[len];
dp[start]=nums[start];
dp[start+1]=Math.max(nums[start],nums[start+1]);
for(int i=start+2;i<=end;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
}
打家劫舍 --树
每个树结点有偷和不偷两种选择,如果偷当前结点,则不能偷其左右儿子,如果不偷当前结点,则选择性地偷左右儿子保证总金额最大。
dp状态表示:
在dp[]中使用两种状态表示偷和不偷
dp[0]表示不偷当前结点的最大金额,dp[1]表示偷当前结点的最大金额,
状态转移:
如果偷当前结点,则不能偷左右儿子
res=cur.val+left[0]+right[0]
如果不偷当前结点,则选择偷左右儿子金额数最大的偷法,
res=max(left[0]+left[1])+max(right[0]+right[1])
class Solution {
public int rob(TreeNode root) {
int []res=new int[2];
res=robAction(root);
return Math.max(res[0],res[1]);
}
public int[] robAction(TreeNode root){
int[]res=new int[2];
if(root==null)
return res;
int []left=robAction(root.left);
int []right=robAction(root.right);
//不偷cur
res[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
//偷cur
res[1]=root.val+left[0]+right[0];
return res;
}
}
编辑距离
判断子序列
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1
if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];
class Solution {
public boolean isSubsequence(String s, String t) {
int lens=s.length();
int lent=t.length();
int[][]dp=new int[lens+1][lent+1];
for(int i=1;i<=lens;i++){
for(int j=1;j<=lent;j++){
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=dp[i][j-1];
}
}
}
return dp[lens][lent]==lens;
}
}
不同的子序列
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。
一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]。
一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]
class Solution {
public int numDistinct(String s, String t) {
int lens=s.length();
int lent=t.length();
int[][]dp=new int[lens+1][lent+1];
//以下标i-1结尾的s中删除后出现为空字符的个数
for(int i=0;i<lens;i++)dp[i][0]=1;
for(int i=1;i<=lens;i++){
for(int j=1;j<=lent;j++){
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[lens][lent];
}
}
字符串删除操作
dp[i][j]表示以i-1结尾的word1和以j-1结尾的word2,通过删除达到相等的最少操作数
当word1[i-1]=word2[j-1]时,dp[i][j]=dp[i-1][j-1],不需要删除
当word1[i-1]!=word2[j-1]时,删除word1,dp[i-1][j]+1; 删除word2 ,dp[i][j-1]+1;
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
int[][]dp=new int[len1+1][len2+1];
for(int i=0;i<=len1;i++)dp[i][0]=i;
for(int j=0;j<=len2;j++)dp[0][j]=j;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;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]+1,dp[i][j-1]+1);
}
}
}
return dp[len1][len2];
}
}
编辑距离
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
if (word1[i - 1] != word2[j - 1]),
- 删除word1[i-1],则dp[i-1][j]+1;
- 删除word2[j-1],则dp[i][j-1]+1;
- word2添加一个元素,相当于word1删除一个元素
- 替换,dp[i-1][j-1]+1
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
int[][]dp=new int[len1+1][len2+1];
for(int i=0;i<=len1;i++)dp[i][0]=i;
for(int j=0;j<=len2;j++)dp[0][j]=j;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=find_min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1;
}
}
}
return dp[len1][len2];
}
public int find_min(int a,int b,int c){
int min=a;
if(b<min)min=b;
if(c<min)min=c;
return min;
}
}
回文子串
dp[i][j]表示[i,j]是回文串
- 如果s[i]=s[j]且i=j或者j-i=1,那么dp[i][j]=true;
- 如果s[i]=s[j]且j-i>1,则当[i+1,j-1]是回文串时,dp[i][j]=true;
因为dp[i][j]由dp[i+1][j-1]推出,则遍历顺序是先求出dp[i+1][j-1]
class Solution {
public int countSubstrings(String s) {
int res=0;
int len=s.length();
boolean[][]dp=new boolean[len+1][len+1];
for(int i=len-1;i>=0;i--){
for(int j=i;j<=len-1;j++){
if(s.charAt(i)==s.charAt(j)&&j-i<=1){
res++;
dp[i][j]=true;
}
if(s.charAt(i)==s.charAt(j)&&j-i>1&&dp[i+1][j-1]){
res++;
dp[i][j]=true;
}
}
}
return res;
}
}
最长回文子序列
dp[i][j]表示s[i,j]中回文序列的长度
- s[i]==s[j],则dp[i][j]=dp[i+1][j-1]+2
- s[i]!=s[j],则要么舍去s[i],要么舍去s[j],取max,dp[i][j]=max(dp[i+1][j],dp[i][j-1])
class Solution {
public int longestPalindromeSubseq(String s) {
int len=s.length();
int[][]dp=new int[len+1][len+1];
for(int i=0;i<len;i++){
dp[i][i]=1;
}
for(int i=len-1;i>=0;i--){
for(int j=i+1;j<len;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][len-1];
}
}