对数器打表找规律

要点
  • 对数器打表找规律的使用场景: 输入参数是简单类型,返回值也是简单类型。

  • 对数器打表找规律的过程:

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

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

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

小明用袋子买苹果

我们通过这个 【小明用袋子买苹果的问题】 来简单了解一下对数器打表找规律是怎样工作的:


小明去商店买苹果,发现只有【能装8个苹果的袋子】和【能装6个苹果的袋子】,节俭的小明只有当自己手上的袋子全部被装满时才会买苹果,现在我想知道小明买苹果需要用掉多少个袋子。

给定n个苹果,如果刚好能装满所有袋子,则返回至少要多少个袋子;如果不存在每个袋子都装满的情况,则返回-1。

示例:

1.买10个苹果 需要装满8苹果的袋子,但是还需要一个不能装满6苹果的袋子 返回-1。

2.买14个苹果 需要一个装满8苹果的袋子和一个装满6苹果的袋子 返回2。


我们先来看看常规解法(暴力递归解法):

首先想到的一定是动态规划解法,给出问题框架如下:

 //输入参数为苹果数x
 int AppleMinBags(int x){
     int result ;
     .....
     return result;
 }

从后往前考虑,不就是 AppleMinBags(x) = min(AppleMinBags(x-8),AppleMinBags(x-6)) + 1 嘛。递归表达式已经出来了,现在我们该考虑边界条件了,这道题的边界条件是什么呢?

很简单,就是 x小于6且不等于0 对应的AppleMinBags(x) 无效,那对于这种无效情况我们应该返回什么呢?

既然是求需要袋子的最小个数,那我们就让无效的AppleMinBags(x) = INT_MAX,但是我们想想,如果递归栈顶的结果是INT_MAX,那我下一层还能+1吗?显然不能,所以我们要改一下递归表达式:

如果递归栈顶的结果是INT_MAX,那我们只能+0,如果递归栈顶的结果不是INT_MAX,那我们正常+1即可。

【由于我们有6容量的袋子,所以最终的苹果数就是6的余数 0,1,2,3,4,5。0则返回0,1,...,5则继续-6或-8,返回INT_MAX】

常规解法代码如下:

 //输入参数为苹果数x
 int AppleMinBags(int x){
     return dp(x) == INT_MAX ? -1, dp(x);
 }
 int dp(int x){
     //边界条件
     if(x == 0)  return 0;
     if(x < 0)   return INT_MAX; //无效解
     
     int p1 = dp(x-8); //用8容量袋子装
     int p2 = dp(x-6); //用6容量袋子装
     p1 += (p1 == INT_MAX ? 0, 1); //如果栈顶无效,则新栈顶继续维持无效(+0)
     p2 += (p2 == INT_MAX ? 0, 1);
     
     return std::min(p1, p2);
 }
对数器打表找规律:

当我们遇到的问题较为复杂时,常规解法可能不能立刻得出来,这时候我们可以先打印出前几个值来找出大概规律。然后把这个大概规律用代码描述出来,这样可以得到比常用递归暴力解法时间复杂度低很多的解法。

由于篇幅有限,读者可自行打印出前100个数据进行观察规律,我这里只给出前四十个数据的表格。

打印代码:

 //打印前一百个数观察规律
 int main(){
     for(int i = 1; i <= 100; i++){
         cout << i << " : " << AppleMinBags(i) << endl;
     }
     return 0;
 }

下面是前四十个数据的表格:

1 : -111 : -121 : -131 : -1
2 : -112 : 222 : 332 : 4
3 : -113 : -123 : -133 : -1
4 : -114 : 224 : 334 : 5
5 : -115 : -125 : -135 : -1
6 : 116 : 226 : 436 : 5
7 : -117 : -127 : -137 : -1
8 : 118 : 328 : 438 : 5
9 : -119 : -129 : -139 : -1
10 : -120 : 330 : 440 : 5

我们发现,前十八个元素中,6、8对应1个袋子,12、14、16对应两个袋子;而18之后,18、20、22、24对应3个袋子,26、28、30、32对应

4个袋子,34、36、37、40对应5个袋子,...。你观察你打印的100个数据能够更加确信这个规律。

现在我们用代码表示这个规律:

 int AppleMinBags(int n){
     //由于 6 + 8 只能为偶数,所以奇数只能返回-1
     if(n % 2 != 0)  return -1;
     
     if(x == 6 || x == 8)    return 1;
     if(x == 14 || x == 16 || x == 12)   return 2;
     if(x >= 18)  return (x-18)/8 + 3;
 }

将这段代码代入运行,会得出和暴力递归解法一样的结果。

