1. 解密数字
现有一串神秘的密文 ciphertext,经调查,密文的特点和规则如下:
密文由非负整数组成
数字 0-25 分别对应字母 a-z
请根据上述规则将密文 ciphertext 解密为字母,并返回共有多少种解密结果。
示例 1:
输入: ciphertext = 216612
输出: 6
解释: 216612 解密后有 6 种不同的形式,分别是 "cbggbc","vggbc","vggm","cbggm","cqgbc" 和 "cqgm"
提示:
0 <= ciphertext < 231
这个问题可以通过动态规划来解决。我们需要找出一种方法,将一个给定的数字 ciphertext
转化为对应的翻译方式,具体翻译规则是:
- 每个数字(1-9)可以被单独翻译。
- 由两个数字组成的两位数(10-25)也可以翻译为一个字符。
动态规划解题思路
-
定义状态:
设dp[i]
表示到数字的第i
位时,所有可能的翻译方法数。我们需要计算dp[n]
,其中n
是ciphertext
的位数。 -
转移方程:
-
对于每一位数字
dp[i]=dp[i−1]+dp[i−2]dp[i] = dp[i-1] + dp[i-2]x[i]
,如果它与前一位数字x[i-1]
组成的两位数在 [10, 25] 范围内,则可以将这两位数作为一个字符翻译。因此,状态转移公式为:其中:
dp[i-1]
表示当前位数字单独翻译的情况。dp[i-2]
表示当前位数字与前一位数字组成的两位数翻译的情况(如果符合条件)。
-
如果
dp[i]=dp[i−1]dp[i] = dp[i-1]x[i-1]
和x[i]
组成的两位数不在 [10, 25] 范围内,则:
-
-
初始化:
dp[0] = 1
,表示“无数字”的翻译方法数。dp[1] = 1
,表示仅有一个数字时的翻译方法数。
-
最后的结果:
结果是dp[n]
,即数字ciphertext
的翻译方案数。
代码实现
方法一:字符串遍历
class Solution {
public int crackNumber(int ciphertext) {
String s = String.valueOf(ciphertext); // 将数字转换为字符串
int a = 1, b = 1; // 初始状态 dp[0] = 1, dp[1] = 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; // 更新 dp[i-1]
a = c; // 更新 dp[i]
}
return a; // 返回 dp[n]
}
}
方法二:数字求余
为了节省空间,我们可以直接利用数字的除法和取余操作来获取每一位数字。这个方法通过从右到左进行动态规划,避免了字符串的使用。
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; // 判断两位数是否在 10 到 25 之间
b = a; // 更新 dp[i-1]
a = c; // 更新 dp[i]
y = x; // 更新当前位数字
}
return a; // 返回 dp[n]
}
}
时间复杂度与空间复杂度
-
时间复杂度:
- 时间复杂度为
O(N)
,其中N
是ciphertext
的位数。每次循环进行常数时间的操作,因此总体时间复杂度是线性的。
- 时间复杂度为
-
空间复杂度:
- 方法一中,空间复杂度为
O(N)
,因为我们需要一个字符串来存储ciphertext
。 - 方法二中,空间复杂度为
O(1)
,因为只使用了常数空间来存储变量。
- 方法一中,空间复杂度为
总结
通过动态规划,我们能够有效地计算出给定数字的所有翻译方式。两种方法分别是字符串遍历和数字求余,通过优化空间使用,第二种方法可以将空间复杂度从 O(N)
降至 O(1)
。
2. 珠宝的最高价值
现有一个记作二维矩阵 frame
的珠宝架,其中 frame[i][j]
为该位置珠宝的价值。拿取珠宝的规则为:
- 只能从架子的左上角开始拿珠宝
- 每次可以移动到右侧或下侧的相邻位置
- 到达珠宝架子的右下角时,停止拿取
注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]
。
示例 1:
输入: frame = [[1,3,1],[1,5,1],[4,2,1]]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最高价值的珠宝
提示:
0 < frame.length <= 200
0 < frame[0].length <= 200
该题可以通过动态规划来求解,关键是利用状态转移方程来递推每个位置的最大珠宝价值。
解题思路:
我们有一个二维棋盘,每个格子里有珠宝,我们需要从左上角 (0, 0) 出发,按照规则(每次只能向右或向下移动)走到右下角 (m-1, n-1),目标是获得的珠宝最大总值。通过动态规划来逐步计算每个位置可能的最大珠宝总值。
动态规划解析:
-
状态定义:
设dp[i][j]
表示从起点 (0, 0) 到达位置 (i, j) 时所能收获的珠宝最大累计价值。 -
转移方程:
- 初始位置
dp[0][0] = frame[0][0]
,即起始位置的珠宝价值。 - 如果
i = 0
且j > 0
,只能从左边到达,dp[0][j] = dp[0][j-1] + frame[0][j]
。 - 如果
i > 0
且j = 0
,只能从上边到达,dp[i][0] = dp[i-1][0] + frame[i][0]
。 - 对于其他位置 (i, j),可以从左边 (
dp[i][j-1]
) 或从上边 (dp[i-1][j]
) 走到,选取两者中的较大值:dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + frame[i][j]
。
- 初始位置
-
空间优化:
由于dp[i][j]
只依赖于dp[i-1][j]
和dp[i][j-1]
,可以在原数组frame
上进行修改,不需要额外的二维数组来存储dp
,因此空间复杂度可以降为O(1)
。
代码实现:
class Solution {
public int jewelleryValue(int[][] frame) {
int m = frame.length, n = frame[0].length;
// 初始化第一行
for (int j = 1; j < n; j++) {
frame[0][j] += frame[0][j - 1];
}
// 初始化第一列
for (int i = 1; i < m; i++) {
frame[i][0] += frame[i - 1][0];
}
// 动态规划计算剩余部分
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
frame[i][j] += Math.max(frame[i][j - 1], frame[i - 1][j]);
}
}
// 返回右下角的珠宝最大值
return frame[m - 1][n - 1];
}
}
代码解释:
-
初始化第一行和第一列:
第一行每个元素只能从左边过来,所以我们把每个元素加上它左边的值;第一列每个元素只能从上面过来,所以加上它上面格子的值。 -
递推计算其他格子:
从第二行第二列开始,对于每个格子,我们取它上边和左边的最大值,加上当前格子的珠宝值,得到从起点到该格子的最大珠宝值。 -
返回结果:
最终右下角的值就是从左上角到右下角的最大珠宝价值。
时间复杂度和空间复杂度:
-
时间复杂度:
时间复杂度为O(M * N)
,其中M
和N
分别是矩阵的行数和列数,因为我们需要遍历整个矩阵。 -
空间复杂度:
空间复杂度为O(1)
,因为我们是在原矩阵上进行修改,没有额外使用空间。
这个解法高效且简单,适用于大多数二维矩阵的动态规划问题。
3. 统计结果概率
你选择掷出 num
个色子,请返回所有点数总和的概率。
你需要用一个浮点数数组返回答案,其中第 i
个元素代表这 num
个骰子所能掷出的点数集合中第 i
小的那个的概率。
示例 1:
输入:num = 3
输出:[0.00463,0.01389,0.02778,0.04630,0.06944,0.09722,0.11574,0.12500,0.12500,0.11574,0.09722,0.06944,0.04630,0.02778,0.01389,0.00463]
示例 2:
输入:num = 5
输出:[0.00013,0.00064,0.00193,0.00450,0.00900,0.01620,0.02636,0.03922,0.05401,0.06944,0.08372,0.09452,0.10031,0.10031,0.09452,0.08372,0.06944,0.05401,0.03922,0.02636,0.01620,0.00900,0.00450,0.00193,0.00064,0.00013]
提示:
1 <= num <= 11
方法一:暴力法
暴力法是通过遍历所有的点数组合来统计每种点数和的出现概率。给定 n
个骰子,每个骰子的点数范围为 1 到 6,因此骰子的所有组合一共有 6^n
种。对于每一种组合,计算其点数和,并统计每种点数和出现的次数。
- 这种方法的时间复杂度为
O(6^n)
,随着n
增大,计算量急剧增加,因此当n
较大时,暴力法的效率会显得十分低下。特别是题目中n
的范围是 1 到 11,因此暴力法不适用于较大的n
。
方法二:动态规划
在动态规划方法中,设 f(n, x)
表示使用 n
个骰子得到点数和 x
的概率。通过递推关系可以求解每种点数和的概率。
状态转移:
- 当已知
n-1
个骰子的解f(n-1)
时,加入第n
个骰子后,可以通过已知的概率计算f(n,x)
。 - 具体来说,点数和为
x
的概率可以通过考虑骰子的每个点数(从 1 到 6)来计算,依次累加前n-1
个骰子中对应的点数和的概率。
递推公式如下:
f(n,x)=∑i=16f(n−1,x−i)×16f(n, x) = \sum_{i=1}^{6} f(n-1, x-i) \times \frac{1}{6}
解决越界问题:
在上述递推公式中,存在越界的风险(即 x-i
小于零)。为了解决这个问题,我们可以将状态转移的方向调整为“正向”,即从 f(n-1, x)
转移到 f(n, x)
。这样可以避免访问无效的状态。
动态规划优化:
- 空间优化:由于
f(n, x)
只依赖于f(n-1, x+1)
到f(n-1, x+6)
,因此可以使用两个一维数组交替存储结果,减少空间复杂度。 - 时间复杂度:时间复杂度为
O(n^2)
,因为每个状态的转移涉及到最多 6 次操作,且总共有n
次状态转移。
代码实现:
class Solution {
public double[] statisticsProbability(int num) {
// 初始化 dp 数组,代表 1 个骰子时的概率
double[] dp = new double[6];
Arrays.fill(dp, 1.0 / 6.0);
// 动态规划计算概率
for (int i = 2; i <= num; i++) {
double[] tmp = new double[5 * i + 1];
for (int j = 0; j < dp.length; j++) {
for (int k = 0; k < 6; k++) {
tmp[j + k] += dp[j] / 6.0;
}
}
dp = tmp;
}
return dp;
}
}
复杂度分析:
- 时间复杂度:
O(n^2)
。每一轮状态转移有多次内循环操作,具体的复杂度取决于骰子的数量n
。 - 空间复杂度:
O(n)
。由于使用了两个一维数组交替存储结果,空间复杂度被优化为线性级别。
总结:
- 暴力法适用于
n
较小的情况,但对于n
较大的情况效率低下。 - 动态规划方法则适合大规模数据,通过优化空间和时间复杂度,使得问题能够在较大范围内有效解决。
4. 破冰游戏
社团共有 num
位成员参与破冰游戏,编号为 0 ~ num-1
。成员们按照编号顺序围绕圆桌而坐。社长抽取一个数字 target
,从 0 号成员起开始计数,排在第 target
位的成员离开圆桌,且成员离开后从下一个成员开始计数。请返回游戏结束时最后一位成员的编号。
示例 1:
输入:num = 7, target = 4
输出:1
示例 2:
输入:num = 12, target = 5
输出:0
提示:
1 <= num <= 10^5
1 <= target <= 10^6
该问题是著名的 约瑟夫环 问题,其核心是求解一个环形数组中最终剩下的数字。这个问题通常使用动态规划或递推的方法来优化计算,避免暴力法的高时间复杂度。
问题描述:
给定 n
个人和一个目标 m
,从第一个人开始按顺序每次删除第 m
个人,直到最后只剩一个人,返回最后剩下的人的编号(编号从 0 开始)。
解题思路:
-
模拟法: 直接模拟每轮删除过程,可以将每个数字视作一个环中的节点。每次删除第
m
个节点,直到只剩一个节点。但由于这个方法需要频繁遍历链表,时间复杂度高,无法满足较大的n
和m
范围。 -
动态规划法: 该问题可以通过递推的方式来求解,利用已知小规模问题的解推导出大规模问题的解。设
f(n)
为长度为n
的数字环,删除第m
个数字后的解。递推关系式为:
f(n)=(f(n−1)+m)%nf(n) = (f(n-1) + m) \% nf(1)
为 0,因为当环中只有一个元素时,剩下的数字必然是 0。- 从
f(1)
推算到f(n)
即可得到最终解。
-
状态转移:
- 状态定义:
dp[i]
表示i
个数字时,最后剩下的数字。 - 递推方程:
dp[i] = (dp[i-1] + m) % i
- 初始条件:
dp[1] = 0
- 状态定义:
-
优化空间复杂度:
- 由于
dp[i]
仅依赖于dp[i-1]
,可以使用一个变量进行递推,从而将空间复杂度优化到O(1)
。
- 由于
代码实现:
class Solution {
public int iceBreakingGame(int num, int target) {
int x = 0;
for (int i = 2; i <= num; i++) {
x = (x + target) % i;
}
return x;
}
}
复杂度分析:
- 时间复杂度:
O(n)
。通过迭代从i=2
到i=n
,每次状态转移只需要O(1)
时间。 - 空间复杂度:
O(1)
。只用了一个额外的变量x
来存储当前的结果,避免了使用额外的数组。
例子:
假设 n = 5
,m = 3
,我们从 f(1) = 0
开始,通过递推逐步得到 f(2)
, f(3)
, f(4)
, f(5)
的值:
f(1) = 0
(初始状态)f(2) = (f(1) + 3) % 2 = (0 + 3) % 2 = 1
f(3) = (f(2) + 3) % 3 = (1 + 3) % 3 = 1
f(4) = (f(3) + 3) % 4 = (1 + 3) % 4 = 0
f(5) = (f(4) + 3) % 5 = (0 + 3) % 5 = 3
最终结果为 f(5) = 3
,表示最后剩下的数字是 3。
这种方法有效避免了暴力法的高时间复杂度,并且能够高效地解决大范围的输入问题。