【算法&数据结构体系篇class38】对数器找规律、根据数据状况猜解法(续)、以及分治

文章介绍了几个编程题目,包括苹果和青草问题、怪兽与金钱问题、子序列最大模值问题和牛牛的零食问题,探讨了如何通过暴力递归、动态规划和分治策略来优化解法,适应不同的数据规模。每个问题提供了多种解法,并通过实例验证了它们的正确性。
摘要由CSDN通过智能技术生成

一、对数器找规律

1)某个面试题,输入参数类型简单,并且只有一个实际参数

2)要求的返回值类型也简单,并且只有一个

3用暴力方法,把输入参数对应的返回值,打印出来看看,进而优化code

二、题目一

小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。

1)能装下6个苹果的袋子

2)能装下8个苹果的袋子

小虎可以自由使用两种袋子来装苹果,但是小虎有强迫症,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。

给定一个正整数N,返回至少使用多少袋子。如果N无法让使用的每个袋子必须装满,返回-1

package class38;

/**对数器找规律
 * 1)某个面试题,输入参数类型简单,并且只有一个实际参数
 *
 * 2)要求的返回值类型也简单,并且只有一个
 *
 * 3)用暴力方法,把输入参数对应的返回值,打印出来看看,进而优化code
 *
 *
 *
 * 小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。
 * 1)能装下6个苹果的袋子
 * 2)能装下8个苹果的袋子
 * 小虎可以自由使用两种袋子来装苹果,但是小虎有强迫症,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。
 * 给定一个正整数N,返回至少使用多少袋子。如果N无法让使用的每个袋子必须装满,返回-1
 */
public class AppleMinBags {
    //方法一: 直接分析解
    //要求使用袋子数最少,那么我们就先用能装8个的袋子 因为比6大,装的多 袋子就少
    //假设100个苹果  最多就是用 100/8=12个袋子
    //从12个 8袋子开始   剩余4个苹果   6袋子装不全  不符合
    //11个   8袋子       剩余12个     2个 6袋子     符合 且肯定是袋子数最少的 直接返回

   public static int minBags(int apple){
       if(apple < 0) return -1;    //苹果个数要大于0 否则无效 返回-1

       int bag8 = apple >> 3;   //8袋子  装满用了多少个袋子
       int rest = apple - (bag8 << 3);   //剩余的苹果数
       while(bag8 >= 0){
           if(rest % 6 == 0){     //如果当前剩余的苹果 刚好够6袋子装满 则退出循环 此时袋子就是最少的情况
               return bag8 + rest/6;  //返回8袋子数 和 6袋子数
           }
           bag8--;        //没有找到的话  8袋子 个数-1
           rest += 8;     //剩余个数+8
       }
       return -1;  //最后没有找到 返回-1
   }

   //方法二: 通过方法一暴力逻辑得出结果 得出的规律:
    //苹果奇数时 都是-1    因为6 和8都是偶数 怎么凑都凑不出奇数情况
    //1-17的苹果情况特殊 没有什么规律 直接根据结果赋值
    //18开始  8个一组  18-25  偶数个是3  奇数个是-1
    // 26-33  偶数个是4  奇数个是-1....
   public static int minBagAwesome(int apple){
       if((apple & 1) != 0) return -1;   //奇数 与运算1 结果就是1 非0 奇数返回-1

       if(apple < 18){
           //苹果数小于18  直接根据结果规律赋值返回
           return apple == 0 ? 0 : (apple == 6 || apple == 8) ? 1 :
                   (apple == 12 || apple == 14 || apple == 16) ? 2 : -1;
       }
       //苹果数大于17 从18开始  8个一组 偶数个第一组都是3  第一组18,25 -18   /8 = 0  +3 返回3个
       //第二组 26-33 -18 /8  = 1 +3 返回4个
       return (apple-18) / 8 + 3;
   }

   public static void main(String[] args){
       for(int apple = 1; apple < 200; apple++){
           System.out.println(apple + ":" + minBagAwesome(apple));
       }
   }
}

三、题目二

给定一个正整数N,表示有N份青草统一堆放在仓库里

有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草

不管是牛还是羊,每一轮能吃的草量必须是:

141664…(4的某次方)

谁最先把草吃完,谁获胜

假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定

根据唯一的参数N,返回谁会赢

package class38;

/**
 * 给定一个正整数N,表示有N份青草统一堆放在仓库里
 * 有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草
 * 不管是牛还是羊,每一轮能吃的草量必须是:
 * 1,4,16,64…(4的某次方)
 * 谁最先把草吃完,谁获胜
 * 假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定
 * 根据唯一的参数N,返回谁会赢
 */
public class EatGrass {
    //方法一:暴力递归
    //n份草 定义先手 后手    如果先手赢 返回先手 也就是 牛赢 ;
    // 后手赢 返回后手也就是羊赢
    public static String whoWin(int n){
        //获胜的条件是 最先把草吃完的一方:
        //n=0  先手到来 就没了 所以后手赢
        //N=1  那么 先手 吃 1份  N=0 没有了 先手赢
        //N=2  先手只能吃 1份  然后来到后手 吃1份 n=0 没有了 后手赢
        //n=3  先手1份 后手1份 先手1份 吃完了 先手赢
        //n=4  先手可以吃1份 4份 直接吃4份 吃完了 先手赢
        //base cae: 当n<5 我们设定特例 当然也可以继续往后推 我们这里就推到4
        //n=0  2时 后手赢
        if(n < 5){
            return n == 0 || n == 2 ? "后手" : "先手";
        }
        //如果大于等于5 那么先手先选 假设为1
        int want =1;
        while( want <= n){    //遍历先手取的情况 不能超过n
            // n-want 递归进去 表示当前先手选好了  到了后手时,进入后手的选择
            // 此时后手是作为本次的先手   而上一次的先手是作为本次的后手
            //如果进入的这个选择 是此时的后手赢了  那么就表示是上次的先手赢 直接返回 先手赢
            if(whoWin(n-want).equals("后手")){
                return "先手";
            }

            //如果先手没有赢 那么就分数*4 扩大
            //但是这里要注意整形大小溢出情况
            //在乘以4之前 判断 是否当前是小于等于 n/4的
            //如果大于 说明不能再扩大 会溢出 所以直接break
            if(want <= ( n / 4)){
                want *= 4;
            }else{
                break;
            }
        }
        //如果遍历先手情况 没有返回先手赢 那么最后就是后手赢 返回后手
        return "后手";
    }

