C++刷题--递归回溯剪枝(二)

47. 全排列 II(较难)★★★★☆

方案一:

cpp 复制代码
class Solution {
public:
    // tmp: 标记数组,记录nums中每个元素是否已被使用
    // tmp[i] = false 表示nums[i]未被使用,true表示已被使用
    vector<bool> tmp;
    
    // path: 当前正在构建的排列路径(临时存储当前DFS路径上的元素)
    vector<int> path;
    
    // ret: 结果集合,使用set自动去重(因为nums可能有重复元素)
    // set会自动排序并去重相同的vector,避免重复排列
    set<vector<int>> ret;

    // 主函数:输入可能包含重复数字的数组,返回所有不重复的全排列
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> su;  // 最终返回的结果数组
        
        int n = nums.size();
        
        // 初始化tmp数组,所有元素初始状态为false(都未使用)
        for(int i = 0; i < n; ++i) {
            tmp.push_back(false);
        }
        
        // 启动深度优先搜索
        dfs(nums);
        
        // 将set中的结果转移到vector中返回
        // (因为set的迭代器遍历得到的是有序、去重后的结果)
        for(auto& i : ret) {
            su.push_back(i);
        }
        
        return su;
    }

    // 深度优先搜索函数:递归构建所有可能的排列
    void dfs(vector<int>& nums) {
        // 递归终止条件:当前路径长度等于原数组长度
        // 说明已经选够了n个元素,形成一个完整排列
        if (path.size() == nums.size()) {
            ret.insert(path);  // 将当前排列加入结果集(set自动去重)
            return;            // 返回上一层,继续搜索其他分支
        }
        
        // 遍历所有元素,尝试将未使用的元素加入当前排列
        for(int i = 0; i < tmp.size(); ++i) {
            // 剪枝:如果当前元素已被使用,则跳过
            if(tmp[i] == false) {
                // 做选择:将nums[i]加入当前路径
                path.push_back(nums[i]);
                // 标记该元素已被使用
                tmp[i] = true;
                
                // 递归进入下一层,继续填充下一个位置
                dfs(nums);
                
                // 撤销选择:回溯,恢复状态(关键步骤!)
                tmp[i] = false;      // 标记为未使用
                path.pop_back();     // 从路径中移除该元素
            }
        }
        // 循环结束,当前层的所有可能选择都已尝试完毕
    }
};

方案二:

cpp 复制代码
class Solution {
    // path: 当前正在构建的排列路径
    vector<int> path;
    
    // ret: 存储所有不重复的全排列结果
    vector<vector<int>> ret;
    
    // check: 标记数组,check[i]表示nums[i]是否已被使用
    // 大小固定为9(根据题目约束,nums长度不超过8,所以9足够)
    bool check[9];

public:
    // 主函数:输入可能包含重复数字的数组,返回所有不重复的全排列
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        // 关键步骤1:先对数组排序,让相同元素相邻
        // 这样便于后续剪枝去重
        sort(nums.begin(), nums.end());
        
        // 初始化check数组为false(C++全局/成员数组默认可能不为0,但这里依赖默认初始化)
        // 实际上应该显式初始化:memset(check, 0, sizeof(check));
        
        // 从第0个位置开始填充
        dfs(nums, 0);
        
