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

从暴力递归到动态规划已经接近尾声,动态规划所有的内容都在这几篇文章中了,这篇文章主要是做一些总结。在总结之前,我们先回顾一下之前改写的整个套路。

题目一:

 给定一个正数数组arr, 请把arr中所有的数分成两个集合,尽量让两个集合的累加和接近 返回: 最接近的情况下,较小集合的累加和

分析:比如说一个数组[3,4,5,1],一定要分成两个数组,并且他们的累加和要尽量接近,那么客观上来讲,3和4 一个数组  5和1 一个数组,他们累加和一个是7 一个是6  我们返回较小的  6 ,那么这道题的尝试怎么写呢,其实我们一眼就看出来了,这道题不就是一个改背包的问题嘛,我们还是先从尝试开始,这里我就直接把尝试代码给大家了,相信大家已经轻车熟路了,看起来一定soeasy。

public static int right(int[] arr){
        if (arr == null || arr.length<2){
            return 0;
        }
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        return process(arr,0,sum / 2);
    }

    //arr[i....] 可以自由选择,请返回尽量接近rest,但不能超过rest的情况下,最接近的累加和是多少
    private static int process(int[] arr, int i, int rest) {
        if (i == arr.length){
            return 0;
        }else {
            int p1 = process(arr,i+1,rest);
            int p2 = 0;
            if (arr[i] <= rest){
                p2 = arr[i] + process(arr,i+1,rest - arr[i]);
            }
            return Math.max(p1,p2);
        }
    }

怎么样,有了前几章的铺垫,看起来是不是很easy啊,接下来我们该干啥了,对,改动态规划呀。这样我们就要关心一个问题了,有没有重复解的出现。有哇,举个栗子,[5,2,3 ........],假如我们要凑小于等于20的解,其中有一个选择,我们要0位置,不要1 不要 2,那么,下一个选择就是3位置凑15  ,还有一个选择,我们不要0,要1,要2  这样,下一个选择依旧是 3位置 凑 15,怎么样,重复解是不是出现了。有重复解,说明我们改dp有利可图。接下来我们看该暴力递归有两个可变参数,是不是这两个可变参数确定了,返回值就确定了,是的,所以这是一张二维表。那么i的变化范围是什么呢  0 - N ,rest的范围呢  0 - sum/2  不会超过这些范围。如果我们把这张表填满,所有返回值就装下了,接下来怎么填满这张表呢,最终我们想要哪个位置呢, (0,sum/2)位置。baseCase是 i == N的时候全填0  ,也就是说这张表的最后一行全是0,接下来我们分析普遍位置怎么依赖,通过观察递归函数得知,当我在第i行,我总是依赖 i+1 的位置。妥了,这样我们不就可以从底往上推,最后推到右上角返回。

接下来就是根据上述分析和暴力递归代码,改为动态规划代码。代码如下:

public static int dp(int[] arr){
        if (arr == null || arr.length<2){
            return 0;
        }
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        sum /= 2;
        int N = arr.length;
        int[][] dp = new int[N+1][sum + 1];
        for (int i = N-1; i >= 0 ; i--) {
            for (int rest = 0; rest <= sum; rest++) {
                int p1 = dp[i+1][rest];
                int p2 = 0;
                if (arr[i] <= rest){
                    p2 = arr[i] + dp[i+1][rest - arr[i]];
                }
                dp[i][rest] =  Math.max(p1,p2);
            }
        }
        return dp[0][sum];
    }

看,是不是很熟悉的套路呀,这就是我们的动态规划版本。

题目二:

给定一个正数数组arr,请把arr中所有的数分成两个集合 如果arr长度为偶数,两个集合包含数的个数要一样多 如果arr长度为奇数,两个集合包含数的个数必须只差一个 请尽量让两个集合的累加和接近 返回: 最接近的情况下,较小集合的累加和

