从暴力递归到动态规划(三)

文章介绍了如何通过动态规划和递归方法解决字符串的最长回文子序列问题,同时探讨了与之类似的象棋马问题,展示了将问题分解、设计状态转移方程和优先队列在算法中的应用。
摘要由CSDN通过智能技术生成

题目一:返回最长回文子序列长度

     给定一个字符串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;
        }
    }

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值