题目描述
小获得了一个任务,需要将数字翻译成字符串。翻译规则是:对应"”,对应"",依此类推直到对应""。一个数字可能有多种翻译方法。小需要一个程序来计算一个数字有多少种不同的翻译方法。例如:数字可以翻译成"","","",""和"",共种方式。
算法
本题主要应用了动态规划算法:
- 使用
dp数组记录到每一位的翻译方法总数,并通过递推关系进行状态更新。 - 该方法能够有效地减少重复计算,提高算法的时间效率。
思路分析
该问题是一个典型的动态规划问题。我们需要计算一个数字的所有可能翻译组合数,数字每两位最多只能翻译成一个字母(0到25对应字母a到z)。可以通过动态规划来解决这一问题,记录到每一位数字为止的不同翻译方式数目。
思路大致如下:
-
预处理数字:将
num转化为字符串ss,并在前面加上字符“0”。加“0”是为了避免越界错误并确保每次都可以安全检查到两位数字组合。 -
状态定义: 用
dp[i]表示前i位数字的不同翻译方法数量。 -
转移方程:
- 单字符翻译:无论什么情况,只要当前字符是有效数字(不为
0),都可以直接由前一状态的方案数dp[i-1]传递过来。 - 双字符翻译:若
ss[i-1]和ss[i]可以组合成一个有效的双字符(即10 <= num <= 25),则我们可以将其作为一个整体翻译,方案数增加dp[i-2]。 - 状态转移方程可以表示为: dp[i] = dp[i-1] + dp[i-2]
- 单字符翻译:无论什么情况,只要当前字符是有效数字(不为
-
结果: 最终
dp[len - 1](即dp的最后一个元素)存储了num的所有可能翻译方法数。
难点分析
本题的难点在于如何将数字的每一位或相邻两位组合成可翻译的字符,同时确保动态规划的状态转换符合题目要求。以下是更详细的难点分析:
-
单字符与双字符翻译的组合判断:
- 对于数字中的每一位,我们既可以单独将其翻译为一个字符,也可以将其与前一位组合成一个双字符来翻译。但并非所有两位组合都是合法的字母(即不超过
25)。 - 例如,对于数字 "12258",
12和25可以翻译为字母,但58不在范围内,不能当成一个字母。如何判定每个双字符组合是否有效(在10到25之间)是解题的核心之一。 - 我们通过将字符串的两位组合成整数来检查范围,同时要确保组合的首位不为
0,因为这会导致不合法的编码。
- 对于数字中的每一位,我们既可以单独将其翻译为一个字符,也可以将其与前一位组合成一个双字符来翻译。但并非所有两位组合都是合法的字母(即不超过
-
状态转移的定义与边界处理:
- 在本题中,
dp[i]表示从第0位到第i位的翻译方式总数,这样可以更直观地考虑每一个位置的翻译组合。 - 然而,为了确保每次都可以安全访问前一位或前两位,我们需要对数字进行预处理,在字符串的最前面加一个“0”来填充。例如,将
12258转换成"012258",这样当我们访问i-2位置时就不会越界。 dp[0]初始化为1,即一个空前缀(没有任何字符)只有一种翻译方法,这是动态规划初始状态,起始条件设置是否正确直接影响到后续的状态累加正确性。
- 在本题中,
-
动态规划状态转移方程的理解:
-
本题的
dp[i]状态来源有两种情况:- 如果
ss[i]可以作为单独的一个字符翻译(即不是0),则dp[i] += dp[i-1]。 - 如果
ss[i-1]和ss[i]可以组合成一个有效的双字符(即在10到25之间),则有dp[i] += dp[i-2]。
- 如果
-
这两种状态的组合在动态规划中的叠加是关键,因为它们反映了将数字的不同部分翻译为字符的不同方式。任何遗漏一个状态都可能导致结果不完整。特别是当双字符组合需要满足额外的条件时,确保只有在满足条件时才累加状态是一个难点。
-
-
动态规划的空间优化思考:
如果仅关注最终结果而不需要保留整个
dp数组,我们可以通过变量来记录前两位状态,减少空间复杂度为O(1)。在面试或工程实现中,这种优化思路非常重要,但实际实现需要确保每个状态都能正确传递。这对于实现与理解状态的更新和变量的复用都有一定的挑战。 -
对边界情况的处理:
- 数字可能包含
0,例如数字10258。0在本题中无法单独翻译,因此会影响翻译路径的选择。特别地,若一个0出现在双字符组合的十位上(如“10”或“20”),需要确保组合解析正确,否则会导致错误的结果。 - 需要关注的边界情况还包括:只有一位数字的情况(如
0到9)、只有可以翻译成字母的双字符情况(如10或25),以及100等情况,这些都需要确保算法处理时边界条件不出错。
- 数字可能包含
代码分析
int solution(int num) {
// 将数字转换为字符串形式,并在前面加0
std::string ss = "0" + std::to_string(num);
int len = ss.size();
std::vector<int> dp(len + 1);
// 初始化第一个状态
dp[0] = 1;
for (int i = 1; i < len; i++) {
// 每一位数字作为单独字符翻译的方案数
dp[i] += dp[i - 1];
// 检查是否可以将 ss[i-1] 和 ss[i] 作为双字符组合进行翻译
int num = (ss[i] - '0') + (ss[i - 1] - '0') * 10;
if ((i >= 2) && num < 26 && ss[i - 1] != '0') {
dp[i] += dp[i - 2];
}
}
// 返回 dp[len - 1],即最终的方案总数
return dp[len - 1];
}
复杂度分析
- 时间复杂度:
O(n),其中n是num的位数。每一位数字只需常数时间来进行状态转移计算。 - 空间复杂度:
O(n),因为需要一个大小为n+1的数组dp来存储状态。
总结
通过动态规划,问题被分解为若干子问题。每一个位置 i 的状态仅依赖于其前面的两种状态 i-1 和 i-2,这种方法使得计算量大大降低。本题的难点在于理解动态规划的状态定义和状态转移公式,并对每一个数字位进行合理的条件判断。最终,算法在时间和空间上都达到了较优的复杂度,适用于大规模输入。