蓝桥云课Java B组----第2篇----动态规划

系列专栏:蓝桥杯Java B组

个人主页:个人主页

目录

阅读提示

一、动态规划基础

        (一)线性dp

        (二)二维dp

        (三)LIS最长上升子序列

        (四)LCS最长公共子序列

二、背包问题

        (一)01背包

        (二)完全背包    

        (三)多重背包    

        (四)单调队列优化多重背包

        (五)二维01背包&分组背包

三、树形DP

        (一)自上而下树形dp

        问题一

        问题二       

        (二)自下而上树形dp

        1.最大独立集

        2.最小点覆盖

        3.最小支配集

        (三)路径相关树形dp

        (四)换根dp

        四、区间DP

        (一)普通区间dp

        (二)环形区间dp

五、状压DP

六、数位DP

七、期望DP


阅读提示

        本文面向已掌握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种方案。推出状态转移方程为:dp[i][j]=\sum_{k=0}^{a[i]}dp[i-1]dp[j-k](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]);
	}
}

优化:(二维 \rightarrow 一维)
        首先有dp[i][j]=dp[i-1][j],相当于将dp[i-1]复制给dp[i],然后dp[i][j]=max(dp[i][j]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值