        return ret;
    }

    // 深度优先搜索
    // pos: 当前要填充的位置(0, 1, 2, ... , nums.size()-1)
    void dfs(vector<int>& nums, int pos) {
        // 递归终止条件:所有位置都已填充完毕
        if(pos == nums.size()) {
            ret.push_back(path);  // 保存当前完整排列
            return;
        }

        // 遍历所有元素,尝试将未使用的元素填入当前pos位置
        for(int i = 0; i < nums.size(); i++) {
            
            // 【核心剪枝逻辑】判断能否使用nums[i]
            // 条件分解:
            // 1. check[i] == false:  nums[i]未被使用(基本前提)
            //
            // 2. i == 0 || nums[i] != nums[i-1] || check[i-1] != false
            //    这个条件确保不会生成重复排列,分为三种情况:
            //    
            //    情况A: i == 0
            //           第一个元素,前面没有元素,可以直接使用
            //
            //    情况B: nums[i] != nums[i-1]
            //           当前元素与前一个元素不同,不会产生重复,可以使用
            //
            //    情况C: check[i-1] != false (即check[i-1] == true)
            //           当前元素与前一个元素相同(nums[i] == nums[i-1])
            //           但前一个相同元素已经被使用了
            //           这意味着当前是在构建"第一个该元素"的子树,可以使用
            //
            //    隐含排除的情况:
            //           nums[i] == nums[i-1] 且 check[i-1] == false
            //           前一个相同元素还没被用,当前却要用这个相同的元素
            //           这会导致重复排列,必须跳过!
            
            if(check[i] == false && (i == 0 || nums[i] != nums[i - 1] || check[i - 1] != false)) {
                
                // 做选择:将nums[i]加入路径,标记为已使用
                path.push_back(nums[i]);
                check[i] = true;
                
                // 递归填充下一个位置
                dfs(nums, pos + 1);
                
                // 撤销选择:回溯,恢复现场
                path.pop_back();      // 从路径移除
                check[i] = false;     // 标记为未使用
            }
        }
    }
};

17.电话号码的字母组合(中等)★★★☆☆

cpp 复制代码
class Solution {
public:
    // 映射表:数字到对应字母字符串的映射
    // 例如:按数字键'2'可以选择'a','b','c'中的一个
    map<char, string> m = {
        {'2', "abc"},
        {'3', "def"},
        {'4', "ghi"},
        {'5', "jkl"},
        {'6', "mno"},
        {'7', "pqrs"},
        {'8', "tuv"},
        {'9', "wxyz"}
    };
    
    // ret: 存储所有可能的字母组合结果
    vector<string> ret;
    
    // path: 当前正在构建的字母组合路径(临时字符串)
    string path;

    // 主函数:输入数字字符串,返回所有可能的字母组合
    // 例如输入"23",返回["ad","ae","af","bd","be","bf","cd","ce","cf"]
    vector<string> letterCombinations(string digits) {
        int n = digits.size();
        
        // 处理空输入的边界情况
        if (n == 0) {
            return ret;  // 返回空数组
        }
        
        // 从第0个数字开始深度优先搜索
        dfs(digits, 0);
        
        return ret;
    }

    // 深度优先搜索函数
    // digits: 输入的数字字符串
    // pos: 当前要处理的数字位置(0, 1, 2, ...)
    void dfs(string& digits, int pos) {
        // 递归终止条件:当前组合长度等于数字串长度
        // 说明已经处理完所有数字,形成一个完整组合
        if (path.size() == digits.size()) {
            ret.push_back(path);  // 将当前组合加入结果集
            return;               // 返回上一层,继续搜索其他分支
        }
        
        // 获取当前位置对应的数字字符
        char c = digits[pos];
        
        // 通过映射表获取该数字对应的所有可选字母
        // 例如:c='2'时,s="abc"
        string s = m[c];
        
        // 遍历该数字对应的所有字母
        for (int i = 0; i < s.size(); ++i) {
            // 做选择:将当前字母加入路径
            path += s[i];
            
            // 递归处理下一个数字位置
            dfs(digits, pos + 1);
            
            // 撤销选择:回溯,移除最后加入的字母
            // 这样path才能复用,尝试该数字的下一个字母
            path.pop_back();
        }
        // 循环结束,当前数字的所有字母选择都已尝试完毕
    }
};

79. 单词搜索(中等)★★★☆☆

cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * @param board string字符串vector  二维字符网格
     * @param word string字符串          要搜索的目标单词
     * @return bool布尔型                是否存在这样的路径
     */
    
    // path: 当前构建的路径字符串(记录已匹配的字符序列)
    string path;
    
    // check: 访问标记数组,check[i][j]表示board[i][j]是否已被当前路径使用
    // 大小101x101足够覆盖一般题目约束(通常board不超过100x100)
    bool check[101][101] = {false};
    
    // ret: 结果标志,一旦找到匹配路径立即设为true,用于提前终止所有搜索
    bool ret = false;

    // 主函数:在二维网格中搜索是否存在一条路径能组成目标单词
    // 路径要求:相邻单元格必须上下左右相邻,且每个单元格只能使用一次
    bool exist(vector<string>& board, string word) {
        // 获取起始字符(单词的第一个字符)
        char c = word[0];
        int i, j;
        
        // 遍历网格中的每一个单元格作为起点
        for (i = 0; i < board.size(); ++i) {
            for (j = 0; j < board[0].size(); ++j) {
                // 从位置(i,j)开始深度优先搜索
                dfs(board, word, i, j, 0);
                
                // 提前退出:如果已经找到答案,无需继续搜索
                if (ret)
                    return true;
            }
        }
        // 所有起点都尝试完毕,仍未找到匹配路径
        return false;
    }

    // 深度优先搜索函数
    // board: 二维字符网格
    // word: 目标单词
    // i, j: 当前在网格中的行坐标和列坐标
    // k: 当前要匹配的目标字符在word中的索引(0, 1, 2, ...)
    void dfs(vector<string>& board, string word, int i, int j, int k) {
        // 【剪枝1】边界检查:坐标越界则直接返回
        // i < 0: 超出上边界
        // i >= board.size(): 超出下边界
        // j < 0: 超出左边界  
        // j >= board[0].size(): 超出右边界
        if (i < 0 || i >= board.size() || j < 0 || j >= board[0].size())
            return;

        // 【剪枝2】访问检查与字符匹配检查
        // check[i][j] == true: 当前单元格已被当前路径使用,不能重复使用
        // board[i][j] != word[k]: 当前单元格字符与目标字符不匹配
        if (check[i][j] || board[i][j] != word[k])
            return;

        // 【剪枝3】全局结果检查:如果已经找到答案,立即停止所有递归
        // 避免不必要的搜索,优化性能
        if (ret)
            return;

        // 【做选择】当前单元格符合要求,加入当前路径
        path.push_back(board[i][j]);
        check[i][j] = true;  // 标记为已访问

        // 【检查是否完成】当前路径已完全匹配目标单词
        if (path == word) {
            ret = true;  // 设置全局标志,通知所有递归层已找到答案
        }

        // 【递归搜索】向四个方向(上下左右)探索下一个字符
        // 注意:k+1表示要匹配word的下一个字符
        dfs(board, word, i - 1, j, k + 1);  // 上
        dfs(board, word, i + 1, j, k + 1);  // 下
        dfs(board, word, i, j - 1, k + 1);  // 左
        dfs(board, word, i, j + 1, k + 1);  // 右

        // 【撤销选择】回溯,恢复状态,尝试其他路径
        check[i][j] = false;  // 取消访问标记
        path.pop_back();      // 从当前路径移除该字符
    }
};
相关推荐
Ulyanov10 小时前
高保真单脉冲雷达导引头回波生成:Python建模与实践
开发语言·python·仿真·系统设计·单脉冲雷达
plus4s11 小时前
2月12日(70-72题)
算法
m0_6727033111 小时前
上机练习第24天
算法
Mr_WangAndy11 小时前
C++数据结构与算法_线性表_数组_概念动态数组,刷题
c++·二分查找·数组刷题·数组字符串逆序·零移动·有序数组的平方
阿猿收手吧!11 小时前
【C++】jthread:优雅终止线程新方案
开发语言·c++
lly20240611 小时前
《JavaScript 实例》
开发语言
edisao11 小时前
序幕-内部审计备忘录
java·jvm·算法
十五年专注C++开发11 小时前
C++中各平台表示Debug的宏
开发语言·c++·debug
shehuiyuelaiyuehao11 小时前
22Java对象的比较
java·python·算法