数组arr代表喝完咖啡的时间,1台洗咖啡机,求让全部咖啡杯变干净的最早时间

数组arr代表喝完咖啡的时间,1台洗咖啡机,求让全部咖啡杯变干净的最早时间

提示:互联网大厂的经典动态规划题:京东原题

解决动态规划问题的重要知识!暴力递归的4种经典尝试模型
最关键的就是这个递归函数,解题的核心在这。
互联网大厂的动态规划题目中的四种经典暴力递归尝试模型:
(1)DP1:从左往右的尝试模型,关注i位置结尾,或者i位置开头的情况,或者看i联合i+1,i+2的情况,填表往往是上到下,或者下到上,左到右,右到左。
(2)DP2:从L–R范围上的尝试模型,关注L和R的情况,填表格式非常固定,主对角,副对角,倒回来填
(3)DP3:多样本位置对应的尝试模型,2个样本,一个样本做行,一个样本做列,关注i和j对应位置的情况,先填边界,再填中间
(4)DP4:业务限制类的尝试模型,比如走棋盘,固定的几个方向可以走,先填边界,再填中间。

本题是——DP4:业务限制类的尝试模型


题目

数组arr代表喝完咖啡的时间,自然有序状态,最后一个人,最后一个喝完
1台洗咖啡机,洗一个杯子需要a时间,杯子自己挥发干净需要b时间,洗咖啡机只能洗完一个杯子接着洗下一个杯子,串行工作,杯子挥发干可以并行挥发。
求让全部咖啡杯变干净的最早时间


一、审题

示例:arr=1 6 12
a=3(洗需要3时间),b=10(自己挥发要10时间);
如果让他们并行挥发则
在这里插入图片描述
则最早只能22点完成

如果全部洗呢?
在这里插入图片描述
15点,比22早,选15

当然,你还可以挥发一个,洗2个,等等等等
看看找最小时间
在这里插入图片描述
虽然1挥发到11点完事,但是别忘了12那个盘子还没喝完呢,人12点喝完,洗完最早也是15点
所以这个还挺不好求的


二、解题

根据上面的分析呢?想要找到最早时间,不仅要看杯子啥时候喝完,还要看你决定每个杯子是洗干净,还是挥发干净?
显然,对于每个杯子,就这俩骚操作,没有别的选择,因此是一个业务限制类模型DP4!

也就是限制你干2种业务,最终你选择一个最早结束时间返回就行!!!

那么其实,我们就把暴力递归的尝试代码写好就行
已经有的参数:arr,喝完咖啡的时间,a,洗咖啡杯要的时间,b自己挥发干要的时间
洗碗机比如j时刻有空,它才能开始洗,最开始j=0,一上来就有空,其他的杯子还需要继续等待的。

咱们要把arr中的杯子全部洗完,来到arr的i位置,说明0–i-1已经搞定,咱们要把剩下的i–N-1位置上的杯子全部搞干净,洗碗机啥时候有空呢?j时刻可以闲下来洗碗了。
所以定义**f(arr,a,b,i,j)**是把所有杯子洗完的最早时间点

这就是咱的暴力递归函数,2个变量。
主函数咋调用,j=0,i=0,从arr的0开始洗,0时刻洗碗机就有闲空,则洗完所有arr的杯子,最早返回时间是f(arr,a,b,0,0)

OK
f(arr,a,b,i,j)内部咋处理呢?
既然是从i=0开始调用,那
(1)当i=N-1时,最后一个杯子了
——如果用洗碗机洗,等洗碗机闲下就可以洗了,完事时间为j+a,也有可能就是喝完立马洗,arr[i]+a,选最大值,因为可能j很大

——如果让它喝完立马挥发干,完事时间为arr[i]+b
这俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
在这里插入图片描述

(2)i是其他的请,除了洗i杯子,还要考虑后续i+1–N-1上的所有杯子洗完,能得到的最早时间,才是咱们此刻的结果。
——i这个杯子,洗的话,两种情况,等洗碗机闲下来洗,完事时间为j+a,也有可能喝完立马洗,arr[i]+a,他们的最大值比如是c,这个c是我i洗完之后咖啡机的闲空时间,只有洗完我i才能洗后续的杯子
注意,除了洗i,还要等后续i+1–N-1的所有杯子洗完,比如是next=f(arr,a,b,i+1,c)
然后对比c和next,谁更晚(即最大值)就是洗i导致的最早结束时间
【这一步是最复杂的,但是要捋清,看下图中c和next对比得到max1,这是洗i导致的最早结束时间】