分析:我们看这题,还是有一些难的哈,他有两个标准,第一个是集合的个数,第二个是他们的累加和。偶数个很好理解一边四个嘛,关键是奇数个的时候,我们该怎么办呢  假如说有7个数,我们需要的累加和是100  第一中情况,我们必须拿够3个数小于等于100,离100最近的,第二种情况,我必须拿够4个数,小于等于100 离100最近的。我们应该返回这两种可能性最接近的那个。我们不能规定就三个或者就4个  例如  100,1,1,1,1,1,1  这样,客观上来讲, 我们只能 [100 1 1 ]   ,[1,1,1,1] 这样分。那么我们怎么写尝试呢,在这之前我们已经会写不要求个数的尝试了,那这一题我们不就加一个挑的个数限制嘛。代码如下:

public static int right(int[] arr){
        if (arr == null || arr.length<2){
            return 0;
        }
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }

        if ((arr.length & 1) == 0){
            return process(arr,0,arr.length/2,sum/2);
        }else {
            return Math.max(process(arr,0,arr.length/2,sum/2),process(arr,0,arr.length/2+1,sum/2));
        }
    }

    //arr[i....] 可以自由选择,挑选个数一定要picks个,请返回尽量接近rest,但不能超过rest的情况下,最接近的累加和是多少
    private static int process(int[] arr, int i, int picks, int rest) {
        if (i == arr.length){
            return picks == 0 ? 0 : -1;
        }else {
            int p1 = process(arr,i+1,picks,rest);

            int p2 = -1;
            int next = -1;
            if (arr[i] <= rest){
                next = process(arr,i+1,picks - 1,rest -arr[i]);
            }
            if (next != -1){
                p2 = arr[i] + next;
            }
            return Math.max(p1,p2);
        }
    }

看是不是很简单,但是有一点,这里是三个可变参数,那么改dp就是一个三维数组,当然我们也不是第一次见三维数组,例如之前的棋盘跳马问题我们就遇到了三维数组,三维就三维嘛,我们根据暴力递归改嘛,把这张三维表填完,就得到答案了,准不会错。

public static int dp(int[] arr){
        if (arr == null || arr.length<2){
            return 0;
        }
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        sum /= 2;
        int N = arr.length;
        int M = (N+1)/2;
        int[][][] dp = new int[N+1][M][sum+1];

        for (int i = 0; i <= N ; i++) {
            for (int j = 0; j <= M ; j++) {
                for (int k = 0; k <= sum ; k++) {
                    dp[i][j][k]  = -1;
                }
            }
        }

        for (int rest = 0; rest <=sum ; rest++) {
            dp[N][0][rest] = 0;
        }

        for (int i = N - 1; i >= 0 ; i--) {
            for (int picks = 0; picks <= M; picks++) {
                for (int rest = 0; rest <= sum ; rest++) {
                    int p1 = dp[i+1][picks][rest];

                    int p2 = -1;
                    int next = -1;
                    if (picks - 1 >=0 && arr[i] <= rest){
                        next = dp[i+1][picks - 1][rest -arr[i]];
                    }
                    if (next != -1){
                        p2 = arr[i] + next;
                    }
                    dp[i][picks][rest] =  Math.max(p1,p2);
                }
            }
        }


        if ((arr.length & 1) == 0){
            return dp[0][arr.length/2][sum];
        }else {
            return Math.max(dp[0][arr.length/2][sum],dp[0][arr.length/2+1][sum]);
        }
    }

我们也见过这么多到题了,接下来我们总结一下。

动态规划最重要的是什么呀,没错,最重要的就是我们的递归要怎么写,我们要怎么尝试,如果这个都写不出来,那么就没有以后了。

怎么尝试?

1)有经验但是没有方法论?

2)怎么判断一个尝试就是最优尝试?

3)难道尝试这件事真的只能拼天赋?那我咋搞定我的面试?

4)动态规划是啥?好高端的样子哦…可是我不会啊!和尝试有什么关系?