    //方法二: 根据方法一  多个n样例得出的规律:
    //n从0开始    模以5 等于0  2 的位置 都是后手 其他都是先手
    public static String winner2(int n){
        if(n % 5 == 0 || n  % 5 == 2){
            return "后手";
        }else {
            return "先手";
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i <= 50; i++) {
            System.out.println(i + " : " + whoWin(i));
        }
    }

}

四、题目三

定义一种数:可以表示成若干(数量>1)连续正数和的数

比如:

5 = 2+35就是这样的数

12 = 3+4+512就是这样的数

1不是这样的数,因为要求数量大于1个、连续正数和

2 = 1 + 12也不是,因为等号右边不是连续正数

给定一个参数N,返回是不是可以表示成若干连续正数和的数

package class38;

/**
 * 定义一种数:可以表示成若干(数量>1)连续正数和的数
 * 比如:
 * 5 = 2+3,5就是这样的数
 * 12 = 3+4+5,12就是这样的数
 * 1不是这样的数,因为要求数量大于1个、连续正数和
 * 2 = 1 + 1,2也不是,因为等号右边不是连续正数
 * 给定一个参数N,返回是不是可以表示成若干连续正数和的数
 */
public class MSumToN {
    //方法一: 暴力解
    //根据题意 我们可以这样设计
    //目标值是num   我们从1开始
    // 我们从1开始 1+2+3... 如果加到 num 还没有  和是num的情况,连续和就已经大于num了 所以不存在1开始连续值的和为num  就退出
    // 接着2开始   2+3+4... 如果加到 num 还没有    继续退出
    // 3 开始      3+4+5...    一直循环  直到存在就返回true
    public static boolean isMSum1(int num){
        for(int start = 1; start <= num; start++){
            //连续正数 从1开始
            int sum = start;
            for(int next = start+1; next <= num; next++){
                if(sum + next > num){
                    break;   //如果累加到超过num 说明当前尝试的连续正数是不存在和是num的 直接退出当前尝试 来到下个start
                }
                if(sum + next == num){
                    return true;   //如果累加到和为 num 那么就表示存在连续正数和等于num 所以就直接返回true
                }
                sum += next;   //前面两种还没遇到 那么就表示累加和 还小于num  继续累加
            }
        }
        return false;    //最后如果没有找到 就返回false
    }

    //方法二: 根据方法一 跑出大量样本  得出结论:
    //当目标值是 2的某次方时  都是false  其余都是true
    //那么2的某次方就是 1 2 4 8 16...  这些数的二进制都有一个特征 就是只有一位是1
    //所以就是判断目标值num 二进制位上是否只有一位是1 其余位0  如果是则表示2的某次方的数 返回false
    //如何判断:  取出num的二进制数最右位的1  就是 num & (~num + 1)
    //比如   num= 4    0100
    //  取反 ~num      1011
    //   +1            1100
    //然后            & 0100
    //得到              0100  取出num的最右位的1  如果与num相等 那么就是表示num二进制只有一位1 也就是2的某次方的数 返回false
    public static boolean isMSum2(int num){
        return num != (num & (~num + 1));

        //也可以第二种思路判断:  就是 num-1 与 num 进行 与运算 如果是只有1位1 的 那么与结果就是0 比如:
        //   4      0100
        //   num-1  0011
        //    &num  0000  结果就是0  表示值是2的某次方  如果不止一位是1 那么&运算后肯定不是0
        //return num & ( num-1 ) != 0;
    }

    public static void main(String[] args) {
        for (int num = 1; num < 200; num++) {
            System.out.println(num + " : " + isMSum1(num));
        }
        System.out.println("test begin");
        for (int num = 1; num < 5000; num++) {
            if (isMSum1(num) != isMSum2(num)) {
                System.out.println("Oops!");
            }
        }
        System.out.println("test end");

    }
}

五、根据数据规模猜解法

1C/C++1秒处理的指令条数为108次方

2Java等语言,1~4秒处理的指令条数为108次方

3)这里就有大量的空间了!

 六、题目四

int[] dd[i]i号怪兽的能力

int[] pp[i]i号怪兽要求的钱

开始时你的能力是0,你的目标是从0号怪兽开始,通过所有的怪兽。

如果你当前的能力,小于i号怪兽的能力,你必须付出p[i]的钱,贿赂这个怪兽,然后怪兽就会加入你,他的能力直接累加到你的能力上;如果你当前的能力,大于等于i号怪兽的能力,你可以选择直接通过,你的能力并不会下降,你也可以选择贿赂这个怪兽,然后怪兽就会加入你,他的能力直接累加到你的能力上。

返回通过所有的怪兽,需要花的最小钱数。

package class38;

