41.【必备】对数器打表找规律的技巧

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解042【必备】对数器打表找规律的技巧_哔哩哔哩_bilibili

一.使用6,8规格的袋子装苹果

题目:

有装下8个苹果的袋子、装下6个苹果的袋子,一定要保证买苹果时所有使用的袋子都装满

对于无法装满所有袋子的方案不予考虑,给定n个苹果,返回至少要多少个袋子

如果不存在每个袋子都装满的方案返回-1

算法原理

  • ①bags1方法(基于递归)
    • 整体逻辑
      • bags1方法调用f方法来计算给定数量apple的结果。如果f方法返回Integer.MAX_VALUE,表示不存在每个袋子都装满的方案,此时bags1方法返回 -1;否则返回f方法的计算结果。
    • f方法原理
      • 基础情况
        • rest < 0时,说明之前的选择导致无法满足要求(苹果数量为负数),返回Integer.MAX_VALUE表示无效解。
        • rest = 0时,表示已经成功将所有苹果装袋,不需要更多袋子,返回0。
      • 递归计算
        • 对于当前剩余的苹果数量rest,分别考虑使用装8个苹果的袋子和装6个苹果的袋子两种情况。
        • 计算p1 = f(rest - 8),这表示先使用一个装8个苹果的袋子后,剩余苹果还需要的袋子数。如果p1不等于Integer.MAX_VALUE,说明这个选择是有效的,那么p1需要加1,表示已经使用了一个8规格的袋子。
        • 同理,计算p2 = f(rest - 6),并根据p2是否为有效解来决定是否加1。
        • 最后返回Math.min(p1, p2),选择使用两种袋子中能得到最少袋子数的方案。
  • bags2方法(基于规律)
    • 整体逻辑
      • 首先通过if ((apple & 1)!= 0)判断苹果数量apple是否为奇数。如果是奇数,由于8和6都是偶数,不可能存在每个袋子都装满的方案,所以直接返回 -1。
      • 然后针对苹果数量小于18的情况进行特殊处理:
        • apple = 0时,不需要袋子,返回0。
        • apple = 6或者apple = 8时,只需要1个袋子。
        • apple = 12(可以用2个6规格袋子)、apple = 14(1个8规格袋子和1个6规格袋子)或者apple = 16(2个8规格袋子)时,需要2个袋子。如果不在上述情况中,说明不存在装满袋子的方案,返回 -1。
      • 对于苹果数量apple >= 18的情况,使用公式(apple - 18) / 8+3来计算最少袋子数。这里的思路是先减去18(可以用2个6规格袋子和1个8规格袋子装满的苹果数),然后除以8得到还需要几个8规格的袋子,再加上已经使用的3个袋子(2个6规格袋子和1个8规格袋子)。

代码实现

// 有装下8个苹果的袋子、装下6个苹果的袋子,一定要保证买苹果时所有使用的袋子都装满
// 对于无法装满所有袋子的方案不予考虑,给定n个苹果,返回至少要多少个袋子
// 如果不存在每个袋子都装满的方案返回-1

//首先使用暴力递归打印所有情况进行找规律
public class Code01_AppleMinBags {

    public static int bags1(int apple) {
        int ans = f(apple);
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    // 当前还有rest个苹果,使用的每个袋子必须装满,返回至少几个袋子
    public static int f(int rest) {
        if (rest < 0) {
            return Integer.MAX_VALUE;
        }
        if (rest == 0) {
            return 0;
        }
        // 使用8规格的袋子,剩余的苹果还需要几个袋子,有可能返回无效解
        int p1 = f(rest - 8);
        // 使用6规格的袋子,剩余的苹果还需要几个袋子,有可能返回无效解
        int p2 = f(rest - 6);
        p1 += p1 != Integer.MAX_VALUE ? 1 : 0;
        p2 += p2 != Integer.MAX_VALUE ? 1 : 0;
        return Math.min(p1, p2);
    }

    //根据规律编写特定代码
    public static int bags2(int apple) {
        if ((apple & 1) != 0) {
            return -1;
        }
        if (apple < 18) {
            if (apple == 0) {
                return 0;
            }
            if (apple == 6 || apple == 8) {
                return 1;
            }
            if (apple == 12 || apple == 14 || apple == 16) {
                return 2;
            }
            return -1;
        }
        return (apple - 18) / 8 + 3;
    }

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

}

二.轮流吃草问题

题目:

草一共有n的重量,两只牛轮流吃草,A牛先吃,B牛后吃

每只牛在自己的回合,吃草的重量必须是4的幂,1、4、16、64....

谁在自己的回合正好把草吃完谁赢,根据输入的n,返回谁赢

算法原理