AB轮流吃草最终谁会赢

简单介绍一下这个例子,草场上有n份草,小牛【小薪】和小牛【小楠】起了争执,谁把最后一份草吃完让对方无

草可吃谁就赢,两只牛每次吃草只能吃4的某次方份草(也就是4^x份草,1,4,16,64...)。在一轮回合中,【小薪】先

吃草,【小薪】和【小楠】都在竭尽全力让自己赢,也就是说决策双方都是在做理智最优决策【理智最优决策可以

自行搜索博弈论的基本策略】,请问最终谁会赢?


还是先按照常规递归思路来分析题目:

当草场上没有草,即有0份草时,A先遇到无草可吃的情况,B赢;有1份草时,A选择吃1份草,轮到B时就只剩0份草,A赢;有2份草时,A只能选择吃1份草,轮到B时B也选择吃1份草,进入下一回合时A先无草可吃,B赢;有3份草时,......,A赢;有4份草时,A吃4份草,B就面临0份草的情况了,......。

从这前5个数据可以看出简单规律,设cur为当前选手,enemy为另一选手,草数少于5时,当草数为0或2时,enemy赢,否则cur赢。

草数大于4时能将情况转化为小于4的情况吗?由于这个问题是一个博弈论的问题,【小薪】和【小楠】都有诸多选择,并且他们都会选择对自己最有益的选择。举个例子:

假设轮到【小薪】吃草,并且还剩下100份草,【小薪】有以下几种选择:

吃 1份草 --> 剩下99份,【小楠】吃

吃 4份草 --> 剩下96份,【小楠】吃

吃16份草 --> 剩下84份,【小楠】吃

吃64份草 --> 剩下36份,【小楠】吃

如果剩下rest份,【小楠】吃的结果是【小薪】赢,那么小薪就会选择当前方案。如果所有选择的结果都是【小薪】输,那么直接返回【小楠】赢就可以了。

以下是常规解法框架:

 string Win(int x){
     string result;
     result = dp(x , "A");
     return result;
 }
 ​
 string dp(int rest, string cur){
     string enemy = (cur == "A" ? "B" : "A");
     if(rest < 5){
         if(rest == 0 || rest == 2)  return enemy;
         return cur;
     }
     int pick = 1;
     while(pick <= rest){    //进入循环说明当前草数可以做出此决定
         if(dp((rest - pick), enemy) == cur){
             return cur;
         }
         pick *= 4;
     }
    //cur做出任何选择都赢不了,那么最终赢家肯定是enemy
    return enemy;
 }
现在我们把数据打印出来看规律:

打印代码:

 int main(){                               
     for(int i = 0; i < 44; i++){
         cout << i << " : " << Win(i) << endl;
     }
     return 0;                                                                  
 }

以下是前44个数据的表格:

0 : B11 : A22 : B33 : A
1 : A12 : B23 : A34 : A
2 : B13 : A24 : A35 : B
3 : A14 : A25 : B36 : A
4 : A15 : B26 : A37 : B
5 : B16 : A27 : B38 : A
6 : A17 : B28 : A39 : A
7 : B18 : A29 : A40 : B
8 : A19 : A30 : B41 : A
9 : A20 : B31 : A42 : B
10 : B21 : A32 : B43 : A

看到这些数据你会惊讶的发现,结果竟然是BABAA,BABAA,......,BABAA的方式出现,也就是以BABAA为周期呈现结果。我们根据打表出来的结果直接给出代码:

 string Win(int n){
     if(n % 5 == 0 || n % 5 == 2){
         return "B";
     }
     return "A";
 }

找出这种简单规律,能够将时间复杂度大大降低,打表也只是一种对递归解法的优化手段。

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

这个问题说的是任取一个正数,判断它是否为连续正整数的和。比如 12 = 3 + 4 + 5,是3个连续正整数的和,而8 = 8,只能由一个正整数8构成,所以它不是连续正整数的和。

和之前一样,我们先看看常规解法:
 //采用最简单的遍历方法
 bool IsNum(int n){
     for(int i = 1;i < n;i++){
         //从i开始,i,i+1,i+2,....,.....累加之后能到n
         int rest = n;
         for(int j = i;rest > 0;j++){
             rest -= j;
             if(rest == 0)   return true;
         }
     }
     return false;
 }

从这段常规解法代码很容易看出来,判断这个数是不是连续正整数的和就是从左往右一个一个试,时间复杂度极大,为O(n^2)。