什么暴力递归可以继续优化?

有重复调用同一个子问题的解,这种递归可以优化 如果每一个子问题都是不同的解,无法优化也不用优化

暴力递归和动态规划的关系

某一个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划 任何动态规划问题,都一定对应着某一个有重复过程的暴力递归 但不是所有的暴力递归,都一定对应着动态规划

面试题和动态规划的关系

解决一个问题,可能有很多尝试方法 可能在很多尝试方法中,又有若干个尝试方法有动态规划的方式 一个问题   可能有   若干种动态规划的解法

如何找到某个问题的动态规划方式?

1)设计暴力递归:重要原则+4种常见尝试模型!重点!

2)分析有没有重复解:套路解决

3)用记忆化搜索 -> 用严格表结构实现动态规划:套路解决

4)看看能否继续优化:套路解决

面试中设计暴力递归过程的原则

1)每一个可变参数的类型,一定不要比int类型更加复杂

2)原则1)可以违反,让类型突破到一维线性结构,那必须是单一可变参数

3)如果发现原则1)被违反,但不违反原则2),只需要做到记忆化搜索即可

4)可变参数的个数,能少则少

知道了面试中设计暴力递归过程的原则,然后呢?

一定要逼自己找到不违反原则情况下的暴力尝试! 如果你找到的暴力尝试,不符合原则,马上舍弃!找新的! 如果某个题目突破了设计原则,一定极难极难,面试中出现概率低于5%!

常见的4种尝试模型

1)从左往右的尝试模型

2)范围上的尝试模型

3)多样本位置全对应的尝试模型

4)寻找业务限制的尝试模型

如何分析有没有重复解

列出调用过程,可以只列出前几层 有没有重复解,一看便知

暴力递归到动态规划的套路

1)你已经有了一个不违反原则的暴力递归,而且的确存在解的重复调用

2)找到哪些参数的变化会影响返回值,对每一个列出变化范围

3)参数间的所有的组合数量,意味着表大小

4)记忆化搜索的方法就是傻缓存,非常容易得到

5)规定好严格表的大小,分析位置的依赖顺序,然后从基础填写到最终解

6)对于有枚举行为的决策过程,进一步优化

动态规划的进一步优化

1)空间压缩

2)状态化简

3)四边形不等式

4)其他优化技巧

题目三 N皇后问题

N皇后问题是指在N*N的棋盘上要摆N个皇后, 要求任何两个皇后不同行、不同列, 也不在同一条斜线上 给定一个整数n,返回n皇后的摆法有多少种。 n=1,返回1 n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0 n=8,返回92

N皇后问题大家都知道,熟悉,所以就不举例了,众所周知,N皇后问题想要得到绝对的方法数,巨暴力,甚至于今天各个国家的超级计算机,测试超级计算机的性能,你的任务怎么特别好的划分,就是那N皇后问题测的。思路呢,就是我们考虑皇后的时候,我们一行一行的填皇后,我们搞一个皇后的轨迹信息,我们看第0行的皇后在哪,再看第一行的皇后在哪,再看第二行的皇后在哪。每一行只填一个皇后,这样我们就不用检查两个皇后是否共行的问题。

public static int num1(int n){
        if(n < 1){
            return 0;
        }
        int[] record = new int[n];
        return process(0,record,n);
    }

    //当前来到 i 行 ,一共是 0 - N - 1 行
    //在i 行上方皇后,所有的列都尝试
    //必须保证跟之前所有的皇后都不打架
    //int[] record  record[x] =  y  之前的x行皇后,都放在了y列
    //返回   不关心i之前发生了什么, i ... 后续有多少中方法数
    private static int process(int i, int[] record, int n) {
        if ( i == n){
            return 1;
        }
        int res = 0;
        // i 行的皇后放那一列呢  j列  全试
        for (int j = 0; j < n; j++) {
            if (isValid(record,i,j)){
                record[i] = j;
                res += process(i+1,record,n);
            }
        }
        return res;
    }

    private static boolean isValid(int[] record, int i, int j) {
        for (int k = 0; k < i; k++) {
            if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)){
                return false;
            }
        }
        return true;
    }

