代码随想录算法训练营第19天|回溯算法理论基础、77. 组合、组合优化、216.组合总和III、17.电话号码的字母组合

回溯算法理论基础

2025-02-19 10:32:52 星期三

文档讲解:代码随想录(programmercarl)回溯算法理论基础
视频讲解:《代码随想录》算法视频公开课:带你学透回溯算法(理论篇)

代码随想录视频内容简记

关于回溯

回溯问题实际上就是纯暴力搜索,不是特别高深的算法,其适用范围多是针对暴力无法解决,或者没办法写出代码的一些问题。

  1. 组合问题

  2. 切割问题

  3. 子集问题

  4. 排列问题

  5. 棋盘问题

回溯算法可以抽象为一棵N叉树,树的宽度表示了集合的大小,需要用for进行遍历,而树的深度则由递归的深度来构成

回溯法怎么用

回溯有模板

void (参数) {
	if (终止条件) {
		存放结果
		return
	}
	for (集合大小) {
		处理结点
		递归函数
		回溯操作(进行撤销)
	}
}

LeetCode77

题目描述:力扣77
文档讲解:代码随想录(programmercarl)77. 组合
视频讲解:《代码随想录》算法视频公开课:带你学透回溯算法-组合问题(对应力扣题目:77.组合)

代码随想录视频内容简记

回溯的三部曲和递归三部曲是一样的

梳理

  1. 确定函数的参数和返回值。

  2. 确定递归的终止条件

  3. 确定单层递归的逻辑

大致代码内容

  1. 首先是确定一个路径变量result<int> path,一个结果集vector<vector<int>> result定义一个没有返回值的函数void backtracking(n, k, index)

  2. 确定递归的终止条件,if (path.size() == k) result.push_back(path); return;

  3. 确定单层递归的逻辑。for (int i = index; i <= n; i++) path.push_back(i);这里就是对结点的处理,向末尾添加元素。backtracking(n, k, i + 1); 递归函数,注意,这里的i + 1作用就是让进入下一次递归的index参数赋值为i + 1,那么下一个for循环i的起始就会被赋值为index,防止再取相同的元素,即自己本身,因为本题是组合问题,所以不同排列的结果是一致的。path.pop_back(i);最后一步就是回溯的撤销

LeetCode测试

点击查看代码
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int n, int k, int index) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        for (int i = index; i <= n; i++) {
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

LeetCode77优化

视频讲解:组合问题的剪枝操作

代码随想录视频内容简记

关于77组合问题的剪枝优化,其实就是针对在对集合进行遍历的过程中,永远无法满足k大小要求的一些结果做删除。比如,输入n = 4, k = 4,那么在取1之后,剩余的2,3,4;取2之后,剩余的3,4;取3之后,剩余的4;取4之后,剩余的空。可见,剩余的后3种情况,都无法满足k = 4的要求,所以剪枝。

在这里,剪枝的核心就是对循环的终止条件做优化。我们知道,在每次取完一个i之后,剩余可供取的元素个数就会变成n - i(为了防止重复取),那么,能确保k的要求被满足的条件就是:可供取的元素个数一定要大于等于当前需要取的元素

for (int i = index; i <= 剪枝; i++) {
	path.push_back(i);
	backtracking(n, k, i + 1);
	path.pop_back();
}

在本题中,已经放入path的元素数量为path.size(),那么待添加的元素(需要取的)数量为k - path.size(),一旦出现ni>=kpath.size(),那么也就是i<=n(kpath.size()),又因为i的取值包括其本身,所以这里应该+1。其实这里不是特别清楚明白,但是姑且按照这么来理解。


后记:做到216的时候,k哥画了一个图易于理解这里

因为剩余算出来的n - (k - path.size())是还剩下的元素,是一个个数,要想让i指向那里,必须让i+1

LeetCode测试

点击查看代码
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int n, int k, int index) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        for (int i = index; i <= n - (k - path.size()) + 1; i++) {
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

LeetCode216

题目描述:力扣216
文档讲解:代码随想录(programmercarl)216.组合总和III
视频讲解:《代码随想录》算法视频公开课:和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III

代码随想录视频内容简记

组合总和的思路和组合也是一样的,区别就是多了一个sum的限制。而针对于这个sum,一开始自己先写代码的时候,有一个疑问,就是到底sum的和,还有path的大小,哪一个才是递归的终止条件呢?
事实上,两个都是,path.size() == k && sum == n,而且sum会多出来一个剪枝,这里要注意

梳理

依旧是递归(回溯)三部曲

  1. 确定函数的参数和返回值

  2. 确定递归的终止条件

  3. 确定单层递归的逻辑

大致代码内容

  1. 确定函数的参数和返回值void backtracking(int k, ink n, int sum, int index)这里会多出来一个参数sum

  2. 确定递归的终止条件,if (path.size() == k && sum == n) result.push_back(path); return;这里和77是一摸一样的

  3. 确定单层递归的逻辑。这里和77也没有什么变化,唯一的区别就是在对于递归的时候多传递一个参数

剪枝优化

对sum做剪枝

sum的剪枝,就是一旦sum大于了题目要求的和n,那么就直接返回

if (sum > n) return

对i做剪枝

和昨天的思路一样,对i的条件判断做剪枝,具体就是i < 9 - (k - path.size()) + 1

LeetCode测试

好久没有看时间复杂度了,这个是O(C(N,K)),😶不太明白

未剪枝

点击查看代码
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    int sum;
    void backtracking(int k, int n, int sum, int index) {
        if (path.size() == k && sum == n) {
            result.push_back(path);
            return;
        }
        for (int i = index; i <= 9; i++) {
            sum += i;
            path.push_back(i);
            backtracking(k, n, sum, i + 1);
            sum -= i;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n, 0, 1);
        return result;
    }
};