——i这个杯子,挥发的话,就是arr[i]+b=d,然后后续所有杯子要洗,最早结束时间是huiFaNext=f(arr,a,b,i+1,j)
为啥用j呢?挥发i之后,后续所有的杯子,不必等i挥发完才搞,直接洗!!!这里千万别搞错了。
然后对比d和huiFaNext,谁更晚(即最大值)就是
挥发i导致的最早结束时间

【注意n2里面j不是[i]+b哦,不像洗i一样,等让j=c,因为洗的话,要等我i洗完,但是挥发i不需要等i,直接洗就行!!!】
【看下图的[i]+b和n2对比得到的max2,这是挥发i导致的最早结束时间】

这俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
在这里插入图片描述
最终max1和max2,看洗和挥发谁更好。取最小值。

暴力递归就这么处理的,没别的了
就俩业务,洗,还是挥发?
决定好了,最后返回最小完事时间即可!

这个逻辑很难,但是也是业务固定为2种,理解也不难

手撕代码:

//所以定义**f(arr,a,b,i,j)**是把所有杯子洗完的最早时间点
    //这就是咱的暴力递归函数,2个变量。
    //0--i-1已经搞定,咱们要把剩下的i--N-1位置上的杯子全部搞干净,洗碗机啥时候有空呢?j时刻可以闲下来洗碗了。
    public static int f(int[] arr, int a, int b, int i ,int j){
        //(1)当i=N-1时,最后一个杯子了
        //——如果用洗碗机洗,等洗碗机闲下就可以洗了,完事时间为j+a,
        // 也有可能就是喝完立马洗,arr[i]+a,选**最大值**,因为可能j很大
        //——如果让它喝完立马挥发干,完事时间为arr[i]+b
        //这俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
        if (i == arr.length - 1){
            int p1 = Math.max(j + a, arr[i] + a);//看谁更晚——洗的结束时间
            int p2 = arr[i] + b;//喝完挥发干的结束时间
            return Math.min(p1, p2);//两者谁早谁返回
        }

        //(2)i是其他的请,除了洗i杯子,还要考虑后续i+1--N-1上的所有杯子洗完,能得到的最早时间,才是咱们此刻的结果。
        //——i这个杯子,洗的话,两种情况,等洗碗机闲下来洗,完事时间为j+a,
        // 也有可能喝完立马洗,arr[i]+a,他们的最大值比如是**c**,
        // 这个c是**我i洗完之后咖啡机的闲空时间,只有洗完我i才能洗后续的杯子**
        int p1 = j + a;//之前j闲空了,洗i
        int p2 = arr[i] + a;//喝完洗
        int c = Math.max(p1, p2);//我i洗完可能很晚,在我之后,i+1--N-1可以洗了
        //注意,除了洗i,还要等后续i+1--N-1的所有杯子洗完,比如是next=f(arr,a,b,**i+1,c**)
        int washAndNextAll = f(arr, a, b, i + 1, c);
        //然后对比c和next,谁更晚(即最大值)就是**洗i导致的最早结束时间**。
        int washiAndNext = Math.max(c, washAndNextAll);//无论如何,洗i,导致的结果,最晚啥事结束?
        //【这一步是最复杂的,但是要捋清】
        //——i这个别字,挥发的话,就是arr[i]+b
        int d = arr[i] + b;
        int huiFaAndNextAll = f(arr, a, b, i + 1, j);//挥发i自后,后续所有洗干净的时间,j就是不必等i挥发
        //然后对比d和huiFaNext,谁更晚(即最大值)就是**挥发i导致的最早结束时间**。
        int huiFaAndNext = Math.max(d, huiFaAndNextAll);//挥发导致的自后结果,最晚啥时候结束?

        //——上面总体俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
        return Math.min(washiAndNext, huiFaAndNext);
    }

    //主函数咋调用,j=0,i=0,从arr的0开始洗,0时刻洗碗机就有闲空,
    // 则洗完所有arr的杯子,最早返回时间是**f(arr,a,b,0,0)**
    public static int washCupsFirstTime(int[] arr, int a, int b){
        if (arr == null || arr.length == 0) return 0;

        return f(arr, a, b, 0, 0);
    }

    public static void test(){
        int[] arr = {1,6,12};
        int a = 3;
        int b = 10;

        System.out.println(washCupsFirstTime(arr, a, b));
    }

    public static void main(String[] args) {
        test();
    }

