华为OD机考双机位A卷 - 乘坐保密电梯
题目描述
有一座保密大楼,你从0楼到达指定楼层m,必须这样的规则乘坐电梯:
给定一个数字序列,每次根据序列中的数字n,上升n层或者下降n层,前后两次的方向必须相反,规定首次的方向向上,自行组织序列的顺序按规定操作到达指定楼层。
求解到达楼层的序列组合,如果不能到达楼层,给出小于该楼层的最近序列组合。
输入描述
第一行:期望的楼层,取值范围[1,50];序列总个数,取值范围[1,23]
第二行:序列,每个值取值范围[1,50]
备注
操作电梯时不限定楼层范围。
必须对序列中的每个项进行操作,不能只使用一部分。
输出描述
能够达到楼层或者小于该楼层最近的序列
一、回溯算法的核心思想
回溯算法是一种系统性地搜索问题所有可能解 的算法,它的核心思想可以用一个词概括:"试探与回溯"。
二、回溯算法得核心要点
1.深度优先搜索 + 状态重置
回溯算法本质上是深度优先搜索的一种改进,关键在于当发现当前路径不可行时,能够返回上一步(回溯)并尝试其他路径。
cpp
void backtrack(当前状态) {
if (满足结束条件) {
记录结果;
return;
}
for (每个可能的选择) {
做出选择; // 前进
backtrack(新的状态); // 探索
撤销选择; // 回溯
}
}
2.决策树遍历
回溯算法将问题转化为决策树的遍历:
-
每个节点代表一个决策点
-
每个分支代表一个可能的选择
-
从根节点到叶子节点的路径代表一个完整的解决方案
3. "尝试-回退"机制
这是回溯最核心的特征:
-
尝试:选择一个分支,深入探索
-
回退:当发现当前路径不可能达到目标时,返回到上一个决策点
-
再尝试:选择另一个分支继续探索
三、回溯算法的关键特征
1.递归结构
cpp
// 典型结构
void backtrack(参数) {
// 1. 终止条件
if (到达终点) {
处理结果;
return;
}
// 2. 遍历所有选择
for (选择 : 所有可能的选择) {
// 3. 做选择
标记已选择;
路径.add(选择);
// 4. 递归探索(深度优先)
backtrack(新状态);
// 5. 撤销选择(关键!)
路径.remove(选择);
取消标记;
}
}
2.剪枝优化
cpp
void backtrack(参数) {
if (满足条件) {
记录结果;
return;
}
for (选择 : 所有选择) {
// 剪枝:如果这个选择明显不可行,直接跳过
if (!isValid(选择)) {
continue; // 剪枝
}
做选择;
backtrack(新状态);
撤销选择;
}
}
四、回溯 vs 深度优先搜索
| 特性 | 深度优先搜索 | 回溯算法 |
|---|---|---|
| 核心 | 遍历所有节点 | 寻找可行解 |
| 状态管理 | 通常不修改原状态 | 需要记录和恢复状态 |
| 目的 | 图/树的遍历 | 组合优化问题 |
| 效率 | 可能遍历所有路径 | 通过剪枝减少搜索 |
五、适用场景
回溯算法特别适合解决以下类型的问题:
-
组合问题:从N个数中找出k个数的所有组合
-
排列问题:N个数的全排列
-
子集问题:求一个集合的所有子集
-
棋盘问题:N皇后、数独、解迷宫
-
切割问题:字符串分割、IP地址划分
-
其他约束满足问题
六、为什么在递归调用之后恢复?
1. 探索当前路径
当我们在递归调用之前 设置了某个选择(比如标记 visited[i] = true),我们就创建了一个特定的探索路径 。然后调用 dfs 去探索这个路径下的所有可能性。
2. 完全探索当前分支
dfs 函数会递归地探索从这个选择出发的所有可能性 。只有当这个分支的所有可能性都探索完了,dfs 才会返回。
3. 准备探索下一个分支
当 dfs 返回时,意味着当前选择对应的所有可能性 都已经探索完毕。为了探索下一个选项,我们需要:
-
撤销当前选择(恢复状态)
-
回到原始状态
-
然后尝试下一个选项
4.一个具体例子
假设我们有楼层 [1, 2, 3],目标值 10,visited 初始为 [false, false, false]。
cpp
// 第一次循环:i = 0
visited[0] = true; // 选择1
current.push_back(1);
dfs(...); // 探索所有以"1"开头的序列
// dfs返回后,意味着所有以"1"开头的序列都探索完了
current.pop_back();
visited[0] = false; // 撤销选择1,准备尝试选择2
// 第二次循环:i = 1
visited[1] = true; // 选择2
current.push_back(2);
dfs(...); // 探索所有以"2"开头的序列
5.可视化过程
cpp
初始状态: visited=[F,F,F], current=[]
选择1:
visited=[T,F,F], current=[1]
└── 探索所有以1开头的序列...
选择2:
visited=[T,T,F], current=[1,2]
└── 探索...
选择3:
visited=[T,T,T], current=[1,2,3]
└── 结束,回溯
回溯后: visited=[T,T,F], current=[1,2]
回溯后: visited=[T,F,F], current=[1]
回溯后: visited=[F,F,F], current=[]
选择2:
visited=[F,T,F], current=[2]
└── 探索所有以2开头的序列...
6.如果恢复在递归之前会怎样?
cpp
// 错误的写法:
for (int i = 0; i < n; i++) {
visited[i] = true; // 做选择
...
visited[i] = false; // 恢复状态(在递归调用之前!)
dfs(...); // 递归探索
}
这样会在递归调用前就恢复状态,导致递归函数内部看到的状态是错误的。递归函数认为自己还在探索某个分支,但实际上标记已经被清除。
七、华为OD机考双机位A卷 - 乘坐保密电梯源代码(C++版)
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/**
* @brief 深度优先搜索函数,用于寻找访问楼层的最优顺序
*
* @param floors 存储所有需要访问的楼层数的向量
* @param visited 标记各楼层是否已被访问的布尔向量
* @param level 目标楼层
* @param min 引用参数,用于保存当前找到的最小楼层差距
* @param cur 当前所在的楼层位置
* @param current 存储当前搜索路径的向量
* @param bestResult 引用参数,用于保存最优路径
* @param depth 当前搜索的深度(已访问的楼层数)
*/
void dfs(vector<int>& floors, vector<bool>& visited, int level, int& min, int cur, vector<int>& current, vector<int>& bestResult, int depth) {
// 递归终止条件:所有楼层都已访问
if (depth == floors.size()) {
// 计算当前位置与目标楼层的差距
int diff = abs(cur - level);
// 如果当前差距小于已知的最小差距,则更新最小差距和最优路径
if (diff < min) {
min = diff;
bestResult = current;
}
return;
}
// 遍历所有未访问的楼层
for (int i = 0; i < floors.size(); i++) {
// 如果当前楼层已被访问,则跳过
if (visited[i]) {
continue;
}
visited[i] = true;
// 将当前楼层加入到当前路径中
current.push_back(floors[i]);
// 根据当前访问次数的奇偶性调整当前位置:
if (depth % 2 == 0) {
cur += floors[i];
} else {
cur -= floors[i];
}
// 递归搜索下一层
dfs(floors, visited, level, min, cur, current, bestResult, depth + 1);
// 回溯:恢复当前位置和访问状态
if (depth % 2 == 0) {
cur -= floors[i];
} else {
cur += floors[i];
}
// 从当前路径中移除当前楼层
current.pop_back();
visited[i] = false;
}
}
int main() {
// 输入目标楼层level和需要访问的楼层数量nums
int level, nums;
cin >> level >> nums;
vector<int> floors(nums);
for (int i = 0; i < nums; i++) {
cin >> floors[i];
}
// 用于存储当前搜索路径的向量
vector<int> current;
// 用于存储最优路径的向量
vector<int> bestResult;
// 用于标记各楼层是否已被访问的布尔向量,初始化为false
vector<bool> visited(nums, false);
// 初始化最小差距为整数最大值
int min = INT_MAX;
// 电梯初始位置在0层
int cur = 0;
// 调用深度优先搜索函数寻找最优解
dfs(floors, visited, level, min, cur, current, bestResult, 0);
if (min == 0) {
// 如果找到完美匹配的路径(差距为0),则输出该路径
for (int i = 0; i < bestResult.size(); i++) {
cout << bestResult[i] << " ";
}
} else {
// 否则输出最小差距
cout << min << endl;
}
return 0;
}