/**根据数据规模猜解法
 *1)C/C++,1秒处理的指令条数为10的8次方
 *
 * 2)Java等语言,1~4秒处理的指令条数为10的8次方
 *
 * 3)这里就有大量的空间了!
 *
 *
 * int[] d,d[i]:i号怪兽的能力
 * int[] p,p[i]:i号怪兽要求的钱
 * 开始时你的能力是0,你的目标是从0号怪兽开始,通过所有的怪兽。
 * 如果你当前的能力,小于i号怪兽的能力,你必须付出p[i]的钱,贿赂这个怪兽,然后怪兽就会加入你,他的能力直接累加到你的能力上;如果你当前的能力,大于等于i号怪兽的能力,你可以选择直接通过,你的能力并不会下降,你也可以选择贿赂这个怪兽,然后怪兽就会加入你,他的能力直接累加到你的能力上。
 * 返回通过所有的怪兽,需要花的最小钱数。
 */
public class MoneyProblem {
    /**
     *process1: 暴力递归 尝试
     * @param d        d[i]:i号怪兽的武力
     * @param p        p[i]:i号怪兽要求的钱
     * @param ability  当前你所具有的能力
     * @param index    来到了第index个怪兽的面前
     * @return   目前,你的能力是ability,你来到了index号怪兽的面前,如果要通过后续所有的怪兽,
     * 	请返回需要花的最少钱数
     */
    public static long process1(int[] d, int[] p, int ability, int index){
        //base case: 如果当前索引来到越界位置  表示没有怪兽 所以也就不能花钱 直接返回0
        if(index == d.length) return 0;

        //分析  如果index 是在区间内的 那么就看情况
        if(ability < d[index]){
            //1.当前怪兽能力大于目前能力值,需要进行贿赂  那么返回的就是 当前怪兽的要求前p[index] 以及来到index+1位置的怪兽需要的钱 此时能力就加上了d[index]
            return p[index] + process1(d,p,ability+d[index],index+1);
        } else{
            //2.当前怪兽能力小于等于目前能力值 那么既可以贿赂 花费增加p[index] 能力增加d[index]  也可以不贿赂 花费能力都不增加
            //那么两者 我们就取返回的花费钱数最小的一者
            return Math.min(
                    p[index] + process1(d,p,ability+d[index],index+1),
                    process1(d, p, ability, index + 1)
            );
        }
    }

    //方法一: 暴力递归 尝试
    //调用方法一递归函数 这种方式是当能力值是比较小的数据量的时候 我们采用的变量参数  根据题意要求的限定变量的大小定义
    //原数据 提供两个数组 :怪兽的武力  怪兽的花费
    public static long func1(int[] d, int[] p){
        //调用递归方法 当前能力0,位置0怪兽开始进行递归 通过全部怪兽后 返回最小的花费
        return process1(d,p,0,0);
    }

    //方法二: 针对方法一的尝试改 动态规划
    public static long func2(int[] d, int[] p){
        //分析递归方法中的变量参数 两个 能力值 范围是0-不超过d[]怪兽能力的全部累加和 ,索引 0-d.length
        int sum = 0; //定义全部怪兽的能力值和
        for(int i = 0; i < d.length; i++){
            sum += d[i];
        }
        //创建dp数组  index索引表示 行数   能力值表示列数
        int[][] dp = new int[d.length + 1][sum+1];
        //base case初始化  索引来到d.length 也就是最后一行数   其中都是0  数组默认值0  所以不用处理  最后一行已经完成初始化

        //接着分析情况依赖  索引个数范围内 也就是dp数组除了最后一行 的其他行 都进行分析处理
        //根据递归情况 index 依赖 index+1 也就是 前一行依赖后一行 目前最后一行已经填充好 接着就从倒数第二行开始往上填充
        for(int row = d.length-1; row >= 0; row--){
            for(int col = 0; col <= sum;col++){
                //从分析上得知 当前能力值col 也就是列值, 加上当前的怪兽的能力d[row] 最大是不超过 全部怪兽能力值的 因为起初本身能力值是0 累加最多就是全部怪兽能力值和 所以遇到这种情况 直接跳过,避免列值长度溢出异常  递归中也是不存在这种情况的
                if(col + d[row] > sum) {
                    continue;
                }

                //分析情况 能力值 col  小于 当前怪兽能力值 需要贿赂怪兽 那么花费就是需要加上 当前怪兽p[row] 以及其对应的下个怪兽的花费值
                if(col < d[row]){
                    //下个怪兽花费 row+1 下个位置  能力值 col当前的加上贿赂的当前怪兽的能力d[row]
                    dp[row][col] = p[row] + dp[row+1][col+d[row]];
                } else{
                    //能力值 col 大于等于 怪兽能力值
                    //可选择贿赂怪兽  和  不贿赂   取其花费较小值
                    dp[row][col] = Math.min(
                            p[row] + dp[row+1][col+d[row]],
                            dp[row+1][col]
                    );
                }
            }
        }
        //根据递归函数调用从 0位置开始 0能力值 所以返回dp[0][0]
        return dp[0][0];
    }


