Hard
题目描述
有一个保险箱,由密码保护。密码是一个由 n 位数字组成的序列,其中每个数字都在范围 [0, k - 1] 内。
保险箱有一种特殊的密码验证方式。当你输入一个序列时,每次输入一个数字,它都会检查你输入的最近 n 位数字。
例如,正确的密码是 “345”,你输入了 “012345”:
- 输入 0 后,最近的 3 位数字是 “0”,不正确。
- 输入 1 后,最近的 3 位数字是 “01”,不正确。
- 输入 2 后,最近的 3 位数字是 “012”,不正确。
- 输入 3 后,最近的 3 位数字是 “123”,不正确。
- 输入 4 后,最近的 3 位数字是 “234”,不正确。
- 输入 5 后,最近的 3 位数字是 “345”,正确,保险箱打开。
返回能在某个时刻解开保险箱的最短字符串。
示例 1:
输入: n = 1, k = 2
输出: "10"
解释: 密码是一位数字,所以输入每个数字。"01" 也能解开保险箱。
示例 2:
输入: n = 2, k = 2
输出: "01100"
解释: 对于每个可能的密码:
- "00" 从第4位数字开始输入。
- "01" 从第1位数字开始输入。
- "10" 从第3位数字开始输入。
- "11" 从第2位数字开始输入。
因此 "01100" 能解开保险箱。"10011" 和 "11001" 也能解开保险箱。
约束条件:
- 1 <= n <= 4
- 1 <= k <= 10
- 1 <= k^n <= 4096
解题思路
这个问题本质上是寻找一个最短的字符串,使得所有可能的 n 位密码都作为子串出现在其中。这等价于在一个有向图中找欧拉回路。
图论建模思路:
我们可以将问题转化为图论问题:
- 节点:所有长度为 (n-1) 的字符串,共有 k^(n-1) 个节点
- 边:从节点 u 到节点 v 的边表示一个长度为 n 的密码,其中前 (n-1) 位是 u,后 (n-1) 位是 v
- 目标:找到一条经过所有边恰好一次的路径(欧拉路径)
具体实现:
- 使用 Hierholzer 算法寻找欧拉回路
- 从任意节点开始(比如全0字符串)
- 使用DFS深度优先搜索,采用后序遍历的方式构造路径
- 为了避免过早陷入死胡同,我们在访问完所有出边后再将当前节点加入结果
算法优化:
- 使用集合记录已访问的边,避免重复访问
- 采用贪心策略,按字典序访问边,保证结果的一致性
- 最终结果长度为 k^n + n - 1
这种方法保证能找到包含所有可能密码的最短字符串。
代码实现
class Solution {
public:
string crackSafe(int n, int k) {
if (n == 1) {
string result = "";
for (int i = 0; i < k; i++) {
result += to_string(i);
}
return result;
}
unordered_set<string> visited;
string start(n - 1, '0');
string result;
function<void(const string&)> dfs = [&](const string& node) {
for (int i = k - 1; i >= 0; i--) {
string edge = node + to_string(i);
if (visited.find(edge) == visited.end()) {
visited.insert(edge);
dfs(edge.substr(1));
result += to_string(i);
}
}
};
dfs(start);
result += start;
return result;
}
};
class Solution:
def crackSafe(self, n: int, k: int) -> str:
if n == 1:
return ''.join(str(i) for i in range(k))
visited = set()
start = '0' * (n - 1)
result = []
def dfs(node):
for i in range(k - 1, -1, -1):
edge = node + str(i)
if edge not in visited:
visited.add(edge)
dfs(edge[1:])
result.append(str(i))
dfs(start)
return ''.join(result) + start
public class Solution {
public string CrackSafe(int n, int k) {
if (n == 1) {
var sb = new StringBuilder();
for (int i = 0; i < k; i++) {
sb.Append(i);
}
return sb.ToString();
}
var visited = new HashSet<string>();
var start = new string('0', n - 1);
var result = new List<char>();
void Dfs(string node) {
for (int i = k - 1; i >= 0; i--) {
var edge = node + i;
if (!visited.Contains(edge)) {
visited.Add(edge);
Dfs(edge.Substring(1));
result.Add((char)('0' + i));
}
}
}
Dfs(start);
return new string(result.ToArray()) + start;
}
}
var crackSafe = function(n, k) {
if (n === 1) {
let result = '';
for (let i = 0; i < k; i++) {
result += i;
}
return result;
}
const visited = new Set();
const totalNodes = Math.pow(k, n - 1);
let result = '0'.repeat(n);
function dfs(node) {
for (let i = 0; i < k; i++) {
const edge = node + i;
if (!visited.has(edge)) {
visited.add(edge);
const nextNode = edge.slice(1);
dfs(nextNode);
result += i;
}
}
}
dfs('0'.repeat(n - 1));
return result;
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(k^n) - 需要访问所有 k^n 个可能的密码组合 |
| 空间复杂度 | O(k^n) - 存储访问状态和递归调用栈的空间 |