剑指 Offer(第2版)面试题 46:把数字翻译成字符串
剑指 Offer(第2版)面试题 46:把数字翻译成字符串
题目来源:59. 把数字翻译成字符串
解法1:动态规划
还记得经典的爬楼梯(斐波那契数列)吗?
每次可以走 1 步或者 2 步,问 n 个台阶一共有多少种爬楼梯的方法?
我们不需要思考就能写出状态转移方程:dp[i] = dp[i-1] + dp[i-2]。
这道题相当于加了一些限制条件,并且倒着推更便于理解。
设 dp[i] 表示从字符串的第 i 位开始到末尾,最大的翻译方法个数。
初始化 dp[n] = 1,dp[n - 1] = 1。
状态转移:
- dp[i] = dp[i + 1],先假设 s[i] 只有一种翻译方法,此时翻译方法保持不变。
- 如果 s[i] 和 s[i+1] 能够翻译成一个字母,则 dp[i] 的翻译方法可以加上 dp[i+2] 的值。
举例:
假设数字为 12xxxxxx,此时考虑到 1,有 2 种情况:
- 1 作为单独的一个数看,翻译方法数和 2xxxxxx 相同.
- 12 作为一个整体看,翻译方法数和 xxxxxx 相同。
代码:
class Solution
{
public:
int getTranslationCount(string s)
{
if (s.empty())
return 0;
int n = s.size();
if (n == 1)
return 1;
vector<int> dp(n + 1, 0);
// 初始化
dp[n] = dp[n - 1] = 1;
// 状态转移
for (int i = n - 2; i >= 0; i--)
{
dp[i] = dp[i + 1];
if (s[i] == '1' || (s[i] == '2' && s[i + 1] >= '0' && s[i + 1] <= '5'))
dp[i] += dp[i + 2];
}
return dp[0];
}
};
复杂度分析:
时间复杂度:O(n),其中 n 是字符串 s 的长度。
空间复杂度:O(n),其中 n 是字符串 s 的长度。
我们发现 dp[i] 的值只与 dp[i+1] 和 dp[i+2] 有关,可以状态压缩:
class Solution
{
public:
int getTranslationCount(string s)
{
if (s.empty())
return 0;
int n = s.size();
if (n == 1)
return 1;
// vector<int> dp(n + 1, 0);
// 初始化
// dp[n] = dp[n - 1] = 1;
int cur = 0, pre1 = 1, pre2 = 1;
// 状态转移
for (int i = n - 2; i >= 0; i--)
{
// dp[i] = dp[i + 1];
cur = pre1;
if (s[i] == '1' || (s[i] == '2' && s[i + 1] >= '0' && s[i + 1] <= '5'))
{
// dp[i] += dp[i + 2];
cur += pre2;
}
pre2 = pre1;
pre1 = cur;
}
return cur;
}
};
还可以顺着进行动态规划:
class Solution
{
public:
int getTranslationCount(string s)
{
if (s.empty())
return 0;
int n = s.size();
if (n == 1)
return 1;
vector<int> dp(n + 1, 0);
// 初始化
dp[0] = dp[1] = 1;
// 状态转移
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i - 1];
if (s[i - 2] == '1' || (s[i - 2] == '2' && s[i - 1] >= '0' && s[i - 1] <= '5'))
dp[i] += dp[i - 2];
}
return dp[n];
}
};
类似题:LeetCode 91. 解码方法
链接:91. 解码方法
从 1 编码到 26,这里 0 是非法字符!
代码:
// 顺推动态规划
class Solution
{
public:
int numDecodings(string s)
{
// 特判(s 只包含数字,并且可能包含前导零)
if (s.empty() || s[0] == '0')
return 0;
if (s.size() == 1)
return 1;
int n = s.size(), pre = s[0] - '0';
// 状态矩阵,并初始化(每个数字均可映射成一个字母)
vector<int> dp(n + 1, 1);
// 状态转移
for (int i = 2; i <= n; i++)
{
int cur = s[i - 1] - '0';
if (cur == 0 && (pre != 1 && pre != 2))
return 0;
if (pre == 1 || (pre == 2 && cur <= 6)) // 当前数字为 10~26
{
if (cur != 0) // cur 不为 0,则当前数字可以拆分,方法数累加
dp[i] = dp[i - 2] + dp[i - 1];
else // 当前数字为 10或 20,不可拆分,只有一种映射,解码方法不增加
dp[i] = dp[i - 2];
}
else // 当前数字为1~9,只有一种映射,解码方法不增加
dp[i] = dp[i - 1];
pre = cur;
}
return dp[n];
}
};
倒推会出现问题,这道题并不保证任何数字都可以翻译成字符串。
简化:
// 简化
class Solution
{
public:
int numDecodings(string s)
{
// 特判(s 只包含数字,并且可能包含前导零)
if (s.empty() || s[0] == '0')
return 0;
if (s.size() == 1)
return 1;
int n = s.size();
s = " " + s;
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++)
{
int a = s[i] - '0', b = (s[i - 1] - '0') * 10 + s[i] - '0';
if (a >= 1 && a <= 9)
dp[i] = dp[i - 1];
if (b >= 10 && b <= 26)
dp[i] += dp[i - 2];
}
return dp[n];
}
};
复杂度分析:
时间复杂度:O(n),其中 n 是字符串 s 的长度。
空间复杂度:O(n),其中 n 是字符串 s 的长度。