    /**
     *process2 第二种 暴力递归解法  参数是用 花费钱  在能力数组的设定范围特别大的时候,用能力变量的解法进行创建数组可能数组会超级大 导致计算会超时 我们需要换 花费值做变量
     * @param d        d[i]:i号怪兽的武力
     * @param p        p[i]:i号怪兽要求的钱
     * @param index  来到了第index个怪兽的面前
     * @param money     从0...index号怪兽 通过要花的费用 必须是等于这个值
     * @return       通过不了的返回-1  能通过的就返回最大能力值
     */
    public static long process2(int[] d, int[] p, int index,int money){
        //base case:   index是递减的趋势,因为函数规定是返回0---index的含义 所以是需要入参是 index就是全部怪兽个数 然后递减 如果减到没有怪兽  金额是0的情况下 就返回能力值0就可以  否则就是-1 因为没有怪兽 是不需要花费的
        if(index == -1){
            return money == 0 ? 0 : -1;
        }

        //分析情况 1.不贿赂当前怪兽 那么当前的的最大能力值如果存在,需要0-index-1怪兽的最大能力值要大于等于当前的怪兽的能力值,花费不变
        long p1 = -1;//假设是不成立的
        long ans1 = process2(d,p,index-1,money);
        //注意判断 如果不贿赂 能成功通过 是需要ans1 返回值不等于-1 也就是是存在最大能力的情况下, 并且0--index-1的怪兽通过情况下的最大能力值ans1要大于等于 index的怪兽的能力的 再把p1进行赋值
        if(ans1 != -1 && ans1 >= d[index]){
            p1 = ans1;
        }

        //情况 2.贿赂当前怪兽  那么当前的的最大能力值如果存在 就是0-index-1的最大能力值+当前怪兽d[index]   那么0-index-1的怪兽 就要减去当前怪兽的花费
        long p2 = -1; //假设不成立
        long ans2 = process2(d,p,index-1,money-p[index]);
        //注意判断 如果贿赂 能成功通过 就需要ans2返回值不等于-1 就表示是存在的最大能力的 其能力 肯定是符合的 就返回并加上 当前怪兽的能力d[index]
        if(ans2 != -1){
            p2 = d[index] + ans2;
        }
        return Math.max(p1,p2);   //最后返回他们两者依赖情况的最大值返回
    }

    //方法三: 暴力递归 尝试
    //调用process2递归函数 这种方式是当花费值是比较小的数据量的时候 我们采用的变量参数  根据题意要求的限定变量的大小定义
    //原数据 提供两个数组 :怪兽的武力  怪兽的花费
    public static long minMoney2(int[] d, int[] p){
        //先求出 全部通过的怪兽 最多需要的花费
        long  allMoney = 0;
        for(int i = 0; i < p.length; i++){
            allMoney += p[i];
        }
        int n = d.length;   //定义怪兽的个数
        //调用递归函数 是返回的 最大能力值, 题目要求的是 最小金额 那我们就从小到大 遍历 金额数 依次入参到函数中
        //如果存在返回的最大能力值不等于-1 也就是存在 那么我们就直接返回 其当前遍历的金额数 就是最小金额数了
        for(int i = 0; i < allMoney; i++){
            //这里n-1 表示每次都是传全部的怪兽个数 最后的索引是n-1 递归函数依次向前递减
            if(process2(d,p,n-1,i) != -1){
                return i;
            }
        }
        return allMoney;  //最后如果没有存在情况 说明就是需要贿赂全部怪兽得到的花费 返回allMoney
    }



    //针对方法三做的 优化 动态规划
    public static long func3(int[] d, int[] p){
        //分析变量 index 索引-1 , d.length-1  负数忽略    金额 0, p[]数组和
        int sum = 0;
        for(int i = 0; i < p.length; i++){
            sum += p[i];
        }
        int[][] dp = new int[d.length][sum+1];
        //base case 当index 负数 这里已经忽略负数范围 不做处理  然后根据递归分析 假设都是-1 情况下 初始化-1
        // dp[i][j]含义:
        // 能经过0~i的怪兽,且花钱为j(花钱的严格等于j)时的武力值最大是多少?
        // 如果dp[i][j]==-1,表示经过0~i的怪兽,花钱为j是无法通过的,或者之前的钱怎么组合也得不到正好为j的钱数
        for(int i = 0; i < d.length; i++){
            for(int j = 0; j <= sum; j++){
                dp[i][j] = -1;
            }
        }
        //接着需要先把第一行填充好 因为递归是后一行依赖上一行 从第一个开始的时候0号怪兽肯定需要贿赂 并且金额就是对应的p[i],其最大能力就是怪兽的能力d[0]
        // 其他列的金额数肯定都不是正好的钱数都是-1
        dp[0][p[0]] = d[0];

        //接着从第二行开始 填充dp数组 开始遍历
        for(int i = 1; i < d.length;i++){
            for(int j = 0; j <= sum; j++){
                //分析情况 1.不贿赂当前怪兽 那么当前的的最大能力值如果存在,需要0-index-1怪兽的最大能力值要大于等于当前的怪兽的能力值,花费不变
                //前提就是dp[i-1][j]的最大能力值 要大于等于当前怪兽能力值d[i]
                if(dp[i-1][j] >=d[i]){
                    dp[i][j] = dp[i-1][j];
                }

                //情况 2.贿赂当前怪兽  那么当前的的最大能力值如果存在 就是0-index-1的最大能力值+当前怪兽d[index]   那么0-index-1的怪兽 就要减去当前怪兽的花费
                //前提就是dp[i-1][j-p[i]] 0-index-1 怪兽 花费是 j- 当前怪兽花费p[i] 是存在最大能力值的   还需要先判断j-p[i]不能越界 避免溢出异常

                if(j-p[i]>=0 && dp[i-1][j-p[i]] != -1){
                    //如果存在 那么就和情况1 两者进行比较 较大值就是当前i j 的最大能力值   注意这里 比较的最大能力值dp[i-1][j-p[i]] 是0-index-1范围的 需要 + 当前怪兽的能力值 d[i]才是表示当前的最大能力值
                    dp[i][j] = Math.max(dp[i][j], dp[i-1][j-p[i]]+d[i]);
                }
            }
        }

        int ans = 0; //定义结果 最小花钱数
        // dp表最后一行上,dp[N-1][j]代表:
        // 能经过0~N-1的怪兽,且花钱为j(花钱的严格等于j)时的武力值最大是多少?
        // 那么最后一行上,最左侧的不为-1的列数(j),就是答案 表示通过全部怪兽的最小金额
        for(int j = 0 ; j <= sum; j++){
            if(dp[d.length-1][j] != -1){
                ans = j;
                return ans;
            }
        }
        return ans;
    }


