Files
LeetCode-Book/leetbook_ioa/docs/LCR 165. 解密数字.md
krahets de1a505d63 Add documents of leetbook IOA and
selected coding interview.
2023-10-10 20:22:09 +08:00

8.4 KiB
Executable File
Raw Permalink Blame History

解题思路:

根据题意,可按照下图的思路,总结出 “递推公式” (即转移方程)。

下图中的 num 对应本题的 ciphertext

Picture1.png{:align=center width=600}

因此,此题可用动态规划解决,以下按照流程解题。

动态规划解析:

记数字 ciphertexti 位数字为 x_i ,数字 ciphertext 的位数为 n 例如: ciphertext = 12258n = 5 , x_1 = 1

  • 状态定义: 设动态规划列表 dp dp[i] 代表以 x_i 为结尾的数字的翻译方案数量。

  • 转移方程:x_ix_{i-1} 组成的两位数字可被整体翻译,则 dp[i] = dp[i - 1] + dp[i - 2] ,否则 dp[i] = dp[i - 1]


dp[i] =
\begin{cases}
dp[i - 1] + dp[i - 2] & {, (10 x_{i-1} + x_i) \in [10,25]} \\
dp[i - 1] & {, (10 x_{i-1} + x_i) \in [0, 10) \cup (25, 99]}
\end{cases}

可被整体翻译的两位数区间分析:x_{i-1} = 0 时,组成的两位数无法被整体翻译(例如 00, 01, 02, \cdots ),大于 25 的两位数也无法被整体翻译(例如 26, 27, \cdots ),因此区间为 [10, 25]

  • 初始状态: dp[0] = dp[1] = 1 ,即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1

  • 返回值: dp[n] ,即此数字的翻译方案数量;

Q 无数字情况 dp[0] = 1 从何而来? Aciphertext1, 2 位的组成的数字 \in [10,25] 时,显然应有 2 种翻译方法,即 dp[2] = dp[1] + dp[0] = 2 ,而显然 dp[1] = 1 ,因此推出 dp[0] = 1

方法一:字符串遍历

  • 为方便获取数字的各位 x_i ,考虑先将数字 ciphertext 转化为字符串 s ,通过遍历 s 实现动态规划。
  • 通过字符串切片 s[i - 2:i] 获取数字组合 10 x_{i-1} + x_i ,通过对比字符串 ASCII 码判断字符串对应的数字区间。
  • 空间使用优化: 由于 dp[i] 只与 dp[i - 1] 有关,因此可使用两个变量 a, b 分别记录 dp[i] , dp[i - 1] ,两变量交替前进即可。此方法可省去 dp 列表使用的 O(N) 的额外空间。

<Picture2.png,Picture3.png,Picture4.png,Picture5.png,Picture6.png,Picture7.png>

代码:

class Solution:
    def crackNumber(self, ciphertext: int) -> int:
        s = str(ciphertext)
        a = b = 1
        for i in range(2, len(s) + 1):
            tmp = s[i - 2:i]
            c = a + b if "10" <= tmp <= "25" else a
            b = a
            a = c
        return a
class Solution {
    public int crackNumber(int ciphertext) {
        String s = String.valueOf(ciphertext);
        int a = 1, b = 1;
        for(int i = 2; i <= s.length(); i++) {
            String tmp = s.substring(i - 2, i);
            int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
}
class Solution {
public:
    int crackNumber(int ciphertext) {
        string s = to_string(ciphertext);
        int a = 1, b = 1, len = s.size();
        for(int i = 2; i <= len; i++) {
            string tmp = s.substr(i - 2, 2);
            int c = tmp.compare("10") >= 0 && tmp.compare("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
};

此题的动态规划计算是 对称的 ,即 从左向右 遍历(从第 dp[2] 计算至 dp[n] )和 从右向左 遍历(从第 dp[n - 2] 计算至 dp[0] )所得方案数一致。从右向左遍历的代码如下所示。

class Solution:
    def crackNumber(self, ciphertext: int) -> int:
        s = str(ciphertext)
        a = b = 1
        for i in range(len(s) - 2, -1, -1):
            a, b = (a + b if "10" <= s[i:i + 2] <= "25" else a), a
        return a
class Solution {
    public int crackNumber(int ciphertext) {
        String s = String.valueOf(ciphertext);
        int a = 1, b = 1;
        for(int i = s.length() - 2; i > -1; i--) {
            String tmp = s.substring(i, i + 2);
            int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
}
class Solution {
public:
    int crackNumber(int ciphertext) {
        string s = to_string(ciphertext);
        int a = 1, b = 1, len = s.size();
        for(int i = len - 2; i > -1; i--) {
            string tmp = s.substr(i, 2);
            int c = tmp.compare("10") >= 0 && tmp.compare("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
};

复杂度分析:

  • 时间复杂度 O(N) N 为字符串 s 的长度(即数字 ciphertext 的位数 \log(ciphertext) ),其决定了循环次数。
  • 空间复杂度 O(N) 字符串 s 使用 O(N) 大小的额外空间。

方法二:数字求余

上述方法虽然已经节省了 dp 列表的空间占用,但字符串 s 仍使用了 O(N) 大小的额外空间。

空间优化:

  • 利用求余运算 ciphertext \mod 10 和求整运算 ciphertext // 10 ,可获取数字 ciphertext 的各位数字(获取顺序为个位、十位、百位…)。
  • 运用 求余求整 运算实现,可实现 从右向左 的动态规划计算。而根据上述动态规划 “对称性” ,可知从右向左计算是正确的。
  • 自此,字符串 s 的空间占用也被省去,空间复杂度从 O(N) 降至 O(1)

<Picture8.png,Picture9.png,Picture10.png,Picture11.png,Picture12.png,Picture13.png,Picture14.png,Picture15.png,Picture16.png,Picture17.png,Picture18.png,Picture19.png,Picture20.png,Picture21.png>

代码:

class Solution:
    def crackNumber(self, ciphertext: int) -> int:
        a = b = 1
        y = ciphertext % 10
        while ciphertext > 9:
            ciphertext //= 10
            x = ciphertext % 10
            tmp = 10 * x + y
            c = a + b if 10 <= tmp <= 25 else a
            a, b = c, a
            y = x
        return a
class Solution {
    public int crackNumber(int ciphertext) {
        int a = 1, b = 1, x, y = ciphertext % 10;
        while(ciphertext > 9) {
            ciphertext /= 10;
            x = ciphertext % 10;
            int tmp = 10 * x + y;
            int c = (tmp >= 10 && tmp <= 25) ? a + b : a;
            b = a;
            a = c;
            y = x;
        }
        return a;
    }
}
class Solution {
public:
    int crackNumber(int ciphertext) {
        int a = 1, b = 1, x, y = ciphertext % 10;
        while(ciphertext > 9) {
            ciphertext /= 10;
            x = ciphertext % 10;
            int tmp = 10 * x + y;
            int c = (tmp >= 10 && tmp <= 25) ? a + b : a;
            b = a;
            a = c;
            y = x;
        }
        return a;
    }
};

复杂度分析:

  • 时间复杂度 O(N) N 为字符串 s 的长度,即数字 ciphertext 的位数 \log(ciphertext) ,其决定了循环次数。
  • 空间复杂度 O(1) 几个变量使用常数大小的额外空间。