今天做到剑指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 0−9” 对应字符 “ a − j a-j a−j”,数字 “ 10 − 25 10-25 10−25” 对应字母 “ k − z k-z k−z”,按照青蛙跳台阶的思路,从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];
}
在遇到问题时,要学会触类旁通,该问题是青蛙跳台阶的衍生问题,要有灵活运用的能力。