    public static int[][] generateTwoRandomArray(int len, int value) {
        int size = (int) (Math.random() * len) + 1;
        int[][] arrs = new int[2][size];
        for (int i = 0; i < size; i++) {
            arrs[0][i] = (int) (Math.random() * value) + 1;
            arrs[1][i] = (int) (Math.random() * value) + 1;
        }
        return arrs;
    }

    public static void main(String[] args) {
        int len = 10;
        int value = 20;
        int testTimes = 10000;
        for (int i = 0; i < testTimes; i++) {
            int[][] arrs = generateTwoRandomArray(len, value);
            int[] d = arrs[0];
            int[] p = arrs[1];
            long ans1 = func1(d, p);
            long ans2 = func2(d, p);
            long ans3 = func3(d, p);
            long ans4 = minMoney2(d,p);
            if (ans1 != ans2 || ans2 != ans3 || ans1 != ans4) {
                System.out.println("oops!");
            }
        }

    }


}

七、根据数据状况猜解法(续)、以及分治

面试中的分治的应用场景:

1,数据量整体做尝试可能性太多了,跑不完

2,数据分成多个块(常见是两块)之后,各自的可能性并不算多

3,合并多个块各自信息的整合过程不复杂

八、题目一

给定一个非负数组arr和一个正数m

返回arr的所有子序列中累加和%m之后的最大值。

package class39;

import java.util.HashSet;
import java.util.TreeSet;

/**
 * 根据数据状况猜解法(续)、以及分治
 * 面试中的分治的应用场景:
 * 1,数据量整体做尝试可能性太多了,跑不完
 * 2,数据分成多个块(常见是两块)之后,各自的可能性并不算多
 * 3,合并多个块各自信息的整合过程不复杂
 * <p>
 * 给定一个非负数组arr,和一个正数m。
 * 返回arr的所有子序列中累加和%m之后的最大值。
 */
public class SubsquenceMaxModM {

    /**
     * 假设m值很大 无法参与用变量逻辑避免超时  数组值和长度都不大 所以就用数组值做变量递归
     * process函数: 将arr数组中的全部子序列累加和都添加到set集合中
     *
     * @param arr   目标数组
     * @param index 数组索引
     * @param sum   子序列和
     * @param set   集合 保存每个子序列(不连续)的累加和 去除重复
     */
    public static void process(int[] arr, int index, int sum, HashSet<Integer> set) {
        //base case: 如果索引越界了 表示递归到能到的最后一个 那么就给集合进行添加当前的子序列累加和
        if (index == arr.length) {
            set.add(sum);
        } else {
            //如果还没越界 那么index位置 可取 或 不取 两者进行递归
            process(arr, index + 1, sum, set);   //不取,sum则保存不变    index来到下个位置
            process(arr, index + 1, sum + arr[index], set);   //取 sum则加上当前位置的值  index来到下个位置
        }
    }

    //主函数: 调用process方法递归
    public static int max1(int[] arr, int m) {
        //定义一个有序集合 hashset
        HashSet<Integer> set = new HashSet<>();
        //调用递归 将全部子序列 保存到set
        process(arr, 0, 0, set);
        //定义一个变量保存最大值
        int max = 0;
        for (int i : set) {
            //遍历每个子序列 模以m 比较得到最大值
            max = Math.max(max, i % m);
        }
        return max;
    }

    //针对max1: 优化动态规划   经典背包问题 布尔类型 dp[i][j] 0...i-1能不能凑到j的值  能则true
    public static int max2(int[] arr, int m){
        //变量的范围  sum  :0- arr数组求和   index 0-arr.length
        int n = arr.length;
        int sum = 0;
        for(int i = 0; i < n; i++){
            sum+= arr[i];
        }
        //定义dp数组dp[i][j] 表示 0..i-1 的子序列 累加和是否刚好为j 是就是true
        boolean[][] dp = new boolean[n][sum+1];
        //初始化: 在n行中 每一种都存在空子序列 凑齐j=0 的情况  ;
        for(int i = 0; i < n; i++){
            dp[i][0] = true;
        }
        //另外当只有一个值的子序列时 也就是第一行 i=0  这一行中 对应相等值的列arr[0]  也是true
        dp[0][arr[0]] = true;

        //此时第一行第一列都填充好 接着就从第二行第二列开始填充
        for(int i = 1; i < n; i++){
            for(int j = 1; j <= sum; j++){
                //情况1: 当前的值不选, 那么其值就等于 前一个值i-1 是否存在能累加和到j
                dp[i][j] = dp[i-1][j];

                //情况2: 当前的值选   那么其值就等于 前一个值i-1 是否存在能累加和到j-arr[i] 然后与情况1 进行或运算 符合其一即可
                //注意数组越界 做判断
                if(j >= arr[i]){
                    dp[i][j] |= dp[i-1][j-arr[i]];
                }
            }
        }
        //dp数组已经填充好 那么要求arr[]数组在全部子序列累加和 也就是在dp数组最后一整行 这里面找,对应累加和j列是true的j值
        //对这些j值 进行模以m 取出最大值 返回
        int max = 0;
        for(int j = 0; j <= sum; j++){
            if(dp[n-1][j]){
                max = Math.max(max,j % m);
            }
        }
        return max;
    }