前四十个正整数数据:
1 : false11 : true21 : true31 : true
2 : false12 : true22 : true32 : false
3 : true13 : true23 : true33 : true
4 : false14 : true24 : true34 : true
5 : true15 : true25 : true35 : true
6 : true16 : false26 : true36 : true
7 : true17 : true27 : true37 : true
8 : false18 : true28 : true38 : true
9 : true19 : true29 : true39 : true
10 : true20 : true30 : true40 : true

从这四十个数据就能看出来,2^x(x=0,1,2,...)的数不是连续的正整数和,其他数都是连续的正整数和。

现在给出打表找出的代码:

 bool IsNum(int n){
     if(n & (n-1) == 0)  return false;
     return true;
 }

要求一个长度>=2的回文子串,求所有长度为n的red字符串中好串的数量

简要介绍一下”好串“的概念,”好串“指的是,如果字符串内部有且只有一个长度>=2的回文子串,那么这个串就叫子串。”回文子串“指的是正反序列一致的字符串。

示例:

  1. ”aba” 有长度为3的回文子串 “aba”,所以 “aba” 是好串。

  2. “ababc” 有长度为3的回文子串 “bab”“aba” ,所以“aba”不是好串。

  3. 同理,“1221”不是好串 ,“12321”是好串。

这个题目的要求就是,字符串中的字符只能从 ‘r’ ,’e’ ,’d’ 三个字符中选,用这三个字符拼出长度为n的字符串,求长度为n的好串的个数。


先看暴力解法:

我们先用最简单的方法得出答案,虽然这种方法提交时会超时,不过我们只对前一小部分的数据进行打印,不会出现运行很久才出结果的情况。

要想知道n长度的所有字符串中好串有多少个,很简单,把长度为n的所有字符串都列出来,一个一个验证其长度>=2的回文子串数量是否为1即可。至于如何验证回文子串,利用双指针(首尾指针)即可。

暴力解法代码如下:

 int f(vector<char>& path, int i, int n);
 bool is(vector<char>& path, int left, int right);
 ​
 int numOfGoodString(int n) {
   vector<char> path(n);
   return f(path, 0, n);
 }
 ​
 int f(vector<char>& path, int i, int n) {
   // 递归结束条件
   if (i == n) {
     int cnt = 0;
     // 枚举所有子串
     for (int j = 0; j < n; j++) {
       for (int k = j + 1; k < n; k++) {
         if (is(path, j, k)) {
           cnt++;
         }
         if (cnt > 1) {
           return 0;
         }
       }
     }
     return cnt == 1 ? 1 : 0;
   }
 ​
   // DFS
   int ans = 0;
   path[i] = 'r';
   ans += f(path, i + 1, n);
   path[i] = 'e';
   ans += f(path, i + 1, n);
   path[i] = 'd';
   ans += f(path, i + 1, n);
 ​
   return ans;
 }
 ​
 // 判断子串是否是回文子串
 bool is(vector<char>& path, int left, int right) {
   while (left <= right) {
     // 结束条件
     if (path[left] != path[right]) return false;
 ​
     left++;
     right--;
   }
   return true;
 }

同学们看这段代码不难看出,暴力解法的解法流程如下:

先从第0层(没有元素)节点开始,进行DFS遍历,通过三个分支进入下一层;如此递归下去,当递归到第n层时(此时字符数组中刚

好有n个元素,对应长度为n的字符串),开始判断此串是否为“好串”。判断此串是否为“好串”是用到了暴力枚举,对所有长度>=2的子串

进行暴力枚举并用计数器记录【初始为0,如果有一个子串是长度>=2的回文子串,则计数器+1】,如果计数器计数为1,则此串为“好

串”。

值得一提的是,我们之前使用DFS算法进行递归的时候,都是自身带数据进节点内部,考虑进入此节点拿到结果后回溯退出的情况比较少,在这段代码中,我们就是将所有子节点的结果全部回溯到父节点中,用ans记录此分支的“好串”数量。这样按步就班地回溯每个分支,就能在最初的f(path, 0 , n)处拿到长度为n的所有字符串中好串数量。

前19个数据表格:
1 : 011 : 72
2 : 312 : 78
3 : 1813 : 84
4 : 3014 : 90
5 : 3615 : 96
6 : 4216 : 102
7 : 4817 : 108
8 : 5418 : 114
9 : 6019 : 120
10 : 66

这个简单规律我就不赘述了哈,偷点懒(手动狗头)。

直接给出代码:

 
int numOfGoodString(int n){
     if(n == 1)  return 0;
     if(n == 2)  return 3;
     if(n == 3)  return 18;
     
     if(n >= 4){
         return 30+6*(n-4);
     }
 }

以上就是我对于对数器打表找规律这一知识点做的详细笔记了,如果同学们想要通过看视频方式学习【对数器打表找规律】,可以b站搜索左程云。

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值