Decode Ways
Problem:
A message containing letters from A-Z is being encoded to numbers using the following mapping:
'A' → 1
'B' → 2
...
'C' → 26
Given an encoded message containing digits, determine the total number of ways to decode it.
For example,
Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).
The number of ways decoding "12" is 2.
分析:
解决问题前,需要清晰题目的要求,明确一些特殊的情况。首先题目中应该不会出现除0~9外的其它字符,其次0只能出现在1或者2后面,如果解码时出现01或者0X的情况均认为是解码失败。有了以上两点,题目的定义已基本明确。
这个问题可以递归解决,假设字符串s的解码方式为f(n)种,那么
f(n) = I(A[0] can be decoded)f(n-1) + I(A[0..1] can be decoded)f(n-2)
其中I(A[0] can be decoded)是指示变量,如果第一个字符可以被解码,那么指示变量值为1,否则为0.
有了以上的递推关系,递归代码很容易写,然而为了提高递归的效率,不需要每次递归的时候都将新的子串传入函数中,而是直接用下标访问。代码如下
1 class Solution { 2 public: 3 int numDecodings(string s) { 4 // Note: The Solution object is instantiated only once and is reused by each test case. 5 return numDecode(s, 0); 6 } 7 8 int numDecode(const string &s, size_t idx) { 9 if(idx >= s.length()) { 10 return 1; 11 } else { 12 int num1 = 0, num2 = 0; 13 if(s[idx] >= '1' && s[idx] <= '9') { 14 num1 = numDecode(s, idx+1); 15 } 16 17 if(idx+1 < s.length()) { 18 int cur = atoi(s.substr(idx,2).c_str()); 19 if(cur >= 10 && cur <= 26) { 20 num2 = numDecode(s, idx+2); 21 } 22 } 23 24 return num1 + num2; 25 } 26 } 27 };
改进一:
但这段代码无法AC,因为执行效率太低了,有大量的重复计算,比如算f(n)时,f(n-2)会计算一次,算f(n-1)时,f(n-2)又会被算一次。提高效率的手段无非就是将计算过的值存下来,所以可以开辟一块新的空间,将已经计算得到的结果存储下来,如果下次还要用这个值,就直接返回,无需计算。改进后代码可以AC了。
1 class Solution { 2 public: 3 int numDecodings(string s) { 4 // Note: The Solution object is instantiated only once and is reused by each test case. 5 if(s.length() == 0) 6 return 0; 7 8 vector<int> computed(s.length()+1, -1); 9 return numDecode(s, computed, 0); 10 } 11 12 int numDecode(const string &s, vector<int> &computed, size_t idx) { 13 if(idx >= s.length()) { 14 return 1; 15 } else if(computed[idx] >= 0) { 16 return computed[idx]; 17 } else { 18 int num1 = 0, num2 = 0; 19 if(s[idx] >= '1' && s[idx] <= '9') { 20 num1 = numDecode(s, computed, idx+1); 21 } 22 23 if(idx+1 < s.length()) { 24 int cur = atoi(s.substr(idx,2).c_str()); 25 if(cur >= 10 && cur <= 26) { 26 num2 = numDecode(s, computed, idx+2); 27 } 28 } 29 30 computed[idx] = num1 + num2; 31 return num1 + num2; 32 } 33 } 34 };
改进二:
改进一中在递归的框架下,用了O(n)的空间,实际上根据递推公式,每次的计算值,只跟前两次的计算值有关。仔细观察,这个形式看上去很像斐波那契数列的递推关系,只是两个值的求解是有条件的,只有能解码才加上相应的值,否则不加,因此可以考虑改成迭代的形式。迭代形式的代码写起来一般比较简单,空间复杂度低,执行效率更高。不过这段代码却不太好写,问题在于指示变量的含义容易搞错,容易将迭代变量的关系错误赋值。迭代是从小到大的迭代,比如要解决f(3),那么f(2)与f(1)都应该计算出结果,同样要解决f(2),f(1)与f(0)也都应该算出结果,即f(3) = f(2) + f(1),f(2) = f(1) + f(0)。如何定义每个问题前面的指示变量,与递归方式中略有不同。
在递归的方式下,f指的是后缀字符串。假设原始的三个字符依次是A[0], A[1], A[2],那么首先判断A[0]是否可以解码,如果可以,需要递归解决f(2)的问题了,如果不可以,只需要判断A[0..1]是否可以解码,如果可以需要递归解决f(1)的问题。而在迭代的方式下,需要换一种方式来看待f,f指的是前缀字符串。f(2)是前两个元素构成的字符串的解码数目,f(1)是第一个元素构成的字符串的解码数目,所以要求f(3),那么f(1)的指示变量是A[1..2]是否可以解码,f(2)的指示变量是A[2]是否可以解码。例如:字符串"121",f(1) = 1,f(2) = 2,那么f(1)的指示变量需要判断"21"是否可以解码,因为f(1)代表了前缀长度为1的子串的解码数目,现在只需要判断后面一部分即可。f(2)的指示变量同理。一句话概括,递归时,递推的部分是后缀,所以指示变量应该判断前面的子串;迭代时,递归的部分是前缀,所以指示变量应该判断后面的子串。理解了这一点,迭代的代码即可迎刃而解。
1 class Solution { 2 public: 3 int numDecodings(string s) { 4 // Note: The Solution object is instantiated only once and is reused by each test case. 5 if(s.length() == 0 || s[0] == '0') 6 return 0; 7 8 int fn1 = 1, fn2 = 1; 9 for(size_t n = 2; n <= s.length(); ++n) { 10 int f = 0; 11 if(s[n-1] >= '1' && s[n-1] <= '9') 12 f += fn1; 13 14 if(s[n-2] == '1' || s[n-2] == '2' && s[n-1] <= '6') 15 f += fn2; 16 17 fn2 = fn1; 18 fn1 = f; 19 } 20 21 return fn1; 22 } 23 };