题目一:返回最长回文子序列长度
给定一个字符串str,返回这个字符串的最长回文子序列长度 比如 : str = “a12b3c43def2ghi1kpm” 最长回文子序列是“1234321”或者“123c321”,返回长度7
首先我们要了解,子序列和子串是不一样的,一般情况下子序列是可以不连续的,而子串是必须连续的。什么是回文呢,回文是正过来念和反过来念一样。如题目所举例子。
如何解?
这里有一个偷巧的方式,不知道大家是否还记得上篇文章中的最长公共子序列的问题,我们将该题的字符串做一个逆序,然后用逆序的字符串和原串的最长公共子序列,不就是该字符串的最长回文子序列的长度嘛。
当然,如果我们不想用这种方式,我们就想用动态规划的方式来解决,那应该如何做呢,上篇文章中的最长公共子序列问题用的是样本对应模型,而最长回文子序列,可以用范围尝试模型来解决。
接下来我们开始尝试!!!
分析:我们定义一个函数f,这个f把字符串,L,R 传进去,f(str,L,R)什么意思,我现在就讨论一件事,在str L 到 R 范围上,你告诉我str的最长子序列是多长。所以返回一个整数,长度,那么我们该如何写呢?
我们还是先看baseCase,如果当L==R的时候,这是我们只有一个元素,有回文子序列?当然有呗,它自己不就是嘛。接下来再讨论有两个元素时,如果两个元素相等,则就有呗,回文子序列为2,如果不等,他们最长回文就是1,要么单独a,要么单独b,但是他们两个构不成回文。接下来我们讨论普遍情况,有几种可能性,可能性一,该回文子序列不以L开头也不以R结尾例如a12345b,那么最长子序列和它有什么关系,有没有它都一样,我们直接去f(str,L+1,R-1)上找,情况二,以L开头,不以R结尾例如1a2345b,那么我们就L保持,R继续往下找,f(str,L,R-1),情况三,不以L开头,以R结尾,例如a1234b5,那么,我们就R保持,L往下找,f(str,L+1,R),情况四,即以L开头,也以R结尾,例如1a234b5,这样的话,我们就有两个子序列了,L和R 我们继续去L+1,R-1中找,代码如下:
public static int longestPalindromeSubseq1(String s){
if (s == null || s.length() == 0){
return 0;
}
char[] chars = s.toCharArray();
return f1(chars,0,chars.length - 1);
}
// str[L...R] 最长回文子序列长度返回
public static int f1(char[] strs,int L,int R){
if (L == R){
return 1;
}
if (L == R - 1){
return strs[L] == strs[R - 1] ? 2:1;
}
//即不以L开头,也不以R结尾
int p1 = f1(strs,L+1,R-1);
int p2 = f1(strs,L,R-1);
int p3 = f1(strs,L+1,R);
int p4 = strs[L] == strs[R] ? (2 + f1(strs,L+1,R-1)) : 0;
return Math.max(Math.max(p1,p2),Math.max(p3,p4));
}
根据经验而来,范围尝试模型,特别在乎讨论开头如何如何,结尾如何如何,样本对应模型特别在乎两个样本结尾如何如何。
好了,递归尝试出来了,接下来就可以开始改动态规划了,根据递归函数有两个可变参数L和R可知dp是一个二维数组,根据主函数调用可知我们需要返回的是dp[0][chars.length-1]这个地方的值。接下来分析依赖,有baseCase可知,对角线全为1,然后对角线的下一个斜线跟str[i]和str[i+1]是否相等有关,根据下面递归的调用,可知一个普通位置的值依赖于该位置左,下和左下的位置,画出依赖表如下所示:
然后我们就可以根据已填的内容,把整张表补充完整,右上角就是我们需要的值,代码如下:
public static int longestPalindromeSubseq2(String s){
if (s == null || s.length() == 0){
return 0;
}
char[] chars = s.toCharArray();
int N = chars.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] = chars[i] == chars[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 = chars[L] == chars[R] ? (2 + dp[L+1][R-1]) : 0;
dp[L][R] = Math.max(Math.max(p1,p2),Math.max(p3,p4));
}
}
return dp[0][N-1];
}
其实,这道题还可以继续优化,根据上述分析,一个普通位置,它依赖于左,下和左下位置,可能性可能性1是左下的值可能性2是左可能性3是下,可能性4如果在存在的情况下是2+左下,我们知道,该值是由这三个值取最大值出来的,所以当前位置的值决不可能比那三个位置的值小,它的左边和下边同样也依赖于这3个地方,由此分析该位置的左下位置其实是不需要的,因为左下位置的值一定比左边和下边位置的值小,所以,我们可以先用左边和下边比一下,如果有第四种情况,我们再和第四种情况比,没有就不比了,优化后代码如下:
public static int longestPalindromeSubseq3(String s){
if (s == null || s.length() == 0){
return 0;
}
char[] chars = s.toCharArray();
int N = chars.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] = chars[i] == chars[i+1] ?2:1;
}
for (int L = N-3; L >=0 ; L--) {
for (int R = L+2; R < N; R++) {
dp[L][R] = Math.max(dp[L][R - 1],dp[L+1][R]);
if (chars[L] == chars[R]){
dp[L][R] = Math.max(dp[L][R],2+dp[L+1][R-1]);
}
}
}
return dp[0][N-1];
}
题目二
请同学们自行搜索或者想象一个象棋的棋盘, 然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置 那么整个棋盘就是横坐标上9条线、纵坐标上10条线的区域 给你三个 参数 x,y,k 返回“马”从(0,0)位置出发,必须走k步 最后落在(x,y)上的方法数有多少种?
大家看到这一题有没有觉得很熟悉,想起来了吧,是不是和之前文章中机器人那一题很像。但是这一题跟机器人那题还有些不同,机器人是只能往左或者往右,而该题,马有8个方向可以走,如下图:
所以有8中可能性,所以,我们先写一个递归。
分析:如果只剩0步,并且该点正好在a,b位置,那么就找到了一种方法,这个就是该题的baseCase,如果马蹦出棋盘,那么就返回0,接下来就是八个位置的跳。
代码如下:
public static int jump(int a,int b,int k){
return process(0,0,k,a,b);
}
//当前来到的位置是x,y
//还剩下rest可以走
//跳完rest步,正好跳到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;
}
看起来很清晰吧,接下来就要改动态规划了,通过递归方法可知,该动态规划dp表是一个三维的,我们先看x变化范围 0 - 9 ,y的变化范围 0 - 8 ,rest的变化范围,0-k,接下来准备一个三维数组。虽然看着很复杂,但是它的依赖关系很简单,我们看当rest == 0时,我们就看 x,y 等不等于a,b,我们由此知道的该三维的第0层,我们再看递归调用都是依赖底下一层的,同一层是不会互相依赖的。
代码如下:
//当前来到的位置是x,y
//还剩下rest可以走
//跳完rest步,正好跳到a,b的方法数是多少
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 = peek(dp,x+2,y+1,rest - 1);
ways += peek(dp,x+1,y+2,rest - 1);
ways += peek(dp,x-1,y+2,rest - 1);
ways += peek(dp,x-2,y+1,rest - 1);
ways += peek(dp,x-2,y-1,rest - 1);
ways += peek(dp,x-1,y-2,rest - 1);
ways += peek(dp,x+1,y-2,rest - 1);
ways += peek(dp,x+2,y-1,rest - 1);
dp[x][y][rest] = ways;
}
}
}
return dp[0][0][k];
}
通过这些题我们可以知道,如果会写尝试,啥都有了,如果直接写动态规划的状态转移方程,那得想到啥时候啊。如果是像这种简单依赖的话,我们是可以改的,但如果依赖巨复杂,我们直接改都不改,直接记忆化搜索。
题目三:咖啡机泡咖啡问题
给定一个数组arr,arr[i]代表第i号咖啡机泡一杯咖啡的时间 给定一个正数N,表示N个人等着咖啡机泡咖啡,每台咖啡机只能轮流泡咖啡 只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯 每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发 假设所有人拿到咖啡之后立刻喝干净, 返回从开始等到所有咖啡机变干净的最短时间 三个参数:int[] arr、int N,int a、int b。
初看这问题,woc,好变态啊,啥思路都没有,其实,我们可以把这题拆开来看,把这题拆成两题,第一题,假设有N个人,我给你返回一个数组,返回一个数组是啥呢,每个人最快喝完的时间。这题我们可以用堆来实现,我们可以搞一个小根堆,小根堆里放一个对象,对象里两个属性,咖啡机还有多长时间可用以及咖啡泡一杯要多久,小根堆怎么排序,用再用的时间加泡一杯要多久,共同决定对象在小根堆中的顺序。因为得到咖啡后会立刻喝完,每个小人可以选择用洗咖啡杯的机器去洗,洗是串行的,也可以让他自行挥发,挥发是可以并行的,要求获得所有杯子都变干净的最小时间。
分析:我们由刚刚拆分的第一问,可以得到开始清洗的时间,我们可以根据清洗时间,写出一个递归,如下:
//process(drinks,3,10,0,0)
//a 洗一杯的时间 固定变量
//b 自己挥发的时间 固定变量
//index [0 ... index - 1] 已经干净,不用担心了
//drinks[index....] 都想要变干净,这是需要操心的, washLine表示洗的机器何时可用
//drinks[index....] 变干净,所需最少时间
public static int bastTime(int[] drinks,int wash,int air,int index,int free){
if (index == drinks.length){
return 0;
}
//index 杯子决定洗,
//自己干净的时间
//如果我喝完了,咖啡机还有70分钟可用,那么我干净的时间就是 70 + 洗杯子时间
//如果我在25时刻喝完了, 但是洗咖啡杯的机器在 15 时刻就开始空余了,我当然能立马洗,所以我干净时间就是 我喝完时刻+洗杯子时间
//所以要取两者最大值
int selfClean1 = Math.max(drinks[index],free) + wash;
int restClean1 = bastTime(drinks,wash,air,index + 1,selfClean1);
int p1 = Math.max(selfClean1,restClean1);
//index 号杯子挥发
int selfClean2 = drinks[index] + air;
int restClean2 = bastTime(drinks,wash,air,index + 1,free);
int p2 = Math.max(selfClean2,restClean2);
return Math.min(p1,p2);
}
通过递归我们发现,只有两个可变参数,这不就是一个二维表嘛,我们知道index的范围是drinks的长度,但free的范围呢?不知道,这种模型叫做业务限制模型,你设计的可变参数不能直观的获取到它的变化范围,我们可以取free的最大值,我们让所有杯子都去洗,free的最大值,所有的free的变化范围肯定在其中。然后,我们就可以根据递归改出动态规划的形式,其中有一点是我从dp取的时候,selfClean1可能越界,如果越界,我们就直接continue就可以了。
public static int minTime(int[] arr,int n,int a,int b){
PriorityQueue<Machine> heap = new PriorityQueue<>(((o1, o2) -> (o1.timePoint+o1.workTime)-(o2.workTime+o2.timePoint)));
for (int i = 0; i < arr.length; i++) {
heap.add(new Machine(0,arr[i]));
}
int[] drinks = new int[n];
for (int i = 0; i < n; i++) {
Machine cur = heap.poll();
cur.timePoint += cur.workTime;
drinks[i] = cur.timePoint;
heap.add(cur);
}
return bastTimeDp(drinks,a,b);
}
//bestTime(drinks,3,10,0,0)
//wash 洗一杯的时间 固定变量
//air 自己挥发的时间 固定变量
//index [0 ... index - 1] 已经干净,不用担心了
//drinks[index....] 都想要变干净,这是需要操心的, free表示洗的机器何时可用
//drinks[index....] 变干净,所需最少时间
public static int bastTime(int[] drinks,int wash,int air,int index,int free){
if (index == drinks.length){
return 0;
}
//index 杯子决定洗,
//自己干净的时间
//如果我喝完了,咖啡机还有70分钟可用,那么我干净的时间就是 70 + 洗杯子时间
//如果我在25时刻喝完了, 但是洗咖啡杯的机器在 15 时刻就开始空余了,我当然能立马洗,所以我干净时间就是 我喝完时刻+洗杯子时间
//所以要取两者最大值
int selfClean1 = Math.max(drinks[index],free) + wash;
int restClean1 = bastTime(drinks,wash,air,index + 1,selfClean1);
int p1 = Math.max(selfClean1,restClean1);
//index 号杯子挥发
int selfClean2 = drinks[index] + air;
int restClean2 = bastTime(drinks,wash,air,index + 1,free);
int p2 = Math.max(selfClean2,restClean2);
return Math.min(p1,p2);
}
public static int bastTimeDp(int[] drinks,int wash,int air){
int maxFree = 0;
for (int i = 0; i < drinks.length; i++) {
maxFree = Math.max(maxFree,drinks[i]) + wash;
}
int N = drinks.length;
int[][] dp = new int[N+1][maxFree + 1];
for (int index = N-1; index >= 0 ; index--) {
for (int free = 0; free <= maxFree; free++) {
int selfClean1 = Math.max(drinks[index],free) + wash;
if (selfClean1 > maxFree){
continue;
}
int restClean1 = dp[index + 1][selfClean1];
int p1 = Math.max(selfClean1,restClean1);
//index 号杯子挥发
int selfClean2 = drinks[index] + air;
int restClean2 = dp[index + 1][free];
int p2 = Math.max(selfClean2,restClean2);
dp[index][free] = Math.min(p1,p2);
}
}
return dp[0][0];
}
public static class Machine{
public int timePoint;
public int workTime;
public Machine(int timePoint, int workTime) {
this.timePoint = timePoint;
this.workTime = workTime;
}
}