【算法&数据结构体系篇class20】:暴力递归到动态规划

一、最长回文子序列长度

LeetCode 516.最长回文子序列

给定一个字符串str,返回这个字符串的最长回文子序列长度
比如 : str = “a12b3c43def2ghi1kpm”
最长回文子序列是“1234321”或者“123c321”,返回长度7
package class20;

/**
 * 给定一个字符串str,返回这个字符串的最长回文子序列长度
 * 比如 : str = “a12b3c43def2ghi1kpm”
 * 最长回文子序列是“1234321”或者“123c321”,返回长度7
 * 

* https://leetcode.com/problems/longest-palindromic-subsequence/ */ public class PalindromeSubsequence { //方法一: 暴力递归 超时 public int longestPalindromeSubseq1(String s) { //边界判断 if (s == null || s.length() == 0) return 0; char[] str = s.toCharArray(); //调用递归尝试 传递字符数组,定义L,R区间的最长回文子序列 初始就是整个字符串区间 return process1(str, 0, str.length - 1); } /** * 递归函数 递归str[l...R]区间的最长回文子序列 初始从0,N-1即整个字符串边界 依次往中间缩减 * 返回最长回文子序列的长度 */ public static int process1(char[] str, int L, int R) { //base case: 如果只剩一个字符 那么最长就是1 自己 if (L == R) return 1; //base case:剩下2个字符 那么如果L R相等 就返回2 因为相等正反取是一样的 如果不等那么最长就是一个字符自身 if (L == R - 1) { return str[L] == str[R] ? 2 : 1; } //接着就分析几种情况 //1.最长回文不以L开头,不以R开头 那么就跳过当前索引 L右移 R左移 int p1 = process1(str, L + 1, R - 1); int p2 = process1(str, L, R - 1); //2.以L开头 不以R开头 那么R就需要左移来到下一位 int p3 = process1(str, L + 1, R); //3.不以L开头 以R开头 那么L就需要右移来到下一位 // 4. 以L R开头 前提就是L R所在字符相等,那么就返回 2 + 下层L+1,R-1范围的最长回文,2表示当前L R是两个长度 ,如果不等 那么就返回0 int p4 = str[L] == str[R] ? 2 + (process1(str, L + 1, R - 1)) : 0; //最后返回其中最大值 就是最长回文长度 return Math.max(Math.max(p1, p2), Math.max(p3, p4)); } //方法二: 动态规划 public int longestPalindromeSubseq2(String s) { //边界判断 if (s == null || s.length() == 0) return 0; char[] str = s.toCharArray(); int N = s.length(); //根据递归函数参数L R 得到其边界范围是0到N-1 那么对应建立N*N的二维数组 int[][] dp = new int[N][N]; //根据base case 逻辑填充初始化的二维数组 //赋值方式 L R相等则值为1 也就是对角线都是1 那么这里我们换个方式赋值 先将最后右下位置填充 //然后剩下的位置,跟 L,L+1一起赋值 也就是第二个base case L == R-1时 就是对角线往上的对角线 这样就一次性把两个base case对角线都赋值好了 dp[N - 1][N - 1] = 1; for (int i = 0; i < N - 1; i++) { //L 行因为前面最后一行N-1已经赋值所以就遍历到N-2 dp[i][i] = 1; //L ==R 值为1 dp[i][i + 1] = str[i] == str[i + 1] ? 2 : 1; //L == R-1 字符相等为2 不等为1 } //这样 对角线以及 往上的对角线的值就初始化好了 目标是要返回dp[0][N-1] ,填充上三角的数据 下三角是L>R无效边界不需要处理 //从递归函数的情况判断可得到 dp[L][R] 依赖L+1,R-1 左下位置 、 L,R-1 左位置 、 L+1,R 下位置 三个位置的最大值 //这里可以进一步小优化,不需要比较三个位置返回最大值: //比如依赖的左位置,他同样也是依赖对应三个位置 左、下、左下位置的最大值得到,而他的下,对应的就是L,R的左下位置,所以可以得到L,R 的左位置是比左下位置大的 //而依赖的下位置,同样依赖其左、下、左下三个位置的最大值得来,其左位置,就是L,R的左下位置, 所以也得到L,R的下位置是比左下位置大的 //既然得到其中左下位置肯定是比左、下两位置小的 就不需要进行比较了。提高效率 //我们将上三角补充好数组,我们从下到上,每行值从左到右赋值 //已知我们前面补充了两个对角线数据了 对角线是在最后一行位置N-1 往上得对角线最后位置是在N-2行,所以从N-3行开始往上遍历 for (int L = N - 3; L >= 0; L--) { //那么R 从哪里开始,前面已经判断对角线L,L 已经赋值,并且往上的一条对角线L,L+1也已经赋值完,所以L,L+2开始遍历 R就从L+2开始填充 for (int R = L + 2; R < N; R++) { //根据前面分析,我们不需要P1 情况的值 也就是dp[L][R] 不需要依赖dp[L+1][R-1] 左下位置 因为是小值 不影响取最大值 dp[L][R] = Math.max(dp[L][R-1],dp[L+1][R]); //p2 p3情况 取左位置和下位置较大值 //p4 如果L R的值相等 那么就再与P2 P3 比较取最大值 if(str[L] == str[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)上的方法数有多少种?
package class20;

/**
 * 请同学们自行搜索或者想象一个象棋的棋盘,
 * 然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置
 * 那么整个棋盘就是横坐标上9条线、纵坐标上10条线的区域 10*9
 * 给你三个 参数 x,y,k
 * 返回“马”从(0,0)位置出发,必须走k步
 * 最后落在(x,y)上的方法数有多少种?
 */
public class HorseJump {

    //方法一: 暴力递归
    //根据题意参数 起始点0,0是固定值, 从0,0 位置出发k步来到a,b的走法
    public static int jump(int a, int b, int k){
        //直接传递可变参数 进行调用递归
        return process(0, 0, k, a, b);
    }

    //递归 当前位置从0,0来到 x,y 还剩下rest步 走完来到 a,b位置的方法数返回
    public static int process(int x, int y, int rest, int a, int b){
        //base case:棋盘越界 10行9列
        if(x < 0 || x > 9 || y < 0 || y > 8) return 0;  //越界 表示当前路是无效的 直接返回0
        //base case:步数走完 来到了a,b 表示是一种方法 返回1 否就是0
        if(rest == 0){
            return x == a && y == b ? 1 : 0;
        }

        //接着就是分析常规情况,根据马的走法,就是走日,一共会有八种走法 那么总共的步法就是将这8种方法数累加返回即可
        //x,y 从下标0,0左下开始。 处于棋盘某个位置时

        //1. 可以往 x+2,y+1走, 走完步数rest-1
        int way = process(x+2,y+1,rest-1,a,b);

        //2.接着可以往x+1,y+2走, 累加
        way += process(x+1, y+2,rest-1,a,b);

        //3.接着可以往x-1,y+2走, 累加
        way += process(x-1, y+2,rest-1,a,b);

        //4.接着可以往x-2,y+1走, 累加
        way += process(x-2, y+1,rest-1,a,b);

        //5.接着可以往x-2,y-1走, 累加
        way += process(x-2, y-1,rest-1,a,b);

        //6.接着可以往x-1,y-2走, 累加
        way += process(x-1, y-2,rest-1,a,b);

        //7.接着可以往x+1,y-2走, 累加
        way += process(x+1, y-2,rest-1,a,b);

        //8.接着可以往x+2,y-1走, 累加
        way += process(x+2, y-1,rest-1,a,b);

        return way;  //最后返回总方法数
    }

    //方法二 动态规划,根据题意递归过程可变参数  我们可以定义三维数组
    public static int dp(int a, int b, int k){
        //x,y就是在棋盘走动 范围就10*9  k步数 递归过程就是0-k 需要k+1长度
        int[][][] dp = new int[10][9][k+1];
        //根据base case 越界为0 不需要处理 步数为0 如果位置来到a,b那么值为1 否则0 默认不需要修改
        dp[a][b][0] = 1;

        //分析依赖情况,8种依赖中都可以看到 步数都是依赖下一层的 比如k 依赖k-1 k-1依赖k-2...因为我们k=0是最底层 三维数组的最底层 而且初始值第0层已经初始化好了 所以就是从下往上刷新
        for(int rest = 1; rest <= k;rest++){
            //最外层遍历每一层步数,1-k位置  每一层的xy二维数组赋值 范围10*9
            for(int x = 0; x < 10; x++){
                for(int y = 0; y < 9; y++){
                    //马走法 可以有八个方向的走法走到下一步,依次追加每个下一步的方法数 这里依次取八种结果值累加 前提是不能越界 需要先判断是否不越界 再取对应的三维数组值,因为前面8种走法需要提前判断越界,不然不处理的话 坐标越界就异常了 定义一个pick函数返回如果越界就为0,不越界就返回对应位置值
                    int way = pick(dp,x+2,y+1,rest-1);
                    way += pick(dp,x+1,y+2,rest-1);
                    way += pick(dp,x-1,y+2,rest-1);
                    way += pick(dp,x-2,y+1,rest-1);
                    way += pick(dp,x-2,y-1,rest-1);
                    way += pick(dp,x-1,y-2,rest-1);
                    way += pick(dp,x+1,y-2,rest-1);
                    way += pick(dp,x+2,y-1,rest-1);
                    dp[x][y][rest] = way;
                }
            }
        }
        //根据递归函数主程序入参是 坐标00开始到k步 所以对应位置直接返回
        return dp[0][0][k];
    }

    //获取该三维数组位置值,如果越界就返回0
    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];
    }


    public static int ways(int a, int b, int step) {
        return f(0, 0, step, a, b);
    }

    public static int f(int i, int j, int step, int a, int b) {
        if (i < 0 || i > 9 || j < 0 || j > 8) {
            return 0;
        }
        if (step == 0) {
            return (i == a && j == b) ? 1 : 0;
        }
        return f(i - 2, j + 1, step - 1, a, b) + f(i - 1, j + 2, step - 1, a, b) + f(i + 1, j + 2, step - 1, a, b)
                + f(i + 2, j + 1, step - 1, a, b) + f(i + 2, j - 1, step - 1, a, b) + f(i + 1, j - 2, step - 1, a, b)
                + f(i - 1, j - 2, step - 1, a, b) + f(i - 2, j - 1, step - 1, a, b);

    }

    public static int waysdp(int a, int b, int s) {
        int[][][] dp = new int[10][9][s + 1];
        dp[a][b][0] = 1;
        for (int step = 1; step <= s; step++) { // 按层来
            for (int i = 0; i < 10; i++) {
                for (int j = 0; j < 9; j++) {
                    dp[i][j][step] = getValue(dp, i - 2, j + 1, step - 1) + getValue(dp, i - 1, j + 2, step - 1)
                            + getValue(dp, i + 1, j + 2, step - 1) + getValue(dp, i + 2, j + 1, step - 1)
                            + getValue(dp, i + 2, j - 1, step - 1) + getValue(dp, i + 1, j - 2, step - 1)
                            + getValue(dp, i - 1, j - 2, step - 1) + getValue(dp, i - 2, j - 1, step - 1);
                }
            }
        }
        return dp[0][0][s];
    }

    // 在dp表中,得到dp[i][j][step]的值,但如果(i,j)位置越界的话,返回0;
    public static int getValue(int[][][] dp, int i, int j, int step) {
        if (i < 0 || i > 9 || j < 0 || j > 8) {
            return 0;
        }
        return dp[i][j][step];
    }

    public static void main(String[] args) {
        int x = 7;
        int y = 7;
        int step = 10;
        System.out.println(ways(x, y, step));
        System.out.println(dp(x, y, step));

        System.out.println(jump(x, y, step));
    }
}