测试:

15

只要逻辑整对了,其实手撕暴力递归的代码非常容易,关键就在你把问题想清楚了!

笔试AC:改记忆化搜索方法,让傻缓存dp表伴随暴力递归

这里,根据业务限制,推导,咱们可能遇到的洗碗机空闲时间的最大值M时刻
就是一切杯子全洗,这样得到的最后一个洗完杯子的时刻就是最大的空闲时刻M

int M = 0;//洗碗机空闲时间
        for (int i = 0; i < N; i++) {
            M = arr[i] + a;//喝完就洗碗,最后一个洗完就是最大的空闲时刻,arr升序的
        }

当然,如果a>b,说明洗碗比挥发狗,干脆返回最后一个碗,挥发之后的时间,不用洗了

int N = arr.length;
        if (a > b) return arr[N - 1] + b;//挥发很快的话,洗个毛啊还

okay,i取0–N-1,N长度
j取0–M,M+1长度
建一个dp表,dp[i][j]代表从j空闲时刻开始,洗碗,把所i–N-1范围内的碗洗完,最早的结束时间;
在这里插入图片描述
然后让dp表做傻缓存,跟随f,只要求过的就放入保存,省的下次再暴力递归求相同的东西

//笔试AC:改记忆化搜索方法,让傻缓存dp表伴随暴力递归
    public static int fDP(int[] arr, int a, int b, int i ,int j, int[][] dp){
        if (dp[i][j] != -1) return dp[i][j];
        //(1)当i=N-1时,最后一个杯子了
        //——如果用洗碗机洗,等洗碗机闲下就可以洗了,完事时间为j+a,
        // 也有可能就是喝完立马洗,arr[i]+a,选**最大值**,因为可能j很大
        //——如果让它喝完立马挥发干,完事时间为arr[i]+b
        //这俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
        if (i == arr.length - 1){
            int p1 = Math.max(j + a, arr[i] + a);//看谁更晚——洗的结束时间
            int p2 = arr[i] + b;//喝完挥发干的结束时间
            dp[i][j] = Math.min(p1, p2);//两者谁早谁返回

            return dp[i][j];
        }

        //(2)i是其他的请,除了洗i杯子,还要考虑后续i+1--N-1上的所有杯子洗完,能得到的最早时间,才是咱们此刻的结果。
        //——i这个杯子,洗的话,两种情况,等洗碗机闲下来洗,完事时间为j+a,
        // 也有可能喝完立马洗,arr[i]+a,他们的最大值比如是**c**,
        // 这个c是**我i洗完之后咖啡机的闲空时间,只有洗完我i才能洗后续的杯子**
        int p1 = j + a;//之前j闲空了,洗i
        int p2 = arr[i] + a;//喝完洗
        int c = Math.max(p1, p2);//我i洗完可能很晚,在我之后,i+1--N-1可以洗了
        //注意,除了洗i,还要等后续i+1--N-1的所有杯子洗完,比如是next=f(arr,a,b,**i+1,c**)
        int washAndNextAll = fDP(arr, a, b, i + 1, c, dp);
        //然后对比c和next,谁更晚(即最大值)就是**洗i导致的最早结束时间**。
        int washiAndNext = Math.max(c, washAndNextAll);//无论如何,洗i,导致的结果,最晚啥事结束?
        //【这一步是最复杂的,但是要捋清】
        //——i这个别字,挥发的话,就是arr[i]+b
        int d = arr[i] + b;
        int huiFaAndNextAll = fDP(arr, a, b, i + 1, j, dp);//挥发i自后,后续所有洗干净的时间,j就是不必等i挥发
        //然后对比d和huiFaNext,谁更晚(即最大值)就是**挥发i导致的最早结束时间**。
        int huiFaAndNext = Math.max(d, huiFaAndNextAll);//挥发导致的自后结果,最晚啥时候结束?

        //——上面总体俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
        dp[i][j] = Math.min(washiAndNext, huiFaAndNext);

        return dp[i][j];
    }
    public static int washCupsFirstTimeDP(int[] arr, int a, int b){
        if (arr == null || arr.length == 0) return 0;
        int N = arr.length;
        if (a > b) return arr[N - 1] + b;//挥发很快的话,洗个毛啊还

        int M = 0;//洗碗机空闲时间
        for (int i = 0; i < N; i++) {
            M = arr[i] + a;//喝完就洗碗,最后一个洗完就是最大的空闲时刻,arr升序的
        }
        //dp[i][j]代表从j空闲时刻开始,洗碗,把所i--N-1范围内的碗洗完,最早的结束时间;
        int[][] dp = new int[N][M + 1];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M + 1; j++) {
                dp[i][j] = -1;//初始化
            }
        }

        return fDP(arr, a, b, 0, 0, dp);
    }

    public static void test(){
        int[] arr = {1,6,12};
        int a = 3;
        int b = 10;

        System.out.println(washCupsFirstTime(arr, a, b));
        System.out.println(washCupsFirstTimeDP(arr, a, b));
    }

    public static void main(String[] args) {
        test();
    }

