admin管理员组文章数量:1565357
一、对数器找规律
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份青草统一堆放在仓库里
有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草
不管是牛还是羊,每一轮能吃的草量必须是:
1,4,16,64…(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+3,5就是这样的数
12 = 3+4+5,12就是这样的数
1不是这样的数,因为要求数量大于1个、连续正数和
2 = 1 + 1,2也不是,因为等号右边不是连续正数
给定一个参数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");
}
}
五、根据数据规模猜解法
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号怪兽的能力,你可以选择直接通过,你的能力并不会下降,你也可以选择贿赂这个怪兽,然后怪兽就会加入你,他的能力直接累加到你的能力上。
返回通过所有的怪兽,需要花的最小钱数。
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/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();
}
}
版权声明:本文标题:【算法&数据结构体系篇class38】对数器找规律、根据数据状况猜解法(续)、以及分治 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dianzi/1725586205a1031208.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论