代码就是这么的短,简单吧,N皇后问题还有一个巨骚的操作,可以用来装逼,就是位运算的写法,位运算的写法和这种写法时间复杂度一样,仅仅是优化了常数时间,在随着皇后数的增多,位运算的形式个该形式消耗时间能差10倍之多。

我们是这样想的,我们是拿状态,一个整型值它的状态来替换record的某些东西,record是一个数组,如果用数组每回都寻址的话就会很慢,我怎么用整数来代替record,就成了N皇后问题优化的关键,假设我是7皇后问题,假设我准备好7个位置,现在我想想这我第0层的皇后我填哪,如下图:

假如说我把皇后放在x的位置,那么第一行的皇后列的限制是什么状态,什么意思,就是下一行会在?位置去填皇后,但是那些是绝对不能填的呢,列限制中画x的位置,那么0行x左下的限制就是如图中左下,没有,因为x左下它出去了,而对右下的限制如图中右下,为啥,因为皇后放在0行x位置,它的右下也就是1行第二个问号位置他是放不了皇后的,所以这些问号能选那些位置,列或上左下或上右下还是0的那些位置。然后继续选皇后,添加限制,很好玩吧。

//请不要超过32个皇后
    public static int  num2(int n){
        if (n < 1 || n > 32){
            return 0;
        }
        //如果你是13皇后问题,limit 最右13个 1  其他都是0
        // 如果是32 皇后  就是 32个 1  -1可以表示
        // 如果不是32皇后 ,那就把1左移皇后位 再减 1  就会变成 最右边皇后个 1
        //为啥要搞出limit 有用
        int limit = n == 32 ? -1:(1<<n)-1;
        return process2(limit,0,0,0);
    }

    //如果是 7 皇后问题  limit 永远都是
    //  limit :  0......0 1 1 1 1 1 1 1
    //  之前皇后的列影响    colLim
    //  之前皇后的左下对角线影响  LiftDiaLim
    //  之前皇后的右下对角线影响  LiftDiaLim
    //  对于这些参数,我只用状态,根本不用他们原始的值

    private static int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim) {
        //当我们发现我们的列限制能够等于limit
        //说明有一种解法。
        if (colLim == limit){
            return 1;
        }
        //pos 中所有是1的位置,是你可以去尝试皇后的位置
        int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
        //  列:  0-0 |   0 0 0 1 0 0 0
        //  左下:0-0 |   0 0 1 0 0 0 0
        //  右下:0-0 |   0 0 0 0 1 0 0
     //或完总限制:0-0 |   0 0 1 1 1 0 0
     // 在总限制里面 是 1 的你不能放皇后  是 0 的可以
// 总限制整体取反:1-1  |   1 1 0 0 0 1 1
   // limit 是 :0-0  |   1 1 1 1 1 1 1
   //与完之后   :0-0  |   1 1 0 0 0 1 1
        // 这样 1 就是所有可以放皇后的位置

        int mostRightOne = 0;
        int res = 0;
        while (pos != 0){
            //提取最右侧的 1
            // 假如说一个二进制数       00001001001000100
            // 提取最右侧的1后就会变成   00000000000000100
            mostRightOne = pos & (~pos + 1);
            pos = pos - mostRightOne;
            res += process2(
                    limit,
                    //已经把皇后点在mostRightOne位置
                    //增加列限制
                    colLim|mostRightOne,
                    (leftDiaLim | mostRightOne)<<1,
                    (rightDiaLim | mostRightOne)>>>1);
        }
        return res;
    }

怎么样,秀不秀,不一样的N皇后。拿去装逼。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值