剪枝

点击查看代码
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    int sum;
    void backtracking(int k, int n, int sum, int index) {
        if (sum > n) return;
        if (path.size() == k && sum == n) {
            result.push_back(path);
            return;
        }
        for (int i = index; i <= 9 - (k - path.size()) + 1; i++) {
            sum += i;
            path.push_back(i);
            backtracking(k, n, sum, i + 1);
            sum -= i;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n, 0, 1);
        return result;
    }
};

LeetCode17

题目描述:力扣17
文档讲解:代码随想录(programmercarl)17.电话号码的字母组合
视频讲解:《代码随想录》算法视频公开课:还得用回溯算法!| LeetCode:17.电话号码的字母组合

代码随想录视频内容简记

首先,17题是一个两个集合的组合问题,这也是他和77还有216不同的地方,一次只在一个集合中去一个数。

另外需要注意的一点是本题递归的终止条件,回看77和216可以发现,递归的深度是和递归的终止条件紧紧相关的。77当中的k控制了path的大小,也就是递归的深度,216当中的k也同样控制path的大小(递归的深度),在本题中,因为一次只在一个集合中取一个元素,那么,集合的个数就控制了递归的深度。k哥用index来表示对集合的遍历

注意在做回溯题的时候,画出他的树形图很重要,很多就一目了然了

梳理

  1. 确定函数的参数和返回值

  2. 确定递归的终止条件

  3. 确定单层递归的逻辑

大致代码内容

  1. 在本题中,需要做一个数字到字母的映射,用map。另外就是void backtracking(string digits, int index)int digit = digits[index] - '0'用index指向每次需要取字符的数字(每个集合的索引),并-'0'表示转换成整形。之后可以用digit取出对应集合的字母元素string letter = letterMap[digit]

    还要注意的就是对string pathvector<string> result的定义,result仍然是一个二维的,因为里面的string是字符串

  2. 确定递归的终止条件,if (index == digits.size()) result.push_back(path); return;

  3. 确定单层递归的逻辑,for (int i = 0; i < letter.size(); i++),这里和前面的题显著不一样的地方就是int i = 0,而不是startIndex,因为这里不再是一个集合,不用去重。之后path.push_back(letter[i]); backtracking(digits, index + 1),注意这里的index + 1就很好地表示了直接进入下一个集合。path.pop_back();

LeetCode测试

感觉整个题最巧妙的地方就是每次递归,用index取digit,再用letter表示为字母的集合,然后就在递归中进行push和pop

在测试的时候出现了这种情况,就是因为index初始为0,而又因为digits.size()=0,相当于会添加一个空的path,也就是下图的两个双引号,我看了一下k哥给的是在主函数直接加了一个判断

完整代码如下

点击查看代码
class Solution {
private:
    string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
    vector<string> result;
    string path;

    void backtracking(string digits, int index) {
        if (index == digits.size()) {
            result.push_back(path);
            return;
        }
        int digit = digits[index] - '0';
        string letter = letterMap[digit];
        for (int i = 0; i < letter.size(); i++) {
            path.push_back(letter[i]);
            backtracking(digits, index + 1);
            path.pop_back();
        }
    }

public:
    vector<string> letterCombinations(string digits) {
        if (digits.size() == 0) return result;
        backtracking(digits, 0);
        return result;
    }
};
posted on   bnbncch  阅读(1011)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示