把数字翻译成字符串/斐波那契数列/青蛙跳台阶

  今天做到剑指Offer中面试题46:把数字翻译成字符串,一看,这不是之前的斐波那契数列青蛙跳台阶的翻版吗?遂记录一下解题思路。

题目:给定一个数字,我们按照如下规则把它翻译为字符串: 0 0 0翻译成 a a a 1 1 1翻译成 b b b,……, 11 11 11翻译成 l l l,……, 25 25 25翻译成 z z z。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"

分析
  转换规则可以理解为数字 “ 0 − 9 0-9 09” 对应字符 “ a − j a-j aj”,数字 “ 10 − 25 10-25 1025” 对应字母 “ k − z k-z kz”,按照青蛙跳台阶的思路,从0开始跳,每次可以跳1个或2个台阶,则跳法有 f ( i ) = f ( i + 1 ) + f ( i + 2 ) f(i) = f(i+1)+f(i+2) f(i)=f(i+1)+f(i+2)
  在本题中将数字的位数当作是台阶数,若是 n u m % 100 num\%100 num%100(即当前数字的前两位)满足 9 < n u m % 100 < 26 9<num\%100<26 9<num%100<26的,则可以一次跳 2 2 2个台阶,否则只能跳 1 1 1个台阶,则类比之下翻译方法有 f ( i ) = f ( i + 1 ) + g ( i , i + 1 ) f ( i + 2 ) f(i)=f(i+1)+g(i,i+1)f(i+2) f(i)=f(i+1)+g(i,i+1)f(i+2),其中 g ( i , i + 1 ) g(i,i+1) g(i,i+1)当满足 9 < n u m % 100 < 26 9<num\%100<26 9<num%100<26时为 1 1 1,否则为 0 0 0
  由以上公式可以写出递归函数:

    int translateNum(int num) {
    
    
        if(num < 0) return 0;
        return recursive(num);
    }

    int recursive(int num) {
    
    
        if(num < 10) return 1;
        int converted = num % 100; //取数字前两位
        if(converted > 9 && converted < 26) {
    
     //若是满足大于等于10小于等于25的条件
        	//此时可以选择跳1次或2次,所以都要计数累加
            return recursive(num/100) + recursive(num/10); 
        } else {
    
    
        	//此时只能够跳一次,则num/=10;
            return recursive(num/10);
        }
    }

  但是当数据是个大数,超出int或是long类型所能表示的数据范围,此时就不可使用如上的方法了,此时该怎么办呢?对,将数字转换为字符串对字符串进行操作是大数问题的常用求解方法,简单利用to_string函数就可以将数字转换为字符串了,再将递归函数中的中间过程的条件判断进行修改即可,代码如下:

    int translateNum(int num) {
    
    
        if(num < 0) return 0;
        return recursive(to_string(num), 0);
    }
    int recursive(const string& num, int curIdx) {
    
    
        if(curIdx >= num.size()-1) return 1;
        int digit1 = num[curIdx] - '0';
        int digit2 = num[curIdx+1] - '0';
        int converted = digit1 * 10 + digit2;
        if(converted < 26 && converted > 9) {
    
    
            return recursive(num, curIdx+1) + recursive(num, curIdx + 2);
        } else {
    
    
            return recursive(num, curIdx+1);
        }
    }

  若是使用如上递归公式,仔细分析就会发现其间会有许多的子问题的重复求解,就像在斐波那契数列数列求解中的那样,以 12258 12258 12258为例,可以分为两个子问题:翻译 1 1 1 2258 2258 2258;翻译 12 12 12 258 258 258;对于 2258 2258 2258再进行翻译,分为 22 22 22 58 58 58 2 2 2 258 258 258两个子子问题,可以看到翻译 258 258 258这个子问题被求解了两次,产生了重复计算。
  递归从上至下,那也可从下至上进行求解,以消除重复子问题。即从数字末尾开始,从右到左依次翻译并计算不同翻译的数目,其实可以看出,也就是使用了dp的思想,其实动态规划就是特殊的递归方式,在写出递归表达式后,基本上都可以使用dp求解,dp本身也是空间换时间的方法。
  在解决大数问题的基础上,改动得到代码如下:

    int translateNum(int num) {
    
    
        if(num < 0) return 0;
        return recursive(to_string(num));
    }
    
    int recursive(const string& num) {
    
    
        int length = num.size();
        //用于记录子问题的解,避免重复子问题
        vector<int> counts = vector<int>(length, 0); 
        //用于记录当前位置的解
        int count = 0;

        for(int i = length - 1; i >= 0; --i) {
    
    
            count = 0;
            if(i < length - 1) {
    
    
            	//若是不是最末位的字母,则以之后一位的子问题的解为基础
                count = counts[i+1]; 
            } else {
    
    
            	//若是最末尾字母,则解为1,即num<10的情况
                count = 1;
            }
            if(i < length - 1) {
    
    
            	//若不是最末尾的字母,则提取出当前和之后的两位字符并计算该二位数
                int digit1 = num[i] - '0';
                int digit2 = num[i+1] - '0';
                int converted = digit1 * 10 + digit2;
                //若是该二位数大于等于10且小于等于25
                if(converted > 9 && converted < 26) {
    
    
                	//若是符合该条件,则count[i]=count[i+1]+count[i+2];
                	//即倒数第三位之前的字符
                	if(i < length - 2) {
    
    
                        count += counts[i+2];
                    //若是倒数第二位,则直接+1
                    } else {
    
    
                        count += 1;
                    }
                }
            }
            //将所得解进行记录
            counts[i] = count;
        }
        return counts[0];
    }

在遇到问题时,要学会触类旁通,该问题是青蛙跳台阶的衍生问题,要有灵活运用的能力。

猜你喜欢

转载自blog.csdn.net/yueguangmuyu/article/details/112789425