三、咖啡机变干净的最短时间

给定一个数组arr,arr[i]代表第i号咖啡机泡一杯咖啡的时间
给定一个正数N,表示N个人等着咖啡机泡咖啡,每台咖啡机只能轮流泡咖啡
只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯
每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发
假设所有人拿到咖啡之后立刻喝干净,
返回从开始等到所有咖啡机变干净的最短时间
三个参数:int[]arr、int N,inta、int b
package class20;


import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * 给定一个数组arr,arr[i]代表第i号咖啡机泡一杯咖啡的时间
 * 给定一个正数N,表示N个人等着咖啡机泡咖啡,每台咖啡机只能轮流泡咖啡
 * 只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯
 * 每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发
 * 假设所有人拿到咖啡之后立刻喝干净,
 * 返回从开始等到所有咖啡机变干净的最短时间
 * 三个参数:int[] arr、int N,int a、int b
 */
public class Coffee {
    //方法一  暴力递归
    //首先题目可以分成两个子问,第一是先得到n个人泡完咖啡且立刻喝干净的最短时间 得到一个drink[]数组
    //第二是根据当前喝完的时间,依次判断洗或者挥发杯子后的最短时间

    //关键点:为了泡咖啡时间最短,我们切入点就是想到排序 这里结合题意采用堆排序
    //定义一个类结构 表示 咖啡机可以用的时间点、咖啡机泡咖啡的时间,堆对象就是保存
    //这个类 排序按 时间点+泡咖啡时间,升序排序 表示人选择哪个咖啡机最快能跑完
    public static class Machine{
        public int timePoint;
        public int workTime;
        public Machine(int t, int w){
            timePoint = t;
            workTime = w;
        }
    }