    /**
     * 假设数组值很大  无法参与用变量逻辑避免超时  m不大 所以就用m做变量递归
     *
     * @param arr 目标数组
     * @param m    模以m值
     * @return    返回最大模以m的值
     */
    public static int max3(int[] arr, int m){
        //dp[i][j] 0,i-1 的子序列 模以m 后的值是否为j 是则为true
        //那么j 的范围就是0- m-1  任何数模以m 后范围就是0 -  m-1
        int n = arr.length;
        boolean[][] dp = new boolean[n][m];

        //初始化dp数组 0-i-1的全部子序列 都有个空序列 模以m值为0 所以赋值true
        for(int i = 0; i < n; i++){
            dp[i][0] = true;
        }
        //i=0 其值模以m 的值arr[0]%m 在对应的列  值就是true
        dp[0][arr[0]%m] = true;

        //已把第一列第一行填充完 然后接着第二行第二列开始填充
        for(int i = 1; i < n; i++){
            for(int j = 1; j < m; j++){
                //分析情况1. 当前i位置 值不选 那么就等同于 i-1位置的值情况 同个j列
                dp[i][j] = dp[i-1][j];

                //分析情况2. 当前i位置 值选
                //那么就需要判断 arr[i] % m的情况
                int cur = arr[i] % m;
                // 2-1.当前i位置的 arr[i]%m 值 是小于等于m-1的 也就是 小于等于j
                // 那么就等同于 i-1位置  子序列和 模以m 等于 j-cur  这个位置为true就表示dp[i][j]也是可得到的
                if(cur <= j){
                    dp[i][j] |= dp[i-1][j-cur];
                } else{
                    //2-2 arr[i]%m 是大于j的情况下 那么就可以多加m 相当于多加一圈再模以m 也是能使得
                    dp[i][j] |= dp[i-1][m+j-cur];
                }
            }
        }

        //dp填充完成 题目要求 0- i-1  取子序列 模完m 的最大值
        //也就是来到最后一行n-1 最右边的是true的值  其j值就是模后的最大值了
        int ans = 0;
        for(int j = 0; j < m; j++){
            if(dp[n-1][j]){
                ans = j;
            }
        }
        return ans;
    }

    //数据情况:假设数组值 m都很大 都不适合做分析  长度有限 相对不大
    // 如果arr的累加和很大,m也很大
    // 但是arr的长度相对不大   进行长度的二分法 分治思想
    public static int max4(int[] arr, int m) {
        //base case 如果长度只有一个 那么结果就是直接模m
        if (arr.length == 1) {
            return arr[0] % m;
        }

        //二分 求中心点
        int mid = (arr.length - 1) / 2;
        //定义一个有序集合 去重 保存左边数组
        TreeSet<Integer> sortSet1 = new TreeSet<>();
        //调用函数递归 从0位置开始到mid  和也是0开始
        process4(arr, 0, 0, mid, m, sortSet1);
        TreeSet<Integer> sortSet2 = new TreeSet<>();
        //定义第二个集合 保存右边数组 mid+1, arr.length-1
        process4(arr, mid + 1, 0, arr.length - 1, m, sortSet2);
        int ans = 0;
        for (Integer leftMod : sortSet1) {
            //最后将左数组集合 依次取出
            //与右数组匹配相加得到一个最终模m的值
            // 而右数组 是要找一个与左数组 +起来后 模m  那么范围就是在<=m-1 且最接近的 那么就是m-1-left
            //依次比较每次的左右数组匹配的最大值 返回
            ans = Math.max(ans, leftMod + sortSet2.floor(m - 1 - leftMod));
        }
        return ans;
    }

    // 从index出发,最后有边界是end+1,arr[index...end]
    public static void process4(int[] arr, int index, int sum, int end, int m, TreeSet<Integer> sortSet) {
        if (index == end + 1) {
            //base case:越界后 就开始将当前的sum累计和模m的值入 集合中
            sortSet.add(sum % m);
        } else {
            //分析情况  不取当前的索引值      取当前的索引值
            process4(arr, index + 1, sum, end, m, sortSet);
            process4(arr, index + 1, sum + arr[index], end, m, sortSet);
        }
    }

    public static int[] generateRandomArray(int len, int value) {
        int[] ans = new int[(int) (Math.random() * len) + 1];
        for (int i = 0; i < ans.length; i++) {
            ans[i] = (int) (Math.random() * value);
        }
        return ans;
    }
    public static void main(String[] args) {
        int len = 10;
        int value = 100;
        int m = 76;
        int testTime = 500000;
        System.out.println("test begin");
        for (int i = 0; i < testTime; i++) {
            int[] arr = generateRandomArray(len, value);
            int ans1 = max1(arr, m);
            int ans2 = max2(arr, m);
            int ans3 = max3(arr, m);
            int ans4 = max4(arr, m);
            if (ans1 != ans2 || ans2 != ans3 || ans3 != ans4) {
                System.out.println("Oops!");
            }
        }
        System.out.println("test finish!");

    }
}

九、题目二

牛牛家里一共有n袋零食, i袋零食体积为v[i],背包容量为w

牛牛想知道在总体积不超过背包容量的情况下,

一共有多少种零食放法,体积为0也算一种放法

1 <= n <= 30, 1 <= w <= 2 * 10^9

v[i] (0 <= v[i] <= 10^9)

以下代码是不考虑变量的限定数量范围,也就是 利用w 背包容量做dp数组列 常规的做法 ,

下面再展示原题,需要结合数据范围的做法 利用n长度 进行分治思路做法:

package class39;

/**
 * 牛牛家里一共有n袋零食, 第i袋零食体积为v[i],背包容量为w。
 * 牛牛想知道在总体积不超过背包容量的情况下,
 * 一共有多少种零食放法,体积为0也算一种放法
 * 1 <= n <= 30, 1 <= w <= 2 * 10^9
 * v[i] (0 <= v[i] <= 10^9)
 *
 * 以下解法是根据w背包容量做的dp  实际上w最大能到 10的9次方 java运行是会超时的
 * 所以先不考虑数据量的边界 在SnacksWaysMain类中的解法就是考虑样本量的情况的解法 分治
 */
