⭐️前言⭐️
本篇文章是由暴力递归到动态规划篇章的第二篇。
🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁
🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言
🍉博客中涉及源码及博主日常练习代码均已上传GitHub
📍内容导读📍
- 🍅背包问题
- 🍅字符串转化
- 🍅最长公共子序列(样本对应模型)
- 🍅最长回文子序列(范围尝试模型)
- 🍅跳马问题
🍅背包问题
题目:
背包问题
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值
给定一个正数bag,表示一个载重bag的袋子,装的物品不能超过这个重量
返回能装下的最大价值
题解思路1:
每个物品有选和不选两种情况,在背包容量充足的情况下,返回两种情况中的最大值
递归函数返回符合条件的情况下,所能获得的最大价值。
代码实现:
public class Knapsack {
public static int maxValue(int[] w,int[] v,int bag) {
if(w==null||v==null||w.length!=v.length||w.length==0||v.length==0) {
return 0;
}
return process(w,v,0,bag);
}
// index:第index个物品
// rest:背包的剩余容量
public static int process(int[] w,int[] v,int index,int rest) {
if(rest<0) {
return -1;
}
if(index==w.length) {
return 0;
}
int p1=process(w,v,index+1,rest);
int p2=0;
int next=process(w,v,index+1,rest-w[index]);
if(next!=-1) {
p2=v[index]+next;
}
return Math.max(p1,p2);
}
}
题解思路2:(动态规划)
有状态相同的情况,所以可以通过缓存表来减少重复计算。
假设数组长度为4,背包容量为10,最后想要获得的结果就是dp[0][bag]位置的结果。
根据暴力递归的解法,来推断出dp表中填写的规则,最后返回所要位置的结果,
代码实现:
public class Knapsack {
public static int dp(int[] w,int[] v,int bag) {
if(w==null||v==null||w.length!=v.length||w.length==0||v.length==0) {
return 0;
}
int N=w.length;
int[][] dp=new int[N+1][bag+1];
for (int index = N-1; index >=0 ; index--) {
for (int rest = 0; rest <=bag ; rest++) {
int p1=dp[index+1][rest];
int p2=0;
int next=rest-w[index]<0?-1:dp[index+1][rest-w[index]];
if(next!=-1) {
p2=v[index]+next;
}
dp[index][rest]=Math.max(p1,p2);
}
}
return dp[0][bag];
}
}
🍅字符串转化
题目:
规定1和A对应、2和B对应、3和C对应…26和Z对应
那么一个数字字符串比如"111”就可以转化为:
“AAA”、“KA"和"AK”
给定一个只有数字字符组成的字符串str,返回有多少种转化结果
题解思路1:
从字符串的第i个字符来考虑转化情况,第一种是第i个字符单独转化,那么转化结果数即为从i+1位置考虑转化的结果数;第二种是第i个字符和第i+1个字符可以一起转化,那么转化结果数即为从i+2位置考虑转化的结果数。
代码实现:
public class CoverToLetterString {
// str只含有数字字符0~9
// 返回多少种转化方案
public static int number(String str) {
if(str==null||str.length()==0) {
return 0;
}
return process(str.toCharArray(),0);
}
// str[0..i-1] 转化无需过问
// str[i...]去转化,返回有多少种转化方法
public static int process(char[] str, int i) {
if(i==str.length) { // 如果到最后,就是一种转化方法
return 1;
}
// i没到最后,说明有字符
if(str[i]=='0') { // 之前的决定有问题
return 0;
}
// str[i]!='0'
// 可能性1,i单独转化
int ways=process(str,i+1);
// 可能性2,i和i+1一同转化
if(i+1<str.length&&(str[i]-'0')*10+str[i+1]-'0'<27) {
ways+=process(str,i+2);
}
return ways;
}
}
题解思路2:
根据上边的暴力递归解法,可以改写出从右往左的动态规划版本,dp[i]表示str[i…]有多少种转化方式
代码实现:
public class CoverToLetterString {
public static int dp(String s) {
if(s==null||s.length()==0) {
return 0;
}
char[] str=s.toCharArray();
int N= str.length;
int[] dp=new int[N+1];
dp[N]=1;
for (int i = N-1; i >=0 ; i--) {
if(str[i]!='0') {
int ways=dp[i+1];
if(i+1<N&&(str[i]-'0')*10+str[i+1]-'0'<27) {
ways+=dp[i+2];
}
dp[i]=ways;
}
}
return dp[0];
}
}
🍅最长公共子序列(样本对应模型)
题目:https://leetcode.cn/problems/longest-common-subsequence/
给定两个字符串str1和str2,
返回这两个字符串的最长公共子序列长度
比如 : str1 = “a12b3c456d”,str2 = “1ef23ghi4j56k”
最长公共子序列是“123456”,所以返回长度6
模型解题:
该模型通常考虑两个样本的结尾边界情况
题解思路1:
考虑str1[0…i]和str2[0…j],这个范围上的最长公共子序列长度:
当str1以i=0结尾的时候,判断i与j位置的字符是否相同,相同返回1;不相同递归(i,j-1);
当str2以j=0的时候,判断i与j位置的字符是否相同,相同返回1;不相同递归(i-1,j);
当i、j都不为0时,
1:str1不考虑以i结尾;2:str2不考虑以j结尾;3:既考虑以i结尾,又考虑以j结尾。
代码实现:
public class LongestCommonSubsequence {
public static int longestCommonSubsequence(String s1,String s2) {
if(s1==null||s2==null||s1.length()==0||s2.length()==0) {
return 0;
}
char[] str1=s1.toCharArray();
char[] str2=s2.toCharArray();
return process(str1,str2,str1.length-1,str2.length-1);
}
// 考虑str1[0...i]和str2[0...j],这个范围上的最长公共子序列长度
public static int process(char[] str1,char[] str2,int i,int j) {
if(i==0&&j==0) { // 都只剩一个字符
return str1[i]==str2[j]?1:0;
}else if (i==0) {
if(str1[i]==str2[j]) {
return 1;
}else {
return process(str1,str2,i,j-1);
}
}else if(j==0) {
if (str1[i]==str2[j]) {
return 1;
}else {
return process(str1,str2,i-1,j);
}
}else { // i!=0&&j!=0
int p1=process(str1,str2,i-1,j);
int p2=process(str1,str2,i,j-1);
int p3=str1[i]==str2[j]?(1+process(str1,str2,i-1,j-1)):0;
return Math.max(p1,Math.max(p2,p3));
}
}
}
题解思路2:
根据暴力递归来改写动态规划,用一张二维表来记录i、j位置的最长公共子序列,返回表的右下角的结果。
代码实现:
public class LongestCommonSubsequence {
public static int longestCommonSubsequence(String s1,String s2) {
if(s1==null||s2==null||s1.length()==0||s2.length()==0) {
return 0;
}
char[] str1=s1.toCharArray();
char[] str2=s2.toCharArray();
int N=str1.length;
int M=str2.length;
int[][] dp=new int[N][M];
dp[0][0]=str1[0]==str2[0]?1:0;
for (int j = 1; j <M ; j++) {
dp[0][j]=str1[0]==str2[j]?1:dp[0][j-1];
}
for (int i = 1; i <N ; i++) {
dp[i][0]=str1[i]==str2[0]?1:dp[i-1][0];
}
for (int i = 1; i <N ; i++) {
for (int j = 1; j <M; j++) {
int p1=dp[i-1][j];
int p2=dp[i][j-1];
int p3=str1[i]==str2[j]?(1+dp[i-1][j-1]):0;
dp[i][j]=Math.max(p1,Math.max(p2,p3));
}
}
return dp[N-1][M-1];
}
}
🍅最长回文子序列(范围尝试模型)
题目:https://leetcode.cn/problems/longest-palindromic-subsequence/description/
给定一个字符串str,返回这个字符串的最长回文子序列长度
比如 : str = “a12b3c43def2ghi1kpm”
最长回文子序列是“1234321”或者“123c321”,返回长度7
模型解题:
该模型通常考虑样本的开头和结尾的判定情况
题解思路1:
将字符串逆序,与原字符串求最长公共子序列,得到的结果即为最长回文子序列。
题解思路2:
考虑str[L…R]范围内的最长回文子序列长度,穷举所有可能性,返回最大的结果:
1、最长回文子序列既不以L开头,也不以R结尾
2、最长回文子序列以L开头,不以R结尾
3、最长回文子序列不以L开头,以R结尾
4、最长回文子序列以L开头,R结尾
代码实现:
public class PalindromeSubsequence {
public static int lpsl1(String s) {
if(s==null||s.length()==0) {
return 0;
}
char[] str=s.toCharArray();
return f(str,0,str.length-1);
}
// str[L...R]最长回文子序列长度返回
public static int f(char[] str,int L,int R) {
if(L==R) {
return 1;
}
if(L==R-1) {
return str[L]==str[R]?2:1;
}
int p1=f(str,L+1,R-1);
int p2=f(str,L,R-1);
int p3=f(str,L+1,R);
int p4=str[L]!=str[R]?0:(2+f(str,L+1,R-1));
return Math.max(Math.max(p1,p2),Math.max(p3,p4));
}
}
题解思路3:
有两个可变参数,可以
构建出dp表,来存储每个范围的最长回文子序列长度,
可以先根据base case,来完成对角线和紧挨对角线两条斜线的初始化,然后再根据依赖关系,完成剩余位置的填写,最后返回标记位置的结果即可。
代码实现:
public class PalindromeSubsequence {
public static int longestPalindromeSubsequence(String s) {
if(s==null||s.length()==0) {
return 0;
}
if (s.length()==1) {
return 1;
}
char[] str=s.toCharArray();
int N=str.length;
int[][] dp=new int[N][N];
dp[N-1][N-1]=1;
// 如果L==R,dp值为1;如果str[L]==str[R-1]dp值为2,否则为1
for (int i = 0; i < N-1; i++) {
dp[i][i]=1;
dp[i][i+1]=str[i]==str[i+1]?2:1;
}
// 其余位置的dp值,依赖于左、左下、下三个位置的dp值
for (int i=N-3;i>=0;i--) {
for (int j = i+2; j < N; j++) {
dp[i][j]=Math.max(dp[i][j-1],dp[i+1][j]);
if(str[i]==str[j]) {
dp[i][j]=Math.max(dp[i][j],2+dp[i+1][j-1]);
}
}
}
return dp[0][N-1];
}
}
🍅跳马问题
题目:
请同学们自行搜索或者想象一个象棋的棋盘,
然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置
那么整个棋盘就是横坐标上9条线、纵坐标上10条线的区域
给你三个 参数 x,y,k
返回“马”从(0,0)位置出发,必须走k步
最后落在(x,y)上的方法数有多少种?
题解思路1:
如果马在棋盘的中间位置,它一步可以到达八个不同的位置,设置base case出界返回0,如果剩0步就判断是不是到达了指定位置,这样去累加八个位置的可能性,最后返回的即为结果。
代码实现:
public class HorseJump {
// 当前来到的位置是(x,y)
// 还剩rest步需要跳
// 跳完rest步,正好跳到a,b的方法数是多少
public static int jump(int a,int b,int k) {
return process(0,0,k,a,b);
}
public static int process(int x,int y,int rest,int a,int b) {
if(x<0||x>9||y<0||y>8) {
return 0;
}
if(rest==0) {
return (x==a&&y==b)?1:0;
}
int ways = process(x + 2, y + 1, rest - 1, a, b);
ways += process(x + 1, y + 2, rest - 1, a, b);
ways += process(x - 1, y + 2, rest - 1, a, b);
ways += process(x - 2, y + 1, rest - 1, a, b);
ways += process(x - 2, y - 1, rest - 1, a, b);
ways += process(x - 1, y - 2, rest - 1, a, b);
ways += process(x + 1, y - 2, rest - 1, a, b);
ways += process(x + 2, y - 1, rest - 1, a, b);
return ways;
}
}
题解思路2:
在递归中有x,y,rest三个可变参数,所以可以根据依赖关系来构建一个三维表,存储不同位置的结果数,最后返回dp(0,0,k)即为所求。
依赖关系都是rest-1的,所以可以一层一层的填充。
代码实现:
public class HorseJump {
public static int dp(int a,int b,int k) {
int[][][] dp=new int[10][9][k+1];
dp[a][b][0]=1;
for (int rest = 1; rest <=k; rest++) {
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 9; y++) {
int ways = pick(dp, x + 2, y + 1, rest - 1);
ways += pick(dp, x + 1, y + 2, rest - 1);
ways += pick(dp, x - 1, y + 2, rest - 1);
ways += pick(dp, x - 2, y + 1, rest - 1);
ways += pick(dp, x - 2, y - 1, rest - 1);
ways += pick(dp, x - 1, y - 2, rest - 1);
ways += pick(dp, x + 1, y - 2, rest - 1);
ways += pick(dp, x + 2, y - 1, rest - 1);
dp[x][y][rest] = ways;
}
}
}
return dp[0][0][k];
}
public static int pick(int[][][] dp,int x,int y,int rest) {
if(x<0||x>9||y<0||y>8) {
return 0;
}
return dp[x][y][rest];
}
}
⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