    public static int minTime1(int[] arr, int n, int a, int b){
        //定义小根堆,对象为前面定义的咖啡机,根据咖啡机当前开始可用时间timePoint加上泡咖啡的时间workTime 进行升序排序,堆顶就是时间最快能跑完的咖啡机选择
        PriorityQueue heap = new PriorityQueue<>((o1, o2) -> (o1.timePoint + o1.workTime) - (o2.timePoint + o2.workTime));

        //遍历咖啡机每台的工作时间arr[i],添加到小根堆,初始的开始可用时间为0
        for(int i = 0; i heap = new PriorityQueue<>(new Comparator(){
            @Override
            public int compare(Machine o1, Machine o2) {
                return (o1.timePoint + o1.workTime) - (o2.timePoint + o2.workTime);
            }
        } );
        for(int ar : arr){
            heap.add(new Machine(0,ar));
        }
        int[] drink = new int[n];  //定义每个人泡完咖啡的时间数组
        for(int i = 0; i < n;i++){
            //遍历每个人 开始取最佳最快泡咖啡的咖啡机 弹出堆顶
            Machine poll = heap.poll();
            poll.timePoint += poll.workTime;  //使用当前咖啡机 那么他可用时间就要累加上泡咖啡结束的工作时间
            drink[i] = poll.timePoint;        //当前i人员泡完咖啡时间就是前面得到的咖啡机泡完咖啡后的时间
            heap.add(poll);                   //再将当前使用完了的咖啡机重新入堆排序 继续给后面的人使用
        }

        //到这里我们就算出了人员泡完咖啡的时间了,传递传参 进行动态规划,
        //我们将drink 以及洗杯子时间a  挥发时间b 固定参数传递
        return minTimeDp(drink,a,b);
    }

    //优化 动态规划
    public static int minTimeDp(int[] drink, int a, int b){
        //根据递归函数 得到可变参数 i 索引 范围就是0-n, free可用时间不确定,我们可以用最坏情况来定义其最大值,最小值就是0,最坏情况free就是当全部咖啡杯洗完后来到的free时刻
        int n = drink.length;
        int maxFree = 0;     //初始先为0
        for(int i = 0; i < n; i++){
            maxFree = Math.max(maxFree,drink[i]) + a; //从头遍历,洗完全部杯子来到的可用时刻就是最大的时刻,注意当前drink[i]泡完时间要先与当前可用时间free比较 取较大的才是能开始洗的时间+洗杯子时间a才是来到下一个 可用时间free
        }

        //边界定义好 ,那么我们就创建一个二维数组 (n+1)*(maxFree+1) 因为n索引递归后能来到边界所以要n+1 maxFree同理
        int[][] dp = new int[n+1][maxFree+1];
        //初始化: 最后一行的时候 值都是为0  默认就是0 无需要处理
        //根据递归依赖 i位置都是依赖i+1 也就是上一行依赖下一行,那么我们就需要确保底部行都赋值好,从下往上取填充 已知最后一行都是0了 从下往上遍历
        for(int i = n-1; i >=0; i--){
            for(int free = 0; free < maxFree+1;free++){
                //开始根据递归情况 填充数组

                //1 当前选择洗杯子 需要耗时a,这里注意有个前提时间,要考虑drink[i]洗完的时间,与当前机器可用时间谁大 就取谁的时间再加上a 洗杯子时间 就是当前杯子洗完来到的时间
                //比如drink[i]=7  free=9 也就是泡完咖啡在7时刻,而机器要到9时刻才能洗 就得等到9时刻才能洗,再加上a 如果相反9   7 那么泡完咖啡9时刻 此时7时刻就可用了 所以9时刻开始就洗杯子加a
                int selectWash1 = Math.max(drink[i],free) + a;
                //  接着来到后面i+1...的杯子进行清洗 递归 此时机器可用时间 就是当前i洗完后的时间selectWash1
                //注意 因为这个selectWash1作为 i+1后的可用时间,这个时间最后加上了a工作时间 而我们定义的大小就是free最大值,再加上a最后就有越界的情况,需要剔除这些越界情况,跳过当前i位置 free越界的后面全部情况 break
                if(selectWash1 > maxFree) break; //防止越界 直接提前跳出当前i位置后面的情况

                //  接着来到后面i+1...的杯子进行清洗 递归 此时机器可用时间 就是当前i洗完后的时间selectWash1
                int restWash1 = dp[i+1][selectWash1];
                //  取完了这个i和 i+1...后面的情况,洗完的时间,我们要取最大值,表示选择洗杯子 要全部洗完的时间 就是包括i  i+1的都洗完了 所以要取较大值 才能包括进来
                int p1 = Math.max(selectWash1,restWash1);

                //2 当前选择挥发干净杯子 挥发干净 是并行的  不需要使用机器 跟机器可用时间无关
                int selectWash2 = drink[i] + b;   //i挥发完就是 从泡完咖啡时间drink[i]加上挥发时间a
                //这里选择挥发,跟free可用时间就没关系了 所以不会有越界情况 直接赋值
                int restWash2 = dp[i+1][free];   //递归i+1..后续的挥发需要的时间 跟机器可用时间无关 不需该
                //同理 选择挥发 取全部挥发完的最大时间
                int p2 = Math.max(selectWash2,restWash2);

                //两者情况 都已经取出来了  洗杯子  或者  挥发 那么就是返回最短的完成时间,取较小值
                dp[i][free] = Math.min(p1,p2);
            }
        }
        return dp[0][0]; //根据递归情况,主程序从0号人开始 0时刻可用时间 对应位置返回
    }

    // 验证的方法
    // 彻底的暴力
    // 很慢但是绝对正确
    public static int right(int[] arr, int n, int a, int b) {
        int[] times = new int[arr.length];
        int[] drink = new int[n];
        return forceMake(arr, times, 0, drink, n, a, b);
    }

    // 每个人暴力尝试用每一个咖啡机给自己做咖啡
    public static int forceMake(int[] arr, int[] times, int kth, int[] drink, int n, int a, int b) {
        if (kth == n) {
            int[] drinkSorted = Arrays.copyOf(drink, kth);
            Arrays.sort(drinkSorted);
            return forceWash(drinkSorted, a, b, 0, 0, 0);
        }
        int time = Integer.MAX_VALUE;
        for (int i = 0; i < arr.length; i++) {
            int work = arr[i];
            int pre = times[i];
            drink[kth] = pre + work;
            times[i] = pre + work;
            time = Math.min(time, forceMake(arr, times, kth + 1, drink, n, a, b));
            drink[kth] = 0;
            times[i] = pre;
        }
        return time;
    }

    public static int forceWash(int[] drinks, int a, int b, int index, int washLine, int time) {
        if (index == drinks.length) {
            return time;
        }
        // 选择一:当前index号咖啡杯,选择用洗咖啡机刷干净
        int wash = Math.max(drinks[index], washLine) + a;
        int ans1 = forceWash(drinks, a, b, index + 1, wash, Math.max(wash, time));

        // 选择二:当前index号咖啡杯,选择自然挥发
        int dry = drinks[index] + b;
        int ans2 = forceWash(drinks, a, b, index + 1, washLine, Math.max(dry, time));
        return Math.min(ans1, ans2);
    }



    // for test
    public static int[] randomArray(int len, int max) {
        int[] arr = new int[len];
        for (int i = 0; i < len; i++) {
            arr[i] = (int) (Math.random() * max) + 1;
        }
        return arr;
    }

    // for test
    public static void printArray(int[] arr) {
        System.out.print("arr : ");
        for (int j = 0; j < arr.length; j++) {
            System.out.print(arr[j] + ", ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int len = 10;
        int max = 10;
        int testTime = 10;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int[] arr = randomArray(len, max);
            int n = (int) (Math.random() * 7) + 1;
            int a = (int) (Math.random() * 7) + 1;
            int b = (int) (Math.random() * 10) + 1;
            int ans1 = right(arr, n, a, b);
            int ans2 = minTime1(arr, n, a, b);
            int ans3 = minTime2(arr, n, a, b);
            if (ans1 != ans2 || ans2 != ans3) {
                printArray(arr);
                System.out.println("n : " + n);
                System.out.println("a : " + a);
                System.out.println("b : " + b);
                System.out.println(ans1 + " , " + ans2 + " , " + ans3);
                System.out.println("===============");
                break;
            }
        }
        System.out.println("测试结束");

    }
}

你可能感兴趣的:(算法,数据结构,动态规划,java)