public class SnacksWays {
    /**
     * 暴力递归:数组arr[index..]自由选择,不超过当前剩余背包容量rest 返回符合的方法数
     * @param arr
     * @param index  下标索引
     * @param rest    容量w所剩余量
     * @return
     */
    public static int process(int[] arr, int index, int rest){
        //base case:如果rest容量小于0 表示没有空间 就不能放 方法数返回0
        if(rest < 0) return 0;

        //base case:如果index已经遍历完 越界 也就是没有零食可选 没有零食可选也作为一种方法 返回1
        if(index == arr.length) return 1;

        //分析情况 rest还有剩余容量  零食index也还有
        //判断当前位置零食 取 或者不取 两者的方法数相加
        // 当前零食不放  那么就表示index+1下个零食的位置 rest容量的 放法
        int p1 = process(arr, index+1, rest);
        //当前零食放 那么index+1下个零食位置 rest容量-当前零食体积arr[index]
        int p2 = process(arr, index + 1, rest-arr[index]);

        //两种情况都是结果放法数 进行累加 p2因为可能存在无效方法 可能返回-1 需要判断 如果有效就直接返回p2 否则就返回0
        return p1 + p2;
    }

    //主函数 调用递归  0位置开始 容量w
    public static int ways1(int[] arr, int w){
        return process(arr, 0, w);
    }

    //改进动态规划
    public static int ways2(int[] arr, int w){
        //可变参数 index 位置  w容量
        int n = arr.length;
        int[][] dp = new int[n+1][w+1];
        //base case 最后一行 值为1
        for(int j = 0; j <= w ; j++){
            dp[n][j] = 1;
        }

        //依赖分析
        for(int i = n-1; i >= 0; i--){
            for(int j = 0; j <= w; j++){
                //两种情况之和  放零食 与 不放 不放则剩余容量j-arr[i]要先判断大于等于0 避免溢出异常
                dp[i][j] = dp[i+1][j] + ( (j - arr[i])>=0? dp[i+1][j-arr[i]] : 0);
            }
        }
        return dp[0][w];  //返回递归入参开始的位置
    }


    //改写 经典背包问题 dp[i][j] 从0...i-1 位置 在j容量的前提 零食放法有多少种
    public static int ways3(int[] arr, int w){
        //dp数组  i行表示 索引   j列表示容量
        int n = arr.length;
        int[][] dp = new int [n][w+1];

        //base case: 在容量0的情况下 0..n-1全部零食中 都存在不放零食 容量0满足的情况 赋值1
        for(int i = 0; i < n; i++){
            dp[i][0] = 1;
        }
        //base case: 当 0..0 在dp[0] 第一个零食 容量列刚好就是 arr[0]体积值 满足该情况
        //前提就是 arr[0] 需要小于等于总的容量w
        if(arr[0] <= w){
            dp[0][arr[0]] = 1;
        }

        //分析情况 当前零食 取 与 不取   依赖的是 0--i-1 前一个零食的取选数
        for(int i = 1; i < n; i++){
            for(int j = 1; j <= w; j++){
                //不取 那么方法数就等同于前面i-1的方法数dp[i-1][j] j容量不变
                //取   方法数等同于前面i-1方法数dp[i-1][j-arr[i]] j容量因为当前i零食取了 那么i-1的容量就要减去其体积
                dp[i][j] = dp[i-1][j] + ((j-arr[i]>=0)?dp[i-1][j-arr[i]]:0);
            }
        }
        //dp填充完成 那么求0...i-1 任意零食的放法数 <=容量w等情况下
        //也就是 最后一行n-1 每一列的值进行累加
        int ans = 0;
        for(int j = 0; j <= w; j++){
            ans +=dp[n-1][j];
        }
        return ans;  //最后累加后返回
    }

    public static void main(String[] args) {
        int[] arr = { 4, 3, 2, 9 };
        int w = 100;
        System.out.println(ways1(arr, w));
        System.out.println(ways2(arr, w));
        System.out.println(ways3(arr, w));

    }

}

[编程题]牛牛的背包问题

 

package class39;


import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;

/**
 * //本文件是Code02_SnacksWays问题的牛客题目解答
 * //但是用的分治的方法
 * //这是牛客的测试链接:
 * //https://www.nowcoder.com/questionTerminal/d94bb2fa461d42bcb4c0f2b94f5d4281
 * //把如下的全部代码拷贝进编辑器(java)
 * //可以直接通过
 *
 * 牛牛家里一共有n袋零食, 第i袋零食体积为v[i],背包容量为w。
 * 牛牛想知道在总体积不超过背包容量的情况下,
 * 一共有多少种零食放法,体积为0也算一种放法
 * 1 <= n <= 30, 1 <= w <= 2 * 10^9
 * v[i] (0 <= v[i] <= 10^9)
 */
public class SnacksWaysMain {

    //根据题目要求 w容量 v体积 数量值大 超过10的8次方的情况下 再做dp数组是会超时的
    //那么通过n 数组的个数 也就是零食的个数 较小 我们作为切入点分析
    //放零食 也就是每个零食2种选择 放 不放 那么30个就有 2^30次方 这样直接遍历也会超时
    //所以我们进行二分思想分治 进行拆分左右两边 各15个,  那么每侧遍历就是 2^15次方 这个数就远远小于 10^8次方 不会超时

