引言
打表法的概念:
打表是一种用空间换时间的技巧,一般指将所有可能需要用到的结果事先计算出来,这样后续可以直接通过查表获取。打表常见的用法有以下几种:
- 在程序中一次性计算出所有需要用到的结果,之后的查询直接取这些结果。
- 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。
- 对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”。
本篇文章主要介绍第三种,当遇到一头雾水的题目,毫无章法可言,并且输入为简单整数,输出为简单整数。通过尝试多个数找到规律,直接写答案。
苹果装袋问题
题目:给定两种规格的袋子:6 和 8,小明准备买 个苹果,问至少需要几个袋子,袋子必须严格装满,否则返回 -1。
Eg : 1,2,3 都是不能装满,返回 -1。
思路历程:
- 第一感觉很像零钱换硬币的问题,用最少的硬币数量凑出钱数 ,贪心的过程,每次以 枚最大面额去尝试,不断减小 直至满足条件。
- 此题我们也这样去尝试,优先都用 8 号袋子装,剩下的用 6 号袋子装,刚好装满则返回 ,如果装不满,就继续 的数量减小。
- 于是写出如下代码 版本:
/**
* 思想历程:首先尝试一下暴力解
* 1.优先装 8 这个袋子,剩下的装入 6 袋子中,严格装满。
* 2.从最多的 n / 8 个袋子一直尝试,直到找到答案返回,否则返回 -1
*/
public static int minBags(int n) {
if (n < 6) return -1;
int count = 0;
int bag8 = n / 8;
for (int i = bag8; i >= 0; i--) {
int mod = n - i * 8;
if (mod % 6 == 0) {
//有效方案
count = i + mod / 6;
return count;
}
}
return -1;
}
继续优化:
- 通过题意,奇数肯定不满足条件,过滤一下。
- 当装完 8 号袋子后剩余苹果为 , 当 直接返回 - 1
原因: 6 与 8 的最大公约数为 24,超过 24 后,25 % 6 = 1 = 25 % 8,26 % 6 = 2 = 26 % 6
既然是求最少袋子数量,模数一样,那肯定选择 8 号袋子,这里有贪心的思想。
- 于是写出如下优化后的代码 :
/**
* 思想历程:首先尝试一下暴力解
* 1.优先装 8 这个袋子,剩下的装入 6 袋子中,严格装满。
* 2.从最多的 n / 8 个袋子一直尝试,直到找到答案返回,否则返回 -1
*/
public static int minBags(int n) {
if (n < 6) return -1;
int count = 0;
int bag8 = n / 8;
for (int i = bag8; i >= 0; i--) {
int mod = n - i * 8;
if (mod % 6 == 0) {
//有效方案
count = i + mod / 6;
return count;
}
}
return -1;
}
打表法代码如下 :
/**
* 打表法:通过观察暴力递归的结果:
* 从18开始:每8个数为一组,奇数为-1,偶数为满足 : (n - 18)/8 + 3
* 于是可以写出如下代码
*/
public static int minBags2(int n) {
if ((n & 1) != 0 || n < 6) return -1;
//直接列举
if (n < 18) {
return (n == 6 || n == 8) ? 1 : (n == 12 || n == 14 || n == 16) ? 2 : -1;
}
//超过18,直接写答案
return (n - 18) / 8 + 3;
}
好家伙,这代码是给人看的么,之前在 LeetCode 里看到类似解析,一顿雾水,佩服的五体投地,咋想出来的,有规律?
打表法场景三:
对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”。
于是,打印出暴力方法从 的解如下:
- 前 17 个数没规律,直接输出,从18以后,每 8 个数一组,第一组为 3,第二组为 4,第三组为 5......
- 所以大于18 有如下结论:
小羊吃草(博弈问题)
题目:有 A , B 两只羊,有 N 堆草,规定每次只能吃 4 的次方数量堆草,既 1,4,16,64.....,判断是先手吃到最后的草堆,还是后手吃到,假定两只羊都绝顶聪明。规定:不能不吃。至少吃 1 。返回: 先手 or 后手。
思路历程:
- 博弈问题常规解法就是理解好递归函数。
- 因为双方都是绝顶聪明,函数入口表示
先手,有n份草,我要如何吃才能赢。 - 重点理解:母过程的
先手,对应在子过程中就是后手。
/**
* 思路:博弈问题,都是绝顶聪明。
* 1.入口函数为先手,表示先吃。
*/
public static String winner(int n) {
//调用该函数的时候当前对象用于表示先手,n = 0了,则表明之前的后手把草吃完了,
//导致我无草可吃,表示后手赢
// 0, 1, 2, 3, 4,
// 后 先 后 先 先
if (n < 5) {
return (n == 0 || n == 2) ? "后手" : "先手";
}
//当n>=5时候
int base = 1;//表示当前先手决定吃的草数量
while (base <= n) {
//当前一共 n 份草,先手吃了 base 份, n - base 是留给后手的草
//母过程先手,在子过程中就是后手,即及过程如果返回 后手,则表明当前的决定能赢
if (winner(n - base).equals("后手")) {
return "先手";
}
//防止 base * 4 越界
if (base > n / 4) break;
base *= 4;
}
//上面过程都不能让先手赢
return "后手";
}
还是打印小范围内的结果看看规律:
可以观察到到输入参数 n 模上 5 等于 2 ,0` 时候都是`后手赢,否则先手赢。
/**
* 观察暴力递归:发现,n % 5 == 0 || n % 5 == 2 时候是后手赢,其他情况都是先手赢
*/
public static String winner2(int n){
if (n % 5 == 0 || n % 5 == 2) return "后手";
return "先手";
}
累加和问题
题目:定义一种数:可以表示成若干 (数量 > 1) 连续正数和的数,返回 true 否则返回 false。
eg: 5 = 2 + 3,12 = 3 + 4 + 5,1 不满足,因为要求数量大于1,2 = 1+1也不满足,因为不是连续递增的。
给定一个参数 N ,返回这个是是否可以表示成若干连续正数和的数字
思路历程:
- 暴力尝试,两层 for 循环,遍历区间累加和,满足则 返回 fasle,否则继续。
代码如下:
/**
* 暴力尝试,从 i - n 不停的尝试,找到了就返回 - 1
*/
public static boolean isSum(int n) {
if (n <= 2) return false;
for (int i = 1; i <= n / 2; i++) {//因为会少两个数,第一个数字的取值范围为 1-n/2
int sum = i;
for (int j = i + 1; j < n ; j++) {
sum += j;
if (sum == n) return true;
if (sum > n) break;
}
}
return false;
}
打表观察结果:
/**
* 打表法:通过观察当 n >=3 后
* 有:4,8,16,32,64...为 false,其它都为 true
*/
public static boolean isSum1(int n){
if (n <= 2) return false;
//判断一个数是否是2的次方:这个是的二进制中只有一个 1
//eg: 16 = 001000 则 n & (n - 1) = 00100 & 000111 = 0,则为2的次方
//eg: 6 = 00110 则 n & (n - 1) = 00110 & 00011 != 0
return (n & (n - 1)) != 0;
}
结语
暴力方法是很多算法的前提,有了暴力算法后,我们可以用它做对数器,判断我们优化后的代码是否是正确的,同时也可以通过输入和输出,找出一点规律,于是就有了打表法规则三。
如果直接看代码,肯定会一头雾水,好的代码逻辑一定是一步步优化出来的。