测试没问题:

15
15

面试精华解:根据暴力递归直接改动态规划填dp表,推转移方程

跟傻缓存一样,只不过这一次,咱不是dp伴随f,咱要把f直接改为动态规划的转移方程:

i取0–N-1,N长度
j取0–M,M+1长度
建一个dp表,dp[i][j]代表从j空闲时刻开始,洗碗,把所i–N-1范围内的碗洗完,最早的结束时间;
在这里插入图片描述
咱们要五角星那个格子:dp[0][0]代表从j=0空闲时刻开始,洗碗,把所0–N-1范围内的碗洗完,最早的结束时间;

根据暴力递归的代码:

//(1)当i=N-1时,最后一个杯子了
        //——如果用洗碗机洗,等洗碗机闲下就可以洗了,完事时间为j+a,
        // 也有可能就是喝完立马洗,arr[i]+a,选**最大值**,因为可能j很大
        //——如果让它喝完立马挥发干,完事时间为arr[i]+b
        //这俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
        if (i == arr.length - 1){
            int p1 = Math.max(j + a, arr[i] + a);//看谁更晚——洗的结束时间
            int p2 = arr[i] + b;//喝完挥发干的结束时间
            return Math.min(p1, p2);//两者谁早谁返回
        }

这一句,就能把dp最后一行填好
其余这些代码,就能把剩余dp的ij格子填好:

//(2)i是其他的请,除了洗i杯子,还要考虑后续i+1--N-1上的所有杯子洗完,能得到的最早时间,才是咱们此刻的结果。
        //——i这个杯子,洗的话,两种情况,等洗碗机闲下来洗,完事时间为j+a,
        // 也有可能喝完立马洗,arr[i]+a,他们的最大值比如是**c**,
        // 这个c是**我i洗完之后咖啡机的闲空时间,只有洗完我i才能洗后续的杯子**
        int p1 = j + a;//之前j闲空了,洗i
        int p2 = arr[i] + a;//喝完洗
        int c = Math.max(p1, p2);//我i洗完可能很晚,在我之后,i+1--N-1可以洗了
        //注意,除了洗i,还要等后续i+1--N-1的所有杯子洗完,比如是next=f(arr,a,b,**i+1,c**)
        int washAndNextAll = f(arr, a, b, i + 1, c);
        //然后对比c和next,谁更晚(即最大值)就是**洗i导致的最早结束时间**。
        int washiAndNext = Math.max(c, washAndNextAll);//无论如何,洗i,导致的结果,最晚啥事结束?
        //【这一步是最复杂的,但是要捋清】
        //——i这个别字,挥发的话,就是arr[i]+b
        int d = arr[i] + b;
        int huiFaAndNextAll = f(arr, a, b, i + 1, j);//挥发i自后,后续所有洗干净的时间,j就是不必等i挥发
        //然后对比d和huiFaNext,谁更晚(即最大值)就是**挥发i导致的最早结束时间**。
        int huiFaAndNext = Math.max(d, huiFaAndNextAll);//挥发导致的自后结果,最晚啥时候结束?

        //——上面总体俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
        return Math.min(washiAndNext, huiFaAndNext);

咱们来改一下:

//面试精华解:根据暴力递归直接改动态规划填dp表,推转移方程
    public static int washCupsFirstTimeDP2(int[] arr, int a, int b){
        if (arr == null || arr.length == 0) return 0;
        int N = arr.length;
        if (a > b) return arr[N - 1] + b;//挥发很快的话,洗个毛啊还

        int M = 0;//洗碗机空闲时间
        for (int i = 0; i < N; i++) {
            M = arr[i] + a;//喝完就洗碗,最后一个洗完就是最大的空闲时刻,arr升序的
        }
        //dp[i][j]代表从j空闲时刻开始,洗碗,把所i--N-1范围内的碗洗完,最早的结束时间;
        int[][] dp = new int[N][M + 1];

        //(1)当i=N-1时,最后一个杯子了
        //——如果用洗碗机洗,等洗碗机闲下就可以洗了,完事时间为j+a,
        // 也有可能就是喝完立马洗,arr[i]+a,选**最大值**,因为可能j很大
        //——如果让它喝完立马挥发干,完事时间为arr[i]+b
        //这俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值

        for (int j = 0; j < M + 1; j++) {
            int p1 = Math.max(j + a, arr[N - 1] + a);//看谁更晚——洗的结束时间
            int p2 = arr[N - 1] + b;//喝完挥发干的结束时间
            dp[N - 1][j] = Math.min(p1, p2);//两者谁早谁返回
        }

        //(2)i是其他的请,除了洗i杯子,还要考虑后续i+1--N-1上的所有杯子洗完,能得到的最早时间,才是咱们此刻的结果。
        //——i这个杯子,洗的话,两种情况,等洗碗机闲下来洗,完事时间为j+a,
        // 也有可能喝完立马洗,arr[i]+a,他们的最大值比如是**c**,
        // 这个c是**我i洗完之后咖啡机的闲空时间,只有洗完我i才能洗后续的杯子**
        for (int i = N - 2; i >= 0; i--) {
            for (int j = 0; j < M + 1; j++) {
                int p1 = j + a;//之前j闲空了,洗i
                int p2 = arr[i] + a;//喝完洗
                int c = Math.max(p1, p2);//我i洗完可能很晚,在我之后,i+1--N-1可以洗了
                //注意,除了洗i,还要等后续i+1--N-1的所有杯子洗完,比如是next=f(arr,a,b,**i+1,c**)
                int washAndNextAll = 0;
                if (c <= M) washAndNextAll= dp[i + 1][c];//确保这个c不会越界
                //然后对比c和next,谁更晚(即最大值)就是**洗i导致的最早结束时间**。
                int washiAndNext = Math.max(c, washAndNextAll);//无论如何,洗i,导致的结果,最晚啥事结束?
                //【这一步是最复杂的,但是要捋清】
                //——i这个别字,挥发的话,就是arr[i]+b
                int d = arr[i] + b;
                int huiFaAndNextAll = dp[i + 1][j];//挥发i自后,后续所有洗干净的时间,j就是不必等i挥发
                //然后对比d和huiFaNext,谁更晚(即最大值)就是**挥发i导致的最早结束时间**。
                int huiFaAndNext = Math.max(d, huiFaAndNextAll);//挥发导致的自后结果,最晚啥时候结束?

                //——上面总体俩情况,到底挥发好呢?还是洗好呢?看完事的最小时间,即取最小值
                dp[i][j] =  Math.min(washiAndNext, huiFaAndNext);
            }
        }

        return dp[0][0];//红色五角星那个格子就是结果
    }

    public static void test(){
        int[] arr = {1,6,12};
        int a = 3;
        int b = 10;

        System.out.println(washCupsFirstTime(arr, a, b));
        System.out.println(washCupsFirstTimeDP(arr, a, b));
        System.out.println(washCupsFirstTimeDP2(arr, a, b));
    }

    public static void main(String[] args) {
        test();
    }

就是把相应的f改为dp就行,ij是下标,途中保证c不要越界,因为M是最大值

结果:

15
15
15

总结

提示:重要经验:

1)本题是DP4:业务限制类模型,今后遇到棋盘走位,只有三个方向可走,洗完,挥发,只能干特定的有限制的业务,变量还少,就可以考虑用这个模型
2)业务限制类的模型,是很难,但是想清楚逻辑,也就能捋清代码了。本题要好好吃透,这个可是京东的面试原题!
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值