1. 加密算法题目解析与实现思路
这个加密算法题目来自华为OD机试真题,考察的是二维矩阵中的路径搜索问题。题目要求我们根据给定的密码本(一个数字矩阵)和明文数字串,找到明文数字在密码本中的匹配路径,并将路径中每个数字的位置坐标拼接成密文。
1.1 问题核心理解
首先我们需要明确几个关键点:
- 密码本是一个M×M的二维数字矩阵
- 明文是一个数字序列,长度N (1 ≤ N ≤ 200)
- 在密码本中寻找明文数字序列时,必须满足:
- 相邻数字在矩阵中也必须相邻(上下左右,不包括对角线)
- 不能重复使用同一个单元格的数字
- 如果找到多个匹配路径,选择字典序最小的那个
- 如果找不到匹配路径,返回"error"
1.2 算法选择分析
这个问题本质上是一个图搜索问题,我们需要在二维矩阵中寻找一条路径,使得路径上的数字序列与明文数字序列完全匹配。由于需要找到所有可能的路径并选择字典序最小的,深度优先搜索(DFS)是最合适的选择。
DFS的优势在于:
- 可以系统地探索所有可能的路径
- 容易实现回溯,避免重复使用单元格
- 可以按特定顺序搜索(如按行优先顺序),便于找到字典序最小的解
2. 详细实现方案
2.1 数据结构设计
我们需要设计几个关键数据结构:
- 密码本表示:使用二维数组存储
- 访问标记:使用同等大小的二维布尔数组标记已访问的单元格
- 路径存储:使用列表存储当前搜索路径的坐标
2.2 核心算法流程
算法的主要步骤如下:
- 预处理输入数据
- 读取明文数字序列
- 读取密码本矩阵
- 对于明文第一个数字,在密码本中找到所有出现位置作为搜索起点
- 对每个起点,启动DFS搜索:
- 标记当前单元格为已访问
- 检查是否匹配明文的下一个数字
- 递归搜索相邻单元格
- 回溯时取消标记
- 收集所有完整匹配路径
- 选择字典序最小的路径作为结果
2.3 关键实现细节
2.3.1 DFS实现要点
java复制private void dfs(int[][] book, boolean[][] visited, int[] plaintext, int index,
int row, int col, List<String> path, List<List<String>> results) {
// 终止条件:匹配完所有明文数字
if (index == plaintext.length) {
results.add(new ArrayList<>(path));
return;
}
// 检查当前单元格是否匹配
if (row < 0 || row >= book.length || col < 0 || col >= book[0].length
|| visited[row][col] || book[row][col] != plaintext[index]) {
return;
}
// 标记并记录路径
visited[row][col] = true;
path.add(row + " " + col);
// 搜索四个方向
dfs(book, visited, plaintext, index + 1, row - 1, col, path, results); // 上
dfs(book, visited, plaintext, index + 1, row + 1, col, path, results); // 下
dfs(book, visited, plaintext, index + 1, row, col - 1, path, results); // 左
dfs(book, visited, plaintext, index + 1, row, col + 1, path, results); // 右
// 回溯
visited[row][col] = false;
path.remove(path.size() - 1);
}
2.3.2 字典序比较
当找到多个匹配路径时,我们需要选择字典序最小的。可以直接将路径转换为字符串进行比较:
java复制private String selectMinPath(List<List<String>> paths) {
if (paths.isEmpty()) return "error";
paths.sort((a, b) -> {
String s1 = String.join(" ", a);
String s2 = String.join(" ", b);
return s1.compareTo(s2);
});
return String.join(" ", paths.get(0));
}
3. 完整代码实现
3.1 Java实现
java复制import java.util.*;
public class EncryptionAlgorithm {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取明文
int n = scanner.nextInt();
int[] plaintext = new int[n];
for (int i = 0; i < n; i++) {
plaintext[i] = scanner.nextInt();
}
// 读取密码本
int m = scanner.nextInt();
int[][] book = new int[m][m];
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
book[i][j] = scanner.nextInt();
}
}
// 加密处理
String ciphertext = encrypt(plaintext, book);
System.out.println(ciphertext);
}
public static String encrypt(int[] plaintext, int[][] book) {
List<List<String>> allPaths = new ArrayList<>();
// 找到所有可能的起点
for (int i = 0; i < book.length; i++) {
for (int j = 0; j < book[0].length; j++) {
if (book[i][j] == plaintext[0]) {
boolean[][] visited = new boolean[book.length][book[0].length];
List<String> path = new ArrayList<>();
dfs(book, visited, plaintext, 0, i, j, path, allPaths);
}
}
}
return selectMinPath(allPaths);
}
private static void dfs(int[][] book, boolean[][] visited, int[] plaintext, int index,
int row, int col, List<String> path, List<List<String>> results) {
if (index == plaintext.length) {
results.add(new ArrayList<>(path));
return;
}
if (row < 0 || row >= book.length || col < 0 || col >= book[0].length
|| visited[row][col] || book[row][col] != plaintext[index]) {
return;
}
visited[row][col] = true;
path.add(row + " " + col);
dfs(book, visited, plaintext, index + 1, row - 1, col, path, results);
dfs(book, visited, plaintext, index + 1, row + 1, col, path, results);
dfs(book, visited, plaintext, index + 1, row, col - 1, path, results);
dfs(book, visited, plaintext, index + 1, row, col + 1, path, results);
visited[row][col] = false;
path.remove(path.size() - 1);
}
private static String selectMinPath(List<List<String>> paths) {
if (paths.isEmpty()) return "error";
paths.sort((a, b) -> {
String s1 = String.join(" ", a);
String s2 = String.join(" ", b);
return s1.compareTo(s2);
});
return String.join(" ", paths.get(0));
}
}
3.2 Go实现
go复制package main
import (
"fmt"
"sort"
"strconv"
"strings"
)
func main() {
var n, m int
fmt.Scan(&n)
plaintext := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scan(&plaintext[i])
}
fmt.Scan(&m)
book := make([][]int, m)
for i := 0; i < m; i++ {
book[i] = make([]int, m)
for j := 0; j < m; j++ {
fmt.Scan(&book[i][j])
}
}
ciphertext := encrypt(plaintext, book)
fmt.Println(ciphertext)
}
func encrypt(plaintext []int, book [][]int) string {
var allPaths [][]string
for i := 0; i < len(book); i++ {
for j := 0; j < len(book[0]); j++ {
if book[i][j] == plaintext[0] {
visited := make([][]bool, len(book))
for k := range visited {
visited[k] = make([]bool, len(book[0]))
}
var path []string
dfs(book, visited, plaintext, 0, i, j, &path, &allPaths)
}
}
}
return selectMinPath(allPaths)
}
func dfs(book [][]int, visited [][]bool, plaintext []int, index, row, col int,
path *[]string, allPaths *[][]string) {
if index == len(plaintext) {
newPath := make([]string, len(*path))
copy(newPath, *path)
*allPaths = append(*allPaths, newPath)
return
}
if row < 0 || row >= len(book) || col < 0 || col >= len(book[0]) ||
visited[row][col] || book[row][col] != plaintext[index] {
return
}
visited[row][col] = true
*path = append(*path, fmt.Sprintf("%d %d", row, col))
dfs(book, visited, plaintext, index+1, row-1, col, path, allPaths)
dfs(book, visited, plaintext, index+1, row+1, col, path, allPaths)
dfs(book, visited, plaintext, index+1, row, col-1, path, allPaths)
dfs(book, visited, plaintext, index+1, row, col+1, path, allPaths)
visited[row][col] = false
*path = (*path)[:len(*path)-1]
}
func selectMinPath(paths [][]string) string {
if len(paths) == 0 {
return "error"
}
sort.Slice(paths, func(i, j int) bool {
s1 := strings.Join(paths[i], " ")
s2 := strings.Join(paths[j], " ")
return s1 < s2
})
return strings.Join(paths[0], " ")
}
4. 算法优化与性能分析
4.1 时间复杂度分析
最坏情况下,算法需要遍历所有可能的路径。对于M×M的密码本和长度为N的明文:
- 每个步骤有最多4个选择(4个方向)
- 路径长度为N
- 因此时间复杂度为O(4^N)
对于N=200的情况,这样的复杂度显然不可接受。但在实际机考中,测试用例通常会限制N的大小,或者密码本设计使得搜索能快速终止。
4.2 优化策略
-
提前终止:一旦找到字典序最小的路径就可以立即返回,不需要搜索所有可能性。可以按行优先顺序搜索,这样第一个找到的完整路径就是字典序最小的。
-
记忆化搜索:可以记录中间状态,避免重复计算,但对于这个问题效果有限。
-
并行搜索:可以并行处理不同的起点,但实现复杂度较高。
4.3 优化后的DFS实现
java复制private String encryptOptimized(int[] plaintext, int[][] book) {
// 按行优先顺序搜索,第一个找到的路径就是字典序最小的
for (int i = 0; i < book.length; i++) {
for (int j = 0; j < book[0].length; j++) {
if (book[i][j] == plaintext[0]) {
boolean[][] visited = new boolean[book.length][book[0].length];
List<String> path = new ArrayList<>();
if (dfsOptimized(book, visited, plaintext, 0, i, j, path)) {
return String.join(" ", path);
}
}
}
}
return "error";
}
private boolean dfsOptimized(int[][] book, boolean[][] visited, int[] plaintext,
int index, int row, int col, List<String> path) {
if (index == plaintext.length) {
return true;
}
if (row < 0 || row >= book.length || col < 0 || col >= book[0].length
|| visited[row][col] || book[row][col] != plaintext[index]) {
return false;
}
visited[row][col] = true;
path.add(row + " " + col);
// 按上、下、左、右的顺序搜索,确保字典序
if (dfsOptimized(book, visited, plaintext, index + 1, row - 1, col, path)) return true;
if (dfsOptimized(book, visited, plaintext, index + 1, row + 1, col, path)) return true;
if (dfsOptimized(book, visited, plaintext, index + 1, row, col - 1, path)) return true;
if (dfsOptimized(book, visited, plaintext, index + 1, row, col + 1, path)) return true;
visited[row][col] = false;
path.remove(path.size() - 1);
return false;
}
5. 测试用例与验证
5.1 示例测试用例
示例1:
输入:
code复制1
3
3
0 0 2
1 3 4
6 6 4
输出:
code复制1 1
示例2:
输入:
code复制2
0 3
3
0 0 2
1 3 4
6 6 4
输出:
code复制0 1 1 1
5.2 边界测试用例
- 最小密码本:
输入:
code复制1
0
1
0
输出:
code复制0 0
- 无解情况:
输入:
code复制1
5
2
0 1
2 3
输出:
code复制error
- 多解情况:
输入:
code复制2
0 0
2
0 0
0 0
输出:
code复制0 0 0 1
5.3 性能测试用例
对于较大的输入,我们需要测试算法的性能:
输入:
code复制10
0 1 2 3 4 5 6 7 8 9
10
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
...
(重复10行)
这个测试用例检查算法是否能处理较大输入而不超时。
6. 常见问题与调试技巧
6.1 常见错误
-
数组越界:在DFS中容易忘记检查行列边界
- 解决方法:在访问数组前总是检查 row 和 col 的范围
-
重复访问:忘记标记已访问的单元格导致无限循环
- 解决方法:确保在进入单元格时标记,回溯时取消标记
-
字典序处理不当:没有按正确顺序搜索导致结果不是字典序最小
- 解决方法:严格按照上、下、左、右的顺序搜索
6.2 调试技巧
- 打印中间状态:在DFS中加入调试打印,输出当前搜索路径
- 小规模测试:先用小密码本测试,验证基本逻辑正确
- 单元测试:为每个辅助函数编写测试用例
- 性能分析:对于较大输入,使用性能分析工具定位瓶颈
6.3 实际编码注意事项
- 输入处理:注意不同语言的输入读取方式差异
- 全局变量:避免使用全局变量,可能导致测试用例间干扰
- 内存管理:在Java中注意对象复用,在Go中注意切片操作
- 代码风格:保持代码整洁,添加必要注释
这个加密算法问题虽然题目描述较长,但核心是标准的DFS应用。通过系统化的分析和实现,可以确保代码的正确性和效率。在实际机考中,建议先处理好输入输出框架,再实现核心算法,最后添加优化和边界处理。