所有的动态规划都可以使用暴力递归推断出来,做动态规划的题目要分成以下几步
1、分析递归的返回条件
2、先写出来暴力递归版本
3、通过写出来的暴力递归版本分析参数的变化范围然后去做DP
解这类的题目有四个模型分别是:从左到右的尝试模型、范围尝试模型、样本分析模型和业务限制模型
一、最长公共子序列
样本对应模型
首先列可能性,可能性讨论,两个字符串分别为s1、s2。以变量i指向s1的尾巴,以变量j指向s2的尾巴
可能性1:不已 i 位置字符结尾,不已 j 位置字符结尾, 例如:1234d , a123e 最长公共子序列123,不已 d 和 e结尾
可能性2:不已 i 字符串结尾 , 但是有可能以 j 位置字符结尾,例如:a123d , a12e3 最长公共子序列123
可能性3:可能以 i 字符串结尾 , 但是不以 j 位置字符串结尾,例如:a123d ,a12e3de 最长公共子序列 123d
暴力尝试
public static int longCommonSubStr(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 proc(str1,str2,str1.length-1,str2.length-1);
}
private static int proc(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 proc(str1,str2,i,j-1);
}
}else if (j == 0){
if (str1[i] == str2[j]){
return 1;
}else{
return proc(str1,str2,i-1,j);
}
}else{
int p1 = proc(str1,str2,i-1,j);
int p2 = proc(str1,str2,i,j-1);
int p3 = str1[i] == str2[j] ? (1 + process1(str1, str2, i - 1, j - 1)) : 0;
return Math.max(Math.max(p1,p2),p3);
}
}
通过暴力尝试的代码改动态规划
确定参数的变化范围,从而确定DP表的大小。s1 的运动范围是 0 ~ N,s2的运动范围是 0 ~ M,所以使用一张n*m的表就可以存下来所有的的记录
public static int dp(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];
}
二、最长公共回文子序列
给定一个字符串,求这个串的最长公共回文子序列
第一种思路:就是将这个串逆序,然后求逆序串和原来串的最长公共子序列即可求解。这里的代码就不写了
现在使用第二种解题思路:范围尝试模型。
将这个问题一步一步的拆解,先求 这个串的 L ~ R的最长回文子序列 L + 1 ~ R - 1 ..... L + N ~ R - 1 ......
列可能性:
可能性1:字符串在 L .... R 范围上 不以 L开头 也不以R结尾 例如 ‘a12321b’
可能性2:字符串在L .... R 范围上 以 L开头 不以R结尾 例如 ‘12321b’
可能性3:字符串在L .... R 范围上 以不 L开头 以R结尾 例如 ‘b12321’
可能性4:字符串在L .... R 范围上 以L开头 以R结尾 例如 ‘a12321a’
分析完成以后写尝试
public static int longestPalindromeSubseq1(String s) {
if (s == null || s.length() == 0){
return 0;
}
char[] str = s.toCharArray();
int N = str.length;
return process(str,0,N-1);
}
/**
* 暴力递归
* @param str
* @param L
* @return
*/
public static int process(char[] str,int L , int R){
if (L == R) {
return 1;
}
/**
* aa 返回2
* ab 返回1 a是回文,或者b是回文,所以返回的是1
*/
if (L == R - 1){
return str[L] == str[R] ? 2 : 1;
}
/**
* 列可能性
* 字符串在L .... R 范围上 不以 L开头 也不以R结尾
* a12321b
* 字符串在L .... R 范围上 以 L开头 不以R结尾
* 12321b
* 字符串在L .... R 范围上 以不 L开头 以R结尾
* a12321
* 字符串在L .... R 范围上 以L开头 以R结尾
* a12321a
*/
int p1 = process(str,L+1,R-1);
int p2 = process(str, L, R-1);
int p3 = process(str, L+1, R);
int p4 = str[L] == str[R]?(2+process(str,L+1,R-1)):0;
return Math.max(Math.max(p1,p2),Math.max(p3,p4));
}
改DP
/**
* DP
* @param s
* @return
*/
public static int longestPalindromeSubseq2(String s) {
if (s == null || s.length() == 0){
return 0;
}
/**
* 首先确定参数的变化范围
*/
char[] str = s.toCharArray();
int N = str.length;
int[][] dp = new int[N][N];
dp[N-1][N-1] = 1;
for (int i=0;i<N-1;i++){
dp[i][i] = 1;
dp[i][i+1] = str[i] == str[i+1] ? 2 : 1;
}
for (int L=N-3;L>=0;L--){
for (int R = L+2 ;R<N; R++){
int p1 = dp[L+1][R-1];
int p2 = dp[L][R-1];
int p3 = dp[L+1][R];
int p4 = str[L] == str[R] ? (2 + dp[L+1][R-1]) : 0;
int max = Math.max(Math.max(p1, p2), Math.max(p3, p4));
dp[L][R] = max;
}
}
return dp[0][N-1];
}
三、纸牌博弈问题
给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B一次拿牌。规定玩家A先拿,玩家B后拿。但是两个玩家只能拿走左边或者右边的牌,玩家A和玩家B都决定聪明。请返回最后获胜者的分数。
这个问题是一个典型的范围尝试模型(范围尝试模型非常注重讨论开头和结尾)
范围讨论
首先排成一条线的纸牌范围是从 0 ~ N 的,玩家A,可以从左边拿牌 拿完以后整个纸牌的范围变成 L+1 ~ N,当然玩家A也可以从右边拿牌,拿完以后纸牌的范围就变成 L ~ R-1。这两边的值谁大就选谁。如果纸牌就一张,那么玩家A直接拿走即可。
玩家B,如果只剩下一张牌,A又是先拿,所以B获得0。分析完成以后开始写尝试函数
尝试版本
public static int win1(int[] arr){
if (arr == null || arr.length == 0){
return 0;
}
int fVal = f(arr,0,arr.length-1);
int gVal = g(arr,0,arr.length-1);
return Math.max(fVal,gVal);
}
//先手函数
public static int f(int[] arr,int L , int R){
if (L == R){
return arr[L];
}
//后手函数拿左边 , 后手函数拿后边
return Math.max(arr[L] + g(arr,L+1,R),arr[R] + g(arr,L,R-1));
}
//后手函数
public static int g(int[] arr,int L,int R){
if (L == R){
return 0;
}
//先拿牌的人肯定会给你留下最小的
return Math.min(f(arr,L+1,R),f(arr,L,R-1));
}
在暴力递归的过程中会出现重复的解,所以我们可以进一步的进行优化,使用缓存,如果在递归的过程中这条递归跑过了,那么就直接拿值。
public static int win2(int[] arr){
if (arr == null || arr.length == 0){
return 0;
}
int N = arr.length;
int[][] fmap = new int[N][N];
int[][] gmap = new int[N][N];
for (int i=0;i<N;i++){
for (int j=0;j<N;j++){
fmap[i][j] = -1;
gmap[i][j] = -1;
}
}
int fVal = f2(arr,0,arr.length-1,fmap,gmap);
int gVal = g2(arr,0,arr.length-1,fmap,gmap);
return Math.max(fVal,gVal);
}
public static int f2(int[] arr,int L , int R,int[][] fmap , int[][] gmap){
int ans = 0;
if (L == R){
ans = arr[L];
}
ans = Math.max(arr[L] + g(arr,L+1,R),arr[R] + g(arr,L,R-1));
fmap[L][R] = ans;
return ans;
}
public static int g2(int[] arr,int L,int R ,int[][] fmap , int[][] gmap){
int ans = 0;
if (L == R){
ans = 0;
}
ans = Math.min(f(arr,L+1,R),f(arr,L,R-1));
gmap[L][R] = ans;
return ans;
}
通过暴力解改出来的动态规划版本,分析先手函数的参数变化范围 0 ~ N,分析后手函数的参数变化范围0 ~ N
public static int win3(int[] arr){
if (arr == null || arr.length == 0){
return 0;
}
int N = arr.length;
int[][] fmap = new int[N][N];
int[][] gmap = new int[N][N];
for (int i = 0;i<N; i++){
fmap[i][i] = arr[i];
gmap[i][i] = 0;
}
for (int col = 1;col < N; col++){
int L = 0;
int R = col;
while(R<N){
fmap[L][R] = Math.max(arr[L]+gmap[L+1][R],arr[R]+gmap[L][R-1]);
gmap[L][R] = Math.min(fmap[L+1][R],fmap[L][R-1]);
L++;
R++;
}
}
return Math.max(fmap[0][N-1],gmap[0][N-1]);
}
四、机器人的移动范围
假设有排成一排的N个位置,记作 1 ~ N , N一定大于或者等于2 开始时机器人在其中的M位置上(M 一定是 1 ~ N中的一个) 如果机器人来到1的位置,那么下一步只能来到右边2的位置 如果机器人来到N的位置,那么下一步只能来到左边N-1的位置 如果机器人来到中间的位置,那么下一步可以向左边或者向右边走 规定机器人必须走k步, 最终能来到P位置(p也是1~N中的一个)的方法有多少种。 给定四个参数N,M,K,P
从左到右的尝试模型
可能性分析
可能性分析1:机器人来到1位置,那么机器人下一步只能走向2
可能性分析2:机器人来到N位置,那么机器人下一步只能走向N-1
可能性分析3:机器人在中间,那么机器人可能走向当前位置+1,或者走向当前位置-1
递归退出条件分析
剩余的步数为 0 ,当前位置 和 目标位置相等 返回 1 ,否则返回 0
暴力尝试版本
public static int walk(int cur , int rest , int aim ,int N){
if (rest == 0){
return cur == aim ? 1:0;
}
if (cur ==1){
return walk(2,rest-1,aim,N);
}
if (cur == N){
return walk(N-1,rest-1,aim , N);
}
return walk(cur+1,rest-1,aim,N) + walk(cur-1,rest-1,aim,N);
}
记忆化搜索版本
public static int walk2(int N , int cur , int rest , int aim){
//分析:需要多大的一张缓存表,范围 cur 的移动范围 1 - N ,rest的移动范围 0 - rest。所以需要一张 (N+1)* rest 大小的一张表
int[][] dp = new int[N+1][rest+1];
//初始化这张缓存表
for (int i=0;i<=N;i++){
for (int j=0;j<=rest;j++){
dp[i][j] = -1;
}
}
return process2(N,cur,rest,aim,dp);
}
public static int process2(int N,int cur , int rest ,int aim,int[][] dp){
if (dp[cur][rest]!=-1){
return dp[cur][rest];
}
int ans = 0;
if (rest == 0){
ans = cur == aim ? 1 : 0;
}else if (cur == 1){
ans = process2(N,2,rest-1,aim,dp);
}else if (cur == N){
ans = process2(N,N-1,rest-1,aim,dp);
}else{
ans = process2(N,cur+1,rest-1,aim,dp) + process2(N,cur-1,rest-1,aim,dp);
}
dp[cur][rest] = ans;
return ans;
}
标准版本的动态规划
public static int walk3(int N , int start , int K , int aim){
int[][] dp = new int[N+1][K+1];
dp[aim][0] = 1;
for (int rest = 1;rest <= K ; rest++){
dp[1][rest] = dp[2][rest - 1];
for (int cur = 2 ;cur < N; cur++){
dp[cur][rest] = dp[cur-1][rest-1] + dp[cur + 1][rest-1];
}
dp[N][rest] = dp[N-1][rest-1];
}
return dp[start][K];
}
五、背包问题
给定两个长度都为N的数组weights和values,weigths[i] 和 values[i]分别代表i号货物的重量和价值,给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量,返回你能装下最多的价值是多少
从左到右的尝试模型
六、字符串组合问题
规定1和A对应,2和B对应,3和c对应 .... 26 和 z对应,那么一个数字字符串比如 “111”,就可以转换为 “AAA” , “KA” , “AK”,给定一个只有数字字符组成的字符串str,返回有多少种转换的结果。
从左到右的尝试模型
七、象棋问题
脑海里补充一个象棋棋盘,然后吧整个棋盘放在第一象限,棋盘的最左下角是(0,0)的位置,那么整个棋盘就是横坐标上9条线,纵坐标上10条线的区域,给你三个参数x,y,z。 返回马从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数有多少种
八、贴纸问题
给定一个字符串str,给定一个字符串类型的数组arr , 出现的字符都是小写英文arr每一个字符串,代表一张贴纸,你可以吧单个字符剪开使用,目的是为了拼出来str,返回需要至少使用多少张贴纸可以完成这个任务。
例子: str = "babac" , arr = {"ba","c","abcd"};
至少需要两张贴纸 "ba" 和 "abcd" ,因为使用这两张贴纸 , 把每一个字符单独的剪开,含有两个a,2个b,1个c。是可以拼出来str的。所以返回2。
九、路径问题
给定一个二维数组matrix , 一个人必须从左上角出发 , 最后到右下角 沿途只可以向下或者向右,沿途的数字都累加就是距离累加和,返回最小距离累加和