  • win1方法(基于递归的方法)
    • 基本思路
      • 该方法通过不断递归地模拟两只牛吃草的过程来判断谁会赢得游戏。
    • 递归函数f的分析
      • 确定对手:在f函数中,首先根据当前选手cur确定对手enemy。如果curA,那么enemy就是B,反之亦然。
      • 处理基础情况(rest < 5:当剩余草量rest小于5时,有以下几种情况。
        • 如果rest = 0,这意味着上一轮对手吃完了草,所以对手获胜,返回enemy
        • 如果rest = 2,此时当前选手无论是A还是B,由于只能吃1或者4,无法吃完这2份草,下一轮对手可以吃完,所以返回enemy
        • 如果rest = 1rest = 3或者rest = 4,当前选手可以直接吃完,所以返回cur
      • 处理递归情况(rest >= 5
        • 从吃1份草(pick = 1)开始,每次将pick乘以4,即尝试吃1、4、16、64……份草。
        • 对于每个pick值,计算f(rest - pick, enemy),这表示在当前选手选择吃pick份草后,对手在剩余rest - pick份草的情况下谁会赢。
        • 如果存在一个pick值使得f(rest - pick, enemy)的结果是当前选手cur赢,那么就返回cur
        • 如果遍历完所有可能的pick值(即pick从1开始,每次乘以4,直到pick > rest)都没有找到当前选手cur赢的情况,那么就返回enemy
  • win2方法(基于规律的方法)
    • 基本思路
      • 这种方法是通过对游戏过程进行分析,找出了根据草量n除以5的余数来判断谁赢的规律。
    • 规律分析
      • 通过对游戏过程的深入研究(可能是经过大量的测试或者数学推导),发现当n % 5 = 0或者n % 5 = 2时,B牛会赢。
      • n除以5的余数为1、3或者4时,A牛会赢。

代码实现

// 草一共有n的重量,两只牛轮流吃草,A牛先吃,B牛后吃
// 每只牛在自己的回合,吃草的重量必须是4的幂,1、4、16、64....
// 谁在自己的回合正好把草吃完谁赢,根据输入的n,返回谁赢
public class Code02_EatGrass {

    // "A"  "B"
    public static String win1(int n) {
        return f(n, "A");
    }

    // rest : 还剩多少草
    // cur  : 当前选手的名字
    // 返回  : 还剩rest份草,当前选手是cur,按照题目说的,返回最终谁赢
    public static String f(int rest, String cur) {
        String enemy = cur.equals("A") ? "B" : "A";
        if (rest < 5) {
            return (rest == 0 || rest == 2) ? enemy : cur;
        }
        // rest >= 5
        // rest == 100
        // cur :
        // 1) 1 ->99,enemy ....
        // 2) 4 ->96,enemy ....
        // 3) 16 -> 84,enemy ....
        // 4) 64 -> 36,enemy ...
        // 没有cur赢的分支,enemy赢
        int pick = 1;
        while (pick <= rest) {
            if (f(rest - pick, enemy).equals(cur)) {
                return cur;
            }
            pick *= 4;
        }
        return enemy;
    }

    public static String win2(int n) {
        if (n % 5 == 0 || n % 5 == 2) {
            return "B";
        } else {
            return "A";
        }
    }

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

}

三.判断一个数字是否是若干数量(数量>1)的连续正整数的和

题目:

判断一个数字是否是若干数量(数量>1)的连续正整数的和

算法原理

  • is1方法(基于双层循环的暴力解法)
    • 基本思路
      • 该方法通过双层循环来尝试找出是否存在若干连续正整数的和等于给定的数字num
    • 外层循环
      • 外层循环以start作为起始的正整数,从1开始,一直到num。对于每个start值,它将作为连续正整数序列的起始数字。
    • 内层循环
      • 内层循环从start + 1开始,每次将当前的数字j累加到sum中(初始sum = start)。
      • 如果sum + j大于num,说明再继续累加下去也不可能等于num,所以直接跳出内层循环。
      • 如果sum + j等于num,则说明找到了若干连续正整数的和等于num,返回true
      • 如果内层循环结束后都没有找到等于num的情况,那么外层循环继续尝试下一个start值,直到外层循环结束,若都没有找到则返回false
  • is2方法(基于位运算的高效解法)
    • 基本思路
      • 这种方法利用了位运算的特性来判断一个数字是否是若干数量(数量>1)的连续正整数的和。
    • 位运算原理
      • 对于一个数num,如果它是2的幂次方(即num = 2^kk为整数),那么它的二进制表示只有一个1,例如2的二进制是104的二进制是100等。
      • num不是2的幂次方时,numnum - 1进行按位与运算(num&(num - 1))的结果不为0。
      • 而一个数如果是若干数量(数量>1)的连续正整数的和,它不可能是2的幂次方(因为2的幂次方只能表示单个数字,不符合连续正整数数量>1的要求),所以通过判断(num&(num - 1))!=0来确定num是否是若干连续正整数的和。

代码实现

// 判断一个数字是否是若干数量(数量>1)的连续正整数的和
public class Code03_IsSumOfConsecutiveNumbers {

    public static boolean is1(int num) {
        for (int start = 1, sum; start <= num; start++) {
            sum = start;
            for (int j = start + 1; j <= num; j++) {
                if (sum + j > num) {
                    break;
                }
                if (sum + j == num) {
                    return true;
                }
                sum += j;
            }
        }
        return false;
    }

    public static boolean is2(int num) {
        return (num & (num - 1)) != 0;
    }

    public static void main(String[] args) {
        for (int num = 1; num < 200; num++) {
            System.out.println(num + " : " + (is1(num) ? "T" : "F"));
        }
    }
}

四.red拼出的字符串中好串数量问题

题目:

可以用r、e、d三种字符拼接字符串,如果拼出来的字符串中
有且仅有1个长度>=2的回文子串,那么这个字符串定义为"好串"
返回长度为n的所有可能的字符串中,好串有多少个
结果对 1000000007 取模, 1 <= n <= 10^9
示例:
n = 1, 输出0
n = 2, 输出3
n = 3, 输出18

算法原理

  • 暴力方法(num1函数)
    • 整体原理

      • 该方法通过穷举所有由'r'、'e'、'd'组成的长度为(n)的字符串,然后检查每个字符串是否满足“好串”的定义,即有且仅有1个长度(\geqslant2)的回文子串。
    • 具体步骤

      • 步骤一:递归构建字符串
        • 在(f)函数中,当(i < path.length)时,对于当前位置(i),依次将(path[i])设为'r'、'e'、'd',然后递归调用(f)函数处理下一个位置(i + 1)。例如,当(n = 3)时,首先将(path)设为'r',然后递归处理(path)和(path)的情况,接着再将(path)设为'e'重复这个过程,最后将(path)设为'd'再次重复。
      • 步骤二:检查回文子串数量
        • 当(i = path.length)时,开始检查回文子串数量。通过双层循环遍历字符串的所有子串,外层循环(l)从(0)开始,每次增加(1),直到(path.length - 1);内层循环(r)从(l + 1)开始,每次增加(1),直到(path.length - 1)。
        • 对于每个子串([l,r]),调用(is)函数来判断是否为回文。(is)函数通过双指针法,从子串的两端向中间比较字符,如果有不相等的字符,则不是回文,返回(false);如果一直比较到中间都相等,则是回文,返回(true)。
        • 在检查回文子串过程中,如果发现回文子串数量(cnt)大于(1),则立即返回(0),表示这个字符串不符合“好串”定义;如果(cnt = 1),则返回(1),表示是“好串”;如果(cnt = 0),则返回(0),表示不是“好串”。
      • 步骤三:计算总数
        • 在每次递归调用(f)函数后,将返回值累加到(ans)中。最后(ans)就是所有可能字符串中“好串”的个数。
  • 正式方法(num2函数)
    • 整体原理

      • 这种方法是基于对较小(n)值((n = 1)、(n = 2)、(n = 3))结果的分析,找到一个适用于(n\geqslant4)的通用公式来计算“好串”的个数,然后对结果进行取模操作。
    • 具体步骤

      • 步骤一:处理基础情况
        • 当(n = 1)时,由于单个字符无法构成长度(\geqslant2)的回文子串,所以直接返回(0)。
        • 当(n = 2)时,根据之前的分析或者其他方式得到“好串”的个数为(3),直接返回(3)。
        • 当(n = 3)时,同样根据之前的分析或者其他方式得到“好串”的个数为(18),直接返回(18)。
      • 步骤二:处理一般情况((n\geqslant4))
        • 对于(n\geqslant4),使用公式((6\times(n + 1)))来计算“好串”的个数。这个公式可能是通过对前面(n = 1)、(n = 2)、(n = 3)的结果进行数学归纳或者其他分析方法得到的,但代码中没有给出具体的推导过程。
        • 计算出结果后,使用((int)(((long)6*(n + 1))%1000000007))将结果对(1000000007)取模,得到最终答案。

代码实现

// 可以用r、e、d三种字符拼接字符串,如果拼出来的字符串中
// 有且仅有1个长度>=2的回文子串,那么这个字符串定义为"好串"
// 返回长度为n的所有可能的字符串中,好串有多少个
// 结果对 1000000007 取模, 1 <= n <= 10^9
// 示例:
// n = 1, 输出0
// n = 2, 输出3
// n = 3, 输出18
public class Code04_RedPalindromeGoodStrings {

    // 暴力方法
    // 为了观察规律
    public static int num1(int n) {
        char[] path = new char[n];
        return f(path, 0);
    }

    public static int f(char[] path, int i) {
        if (i == path.length) {
            int cnt = 0;
            for (int l = 0; l < path.length; l++) {
                for (int r = l + 1; r < path.length; r++) {
                    if (is(path, l, r)) {
                        cnt++;
                    }
                    if (cnt > 1) {
                        return 0;
                    }
                }
            }
            return cnt == 1 ? 1 : 0;
        } else {
            // i正常位置
            int ans = 0;
            path[i] = 'r';
            ans += f(path, i + 1);
            path[i] = 'e';
            ans += f(path, i + 1);
            path[i] = 'd';
            ans += f(path, i + 1);
            return ans;
        }
    }

    public static boolean is(char[] s, int l, int r) {
        while (l < r) {
            if (s[l] != s[r]) {
                return false;
            }
            l++;
            r--;
        }
        return true;
    }

    // 正式方法
    // 观察规律之后变成代码
    public static int num2(int n) {
        if (n == 1) {
            return 0;
        }
        if (n == 2) {
            return 3;
        }
        if (n == 3) {
            return 18;
        }
        return (int) (((long) 6 * (n + 1)) % 1000000007);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            System.out.println("长度为" + i + ", 答案:" + num1(i));
        }
    }

}

五.总结

1) 可以用最暴力的实现求入参不大情况下的答案,往往只需要最基本的递归能力

2) 打印入参不大情况下的答案,然后观察规律

3) 把规律变成代码,就是最优解了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值