【算法】回溯算法的基本原理与实例:华为OD机考双机位A卷 - 乘坐保密电梯

华为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 深度优先搜索

特性 深度优先搜索 回溯算法
核心 遍历所有节点 寻找可行解
状态管理 通常不修改原状态 需要记录和恢复状态
目的 图/树的遍历 组合优化问题
效率 可能遍历所有路径 通过剪枝减少搜索

五、适用场景

回溯算法特别适合解决以下类型的问题:

  1. 组合问题:从N个数中找出k个数的所有组合

  2. 排列问题:N个数的全排列

  3. 子集问题:求一个集合的所有子集

  4. 棋盘问题:N皇后、数独、解迷宫

  5. 切割问题:字符串分割、IP地址划分

  6. 其他约束满足问题

六、为什么在递归调用之后恢复?

1. 探索当前路径

当我们在递归调用之前 设置了某个选择(比如标记 visited[i] = true),我们就创建了一个特定的探索路径 。然后调用 dfs 去探索这个路径下的所有可能性。

2. 完全探索当前分支

dfs 函数会递归地探索从这个选择出发的所有可能性 。只有当这个分支的所有可能性都探索完了,dfs 才会返回。

3. 准备探索下一个分支

dfs 返回时,意味着当前选择对应的所有可能性 都已经探索完毕。为了探索下一个选项,我们需要:

  • 撤销当前选择(恢复状态)

  • 回到原始状态

  • 然后尝试下一个选项

4.一个具体例子

假设我们有楼层 [1, 2, 3],目标值 10visited 初始为 [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;
}
相关推荐
六bring个六2 小时前
自实现线程池
c++·线程池
McGrady-1752 小时前
portal 在scene graph 中怎么生成?
算法·机器人
老王熬夜敲代码2 小时前
C++新特性:string_view
开发语言·c++·笔记
川西胖墩墩3 小时前
智能体在科研辅助中的自动化实验设计
人工智能·算法
ouliten3 小时前
石子合并模型
c++·算法
weixin_461769403 小时前
5. 最长回文子串
数据结构·c++·算法·动态规划
补三补四3 小时前
XGBoost(eXtreme Gradient Boosting)算法的核心原理与底层实现技术
算法·集成学习·boosting
多打代码3 小时前
2026.1.2 删除二叉搜索树中的节点
开发语言·python·算法
渡我白衣3 小时前
计算机组成原理(12):并行进位加法器
网络协议·tcp/ip·算法·信息与通信·tcpdump·计组·数电