本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~
一.使用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
。如果cur
是A
,那么enemy
就是B
,反之亦然。 - 处理基础情况(
rest < 5
):当剩余草量rest
小于5时,有以下几种情况。- 如果
rest = 0
,这意味着上一轮对手吃完了草,所以对手获胜,返回enemy
。 - 如果
rest = 2
,此时当前选手无论是A
还是B
,由于只能吃1或者4,无法吃完这2份草,下一轮对手可以吃完,所以返回enemy
。 - 如果
rest = 1
、rest = 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
。
- 从吃1份草(
- 确定对手:在
- 基本思路
-
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^k
,k
为整数),那么它的二进制表示只有一个1,例如2
的二进制是10
,4
的二进制是100
等。 - 当
num
不是2的幂次方时,num
和num - 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) 把规律变成代码,就是最优解了