系列专栏:蓝桥杯Java B组
个人主页:个人主页
目录
阅读提示
本文面向已掌握Java基础语法的开发者,初学java者建议先夯实基础再阅读。再者,文章中所有的例题都在蓝桥官方题库网站 题库 - 蓝桥云课 里能查到,本文只提供编号,读者需输入例题编号自行查看题目,在学习例题过程当中,代码中的注释会给您提供帮助。
一、动态规划基础
动态规划(Dynamic Programming,简称DP)是一种将原问题分解为相对简单的子问题来求解的算法。适用于具有重叠子问题和最优子结构性质的问题。通过保存子问题的解,避免重复计算,提升效率
步骤:
1.划分子问题:将原问题分解为若干子问题
2.状态表示:用dp[i]表示当前状态
3.状态转移:确定状态之间的关系,如dp[i] = dp[i-1]表示当前状态由上一个状态转移而来
4.确定边界:明确初始状态和边界条件,确保状态转移的正确性
(一)线性dp
线性动态规划是动态规划中的一类问题,其特点是状态之间存在线性关系。递推方程通常呈现明显的线性关系,可能是一维线性(如dp[i])或二维线性(如dp[i][j])
求解方法:
1.按照线性顺序逐步求解,例如一行一行或一列一列地计算状态值
2.确保每个状态依赖于已计算的前驱状态
例题3297 取钱
import java.util.*;
public class 线性dp {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//用dp[i]表示用户取金额i时 黄应该给她至少多少张钞票
int dp[] = new int[(int) 1e5 + 1]; //根据题目界限要求 设置最大界限
Arrays.fill(dp, (int) 1e9); //先初始化一个很大的数
dp[0] = 0; //当取金额为0时 钞票数为0张
int arr[] = { 1, 4, 5, 10, 20 }; //arr存储银行钞票面额
for (int i = 1; i < dp.length; i++) {
for (int j : arr) {
if (i >= j) { //若用户取的金额>=钞票面值
//找金额数-钞票面值的最少钞票数dp[i - j],
//将此+1再与本身dp[i]取最小 赋值给dp[i]
dp[i] = Math.min(dp[i - j] + 1, dp[i]);
}
}
}
while (scan.hasNextInt()) { //若有输入 进入循环
int n = scan.nextInt();
System.out.println(dp[n]); //打印对应金额下的最少钞票数
}
scan.close();
}
}
例题3367 破碎的楼梯
import java.util.*;
public class 线性dp {
static long mod = (long) 1e9 + 7;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
int arr[] = new int[m];
for (int i = 0; i < arr.length; i++) {
arr[i] = scan.nextInt();
}
scan.close();
//dp[i]表示上到第i级台阶的方案数
long dp[] = new long[(int) 1e5 + 1]; //根据题目界限设置dp大小
Arrays.fill(dp, -1); //初始化为-1
dp[0] = 1; //没有上0级这一说 为了方便计算dp[2] 把它初始化为1
dp[1] = 1; //上到1级台阶的方案数为1
for (int j : arr) {
dp[j] = 0; //把坏了的台阶的dp值都设为0 表示没有方法到达
}
for (int i = 2; i < dp.length; i++) { //从第2级台阶开始遍历
if (dp[i] == 0) //如果台阶坏了就跳过
continue;
//线性表示: 到达i级台阶的方案数=少迈1级的方案数+少迈2级的方案数
long tmp = dp[i - 1] + dp[i - 2];
dp[i] = tmp % mod; //取模
}
System.out.println(dp[n]);
}
}
(二)二维dp
二维动态规划是线性动态规划的扩展,用于解决状态具有两个维度的问题,通常用dp[i][j]表示状态。适用于平面上的规划问题,例如棋盘、矩阵或路径问题
特点:
1.在二维平面上进行状态转移,递推关系可能涉及行、列或其他平面方向上的依赖
2.求解时需按照一定的顺序计算状态值,确保依赖的前驱状态已计算完成
例题:
对于n*m矩阵,每个点只能往下或往右
计算从(0,0)点走到(n-1,m-1)点一共有多少路径
思路:我们定义dp数组dp[i][j]为到达第i行第j列的路径数,因为只能向下和向右所以推出状态转移方程为 dp[i][j]=dp[i-1][j]+dp[i][j-1],dp数组初始状态为dp[0][0]=1,表示起点有一种方案
import java.util.*;
public class 二维dp {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
scan.close();
// 二维dp[i][j]数组存储到(i,j)有多少种方案
int dp[][] = new int[n][m];
dp[0][0] = 1; // 初始化为1 起点有1种方案
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (i == 0 && j == 0) // 跳过(0,0)点
continue;
if (i > 0) //界限判定 避免i-1<0 表示可以从上面转移过来
dp[i][j] += dp[i - 1][j];
if (j > 0) //界限判定 避免j-1<0 表示可以从左面转移过来
dp[i][j] += dp[i][j - 1];
}
}
System.out.println(dp[n-1][m-1]);
}
}
例题525 传球游戏
思路:定义二维数组,dp[i][j]表示第i次传球到j号同学的方案数,因为球可以从左边和右边传来,所以状态转移方程为:dp[i][j]=dp[i-1][(j+1)%n]+dp[i-1][(j-1+n)%n] (注:%n的操作是因为同学围成的是一个圆 最后一个同学的右面是1号),定义初始状态:dp[0][0]=1 从0号开始传,最后打印 dp[m][0] 表示用m次传回0号
import java.util.*;
public class 二维dp {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
scan.close();
//dp[i][j]存储第i次转球到j号同学的方案数
int dp[][]=new int[m+1][n];
dp[0][0]=1; //初始化为1 从0号开始传
for(int i=1;i<=m;i++) { //第1次--第m次
for(int j=0;j<n;j++) {
dp[i][j]=dp[i-1][(j+1)%n]+dp[i-1][(j-1+n)%n]; //状态转移方程
}
}
System.out.println(dp[m][0]); //输出从0号开始 通过m次传回0号的方法数
}
}
例题389 摆花
思路:定义二维dp数组,dp[i][j]表示前i种花摆了j盆的方案数。初始化 dp[i][0]=1,表示前i种花摆了0盆为1种方案。推出状态转移方程为:(k表示第i种花摆了k盆)
import java.util.*;
public class 二维dp {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
int a[] = new int[n + 1];// 存储第i种花不能超过a[i]盆
for (int i = 1; i <= n; i++) { //从第1种开始
a[i] = scan.nextInt();
}
scan.close();
long mod = (long) 1e6 + 7;
long dp[][] = new long[n + 1][m + 1];// dp[i][j]表示前i种花摆了j盆的方案数
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;// 初始化前i种花摆了0盆为1种方案
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 求和
for (int k = 0; k <= a[i] && k <= j; k++) {// k<=j因为界限判定
dp[i][j] += dp[i - 1][j - k];
dp[i][j] %= mod;// 取模
}
}
}
System.out.println(dp[n][m]);
}
}
(三)LIS最长上升子序列
给定一个序列 bi ,若满足 b1 < b2 < ... < bs ,则称该序列为上升序列。 例如,序列 {1,7,3,5,9,4,8} 的一些上升子序列为 {1,7,9}、{3,4,8}、{1,3,5,8} 等。其中最长的子序列(如 {1,3,5,8}) 长度为 4,因此该序列的最长上升子序列长度为 4
补充:
1.子序列是指数组中不一定连续但先后顺序一致的n个数
2.这是一个经典的动态规划问题,通过状态转移逐步求解最长上升子序列的长度
模板:
1.定义状态:dp[i] 表示以索引 i 结尾的最长上升子序列的长度
2.状态转移:
if(f[i]>f[j]) dp[i]=Math.max(dp[j]+1,dp[i]); (注:0<=j<i)
else dp[i]=Math.max(1,dp[i]);
3.时间复杂度:O(n^2)
例题2049 蓝桥勇士
思路:本题是个LIS模板题,我们定义dp[i]为以i结尾可以挑战的最多对手
import java.util.*;
public class LIS {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
long a[] = new long[n];
for (int i = 0; i < n; i++) {
a[i] = scan.nextLong();
}
scan.close();
int dp[] = new int[n]; //dp[i]为以i结尾可以挑战的最多对手
dp[0] = 1; //初始化以索引为0的最长子序列为1
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) { //遍历索引i之前的数
if (a[i] > a[j])
dp[i] = Math.max(dp[j] + 1, dp[i]); //模板
else
dp[i] = Math.max(1, dp[i]);
}
}
int max = 0;
for(int i=0;i<n;i++) {
max=Math.max(max, dp[i]); //取dp里面最大的数 即数组a的最长上升子序列
}
System.out.println(max);
}
}
(四)LCS最长公共子序列
给定两个序列 A 和 B,求它们的最长公共子序列(LCS)。子序列是指从原序列中删除若干元素后得到的序列,不要求连续但顺序必须一致
特点:
1.通过二维动态规划逐步求解两个序列的最长公共子序列
2.适用于字符串匹配、序列比对等场景
模板:
1.定义状态:dp[i][j]表示A[1~i]序列和B[1~j]序列中的最长公共子序列的长度
2.状态转移:
if(a[i]==b[j]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
3.时间复杂度:O(n^2)
例题1189 最长公共子序列
import java.util.*;
public class LCS {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
long a[] = new long[n];
long b[] = new long[m];
for (int i = 0; i < n; i++) {
a[i] = scan.nextLong();
}
for (int i = 0; i < m; i++) {
b[i] = scan.nextLong();
}
scan.close();
//dp[i][j]表示A[1~i]序列和B[1~j]序列中的最长公共子序列的长度
int dp[][] = new int[n + 1][m + 1];
int max = 0; //max存储最大值
for (int i = 1; i <= n; i++) { //遍历A i从1开始使i-1>0 边界判定
for (int j = 1; j <= m; j++) { //遍历B
if (a[i - 1] == b[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]);
max = Math.max(max, dp[i][j]); //取dp中的最大值
}
}
System.out.println(max);
}
}
二、背包问题
(一)01背包
有一个容量为 V 的背包和 n 个物品,每个物品有一个价值 v 和体积 w 。每个物品只能选择拿或不拿一次,求背包能装下的最大价值
特点:
1.每个物品只有“拿”或“不拿”两种状态
2.通过状态转移逐步求解最大价值,时间复杂度为 O(n*V)
模板:
1.定义状态:dp[i][j] 表示到第 i 个物品为止,拿的物品总体积为 j 的情况下的最大价值
2.状态转移:
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w]+v);
3.解释:
若不拿物品i,那么最大价值就是dp[i-1][j]
若拿了就是从体积j-w转移过来,价值增加v
4.最终结果:输出 dp[n][V]
例题1174 小明的背包1
import java.util.*;
public class _01背包 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int V = scan.nextInt();
int w[] = new int[n]; //存储索引为i重量是w[i]
int v[] = new int[n]; //存储索引为i价值是v[i]
for (int i = 0; i < n; i++) {
w[i] = scan.nextInt();
v[i] = scan.nextInt();
}
scan.close();
//dp[i][j]表示到第i个物品为止,拿的物品总体积为j的情况下的最大价值
int dp[][] = new int[n + 1][V + 1];
for (int i = 1; i <= n; i++) { //从第1个物品开始
for (int j = 0; j <= V; j++) {
if (j >= w[i-1]) //边界判定 要保证j-w[i-1]>0 表示拿了物品
dp[i][j] = Math.max(dp[i - 1][j - w[i-1]] + v[i-1], dp[i][j]);
// 没拿物品
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j]);
}
}
System.out.println(dp[n][V]);
}
}
优化:(二维 一维)
首先有dp[i][j]=dp[i-1][j],相当于将dp[i-1]复制给dp[i],然后dp[i][j]=max(dp[i][j]