《算法学习一头雾水的打表法》

1,198 阅读5分钟

引言

打表法的概念

打表是一种用空间换时间的技巧,一般指将所有可能需要用到的结果事先计算出来,这样后续可以直接通过查表获取。打表常见的用法有以下几种:

  • 在程序中一次性计算出所有需要用到的结果,之后的查询直接取这些结果。
  • 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。
  • 对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”。

本篇文章主要介绍第三种,当遇到一头雾水的题目,毫无章法可言,并且输入为简单整数,输出为简单整数。通过尝试多个数找到规律,直接写答案。

苹果装袋问题

题目:给定两种规格的袋子:6 和 8,小明准备买 NN 个苹果,问至少需要几个袋子,袋子必须严格装满,否则返回 -1。

Eg : 1,2,3 都是不能装满,返回 -1。

思路历程

  • 第一感觉很像零钱换硬币的问题,用最少的硬币数量凑出钱数 NN,贪心的过程,每次以 aa 枚最大面额去尝试,不断减小 aa 直至满足条件。
  • 此题我们也这样去尝试,优先都用 8 号袋子装,剩下的用 6 号袋子装,刚好装满则返回 count8+count6count8 + count6 ,如果装不满,就继续 count8count8 的数量减小。
  • 于是写出如下代码 V1.0V1.0 版本:
 /**                                          
  * 思想历程:首先尝试一下暴力解                            
  * 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 号袋子后剩余苹果为 modmod , 当 mod>24mod > 24 直接返回 - 1

原因: 6 与 8 的最大公约数为 24,超过 24 后,25 % 6 = 1 = 25 % 8,26 % 6 = 2 = 26 % 6

既然是求最少袋子数量,模数一样,那肯定选择 8 号袋子,这里有贪心的思想。

  • 于是写出如下优化后的代码 V2.0V2.0
 /**                                          
  * 思想历程:首先尝试一下暴力解                            
  * 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;                               
 }                                            

打表法代码如下 V3.0V3.0

 /**
  * 打表法:通过观察暴力递归的结果:
  * 从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 里看到类似解析,一顿雾水,佩服的五体投地,咋想出来的,有规律?

打表法场景三

对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”。

于是,打印出暴力方法从 1N1-N 的解如下:

image-20220427164344912.png

  • 前 17 个数没规律,直接输出,从18以后,每 8 个数一组,第一组为 3,第二组为 4,第三组为 5......
  • 所以大于18 有如下结论: (n18)/8+3(n - 18) / 8 + 3

小羊吃草(博弈问题)

题目:有 A , B 两只羊,有 N 堆草,规定每次只能吃 4 的次方数量堆草,既 141664.....,判断是先手吃到最后的草堆,还是后手吃到,假定两只羊都绝顶聪明。规定:不能不吃。至少吃 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 "后手";                                         
}                                                        

还是打印小范围内的结果看看规律

image-20220427170339730.png

可以观察到到输入参数 n 模上 5 等于 20` 时候都是`后手赢,否则先手赢。

/**                                                              
 * 观察暴力递归:发现,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;                                      
}                                                                   

结语

暴力方法是很多算法的前提,有了暴力算法后,我们可以用它做对数器,判断我们优化后的代码是否是正确的,同时也可以通过输入和输出,找出一点规律,于是就有了打表法规则三。

如果直接看代码,肯定会一头雾水,好的代码逻辑一定是一步步优化出来的。