    /**
     * 递归函数 返回 arr数组 从index开始到 end 结束的范围 零食,在bag容量内的零食放法数  之前的零食选择已经形成了累加的体积和w,不能超过bag
     * @param arr
     * @param index  遍历的起始索引
     * @param w      累加前面已经选择了的 零食体积和w 不能超过bag容量
     * @param end    遍历零食的终止索引
     * @param bag    背包容量 零食放进来的体积量要小于等于bag
     * @param map    有序表 按序保存 零食选取后的体积和 : 对应的方法数
     *               比如 arr[3,3,3,3] bag = 6
     *               一开始都不选: 也是一种方法 体积就是0,方法数1  map:{0: 1}
     *               接着选 arr[0]   3<6是满足的 符合 体积就是3 方法数返回1   此时 map: {0:1},{3:1}
     *               接着选 arr[1]   3<6是满足的 符合 体积就是3 方法数返回1   此时 map: {0:1},{3:2} 因为体积和3 的key存在 就是将value累加...
     * @return
     */
    public static long process(int[] arr, int index, long w, int end, int bag, TreeMap<Long,Long> map){
        //base case: 当w累计放零食的体积和大于bag 那么就是无效的放法 返回0
        if(w > bag){
            return 0;
        }
        //base case: 当范围零食都已经选择完了 越界情况  前提就是w<=bag 容量允许的前提 不满足的话就会走第一个判断分支
        if(index > end){
            if(w != 0){
                //如果选择了零食的情况 也就是w体积是大于0的 那么就表示符合零食能放到背包才能走到这里的判断 返回方法数1
                //并且将 其体积和w 添加到有序表中 如果存在相同的体积记录 那么就是刷新value
                if(map.containsKey(w)){
                    map.put(w, map.get(w)+1);
                } else {
                    map.put(w,1L);
                }
                return 1;
            } else{
                //如果w==0 表示没有选择零食 在这里递归函数中 我们定义 至少要选择零食  没有选择的是无效的 返回0
                // 这个不选零食的方法,实际题意是允许的 我们在上层调用递归最后在结果+1
                return 0;
            }
        }

        //分析情况 当前 index<=end 存在还有零食, 且体积 w 在背包bag容量内
        //1.当前零食 不选  那么其方法数就等同于 下个位置index+1 的零食方法数
        long p1 = process(arr,index+1,w,end,bag,map);
        //2.当前零食  选择   等同于 下个位置index+1零食方法 , w体积就要累计当前选择的零食
        long p2 = process(arr,index+1,w+arr[index], end, bag , map);

        return p1+p2;
    }

    //调用递归函数 传递目标数组arr, 背包容量 bag ,返回全部的零食放法
    public static long ways(int[] arr, int bag){
        //base case: 无效入参 返回0
        if(arr == null || arr.length == 0) return 0;
        if(arr.length == 1){
            //长度为1 只有一个零食时,那么方法 至少1个 就是不选 其次如果bag大于等于零食体积 可选 就有2个
            return (arr[0] <= bag) ? 2 : 1;
        }

        //接着进行目标数组的 二分操作 mid作为中点
        int mid = (arr.length -1) >> 1;
        //定义左侧数组 对应存放 零食存放的方法种树到lmap有序表内
        TreeMap<Long,Long> lmap = new TreeMap<>();
        //调用递归函数 从数组0,mid区间 取零食进行判断 体积从0开始 容量bag 有序表lmap  最后返回全部的方法数, lmap按序保存 零食体积和:方法数的 记录
        long ways = process(arr,0,0,mid,bag,lmap);

        //右侧数组同理 范围:mid+1,arr.length-1  将方法数累加到ways中
        TreeMap<Long,Long> rmap = new TreeMap<>();
        ways += process(arr,mid+1,0,arr.length-1,bag,rmap);

        //左右两边各自的方法数已经求好,接着还有一种情况,就是左侧的零食和右侧的零食同时选中的情况 那么就是要结合 l r两边的情况做分析
        //我们按左边保存的方法数 lmap 为结果  依次找 右侧符合与lmap能匹配的方法数
        //思路如下: 假设bag = 8
        // lmap中有 3:2 记录,表示体积和3的零食 有2种方法入背包 那么右侧中 可以找 是否存在 <=8-3 的体积和有方法入背包的
        // 假设存在 rmap中有 1:3 2:1  4:2 5:3  6:4 那么其中符合的key就是 1 2 4 5  因为<=5 就可以和3 组合 总共>=8背包
        // lmap有2种  rmap有 3+1+2+3=9种  交叉配对 就是2*9 = 18种    依次找出每个lmap的值对应在右边存在其小于等于bag-lmap的key 的全部方法数 两者相乘 依次累加到ways中

        //根据分析思路 我们先要定义一个新的有序表 保存 rmap中的 <=key 的方法数 就是将value遍历累加
        //假设rmap= {2:1},{4,3},{5,3}..key是有序的升序  依次累加 =>转换得到 rpre {2:1} {4:1+3} {5: 1+3+3}..
        TreeMap<Long,Long> rpre = new TreeMap<>();
        long pre = 0;
        for(Map.Entry<Long,Long>entry : rmap.entrySet()){
            pre += entry.getValue();   //value值累加
            rpre.put(entry.getKey(), pre);
        }

        //开始遍历lmap 以其中每个key 也就是零食体积和 依次匹配rpre
        for(Map.Entry<Long,Long>entry: lmap.entrySet()){
            long lw = entry.getKey();
            long lways = entry.getValue();
            Long rw = rpre.floorKey(bag - lw);   //如前面分析 找出右边数组对应的 <=bag-lw左零食体积和  最接近的一个key值
            if(rw != null){
                //判断是否存在这个最接近的且 <=bag-lw的key 注意rw要用Long 包装类 才能判断是否null值
                long rways = rpre.get(rw);
                ways += lways*rways;
            }
        }
        return ways + 1 ;   //最后返回ways+1  1这种方法是 全部零食不选的情况 也是符合的 在递归函数中是不处理这种不选的情况的 所以要加上
    }


    public static void  main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();    //输入 n  数组零食的长度
        int bag = sc.nextInt();  //输入 bag 背包容量
        int[] arr = new int[n];  //初始化数组
        for(int i = 0; i < n; i++){
            arr[i] = sc.nextInt();  //依次输入每个零食的体积值
        }
        //完成好输入数组 arr, bag  接着调用 ways方法 或者对应的零食取法数
        long ways = ways(arr,bag);
        System.out.println(ways);
        sc.close();

    }


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值