提高 C++ 程序的输入输出效率,尤其是在需要大量输入输出操作时。
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
[传送门]( 77. 组合 - 力扣(LeetCode) )
class Solution {
public:
vector<vector<int>> res;
//vector path;
void backtracking(int n, int k, int startIndex, vector<int>& path)
{
if(k == path.size())
{
res.push_back(path);
return;
}
for(int i = startIndex; i <= n; i++)
{
path.push_back(i);
backtracking(n, k, i+1, path);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<int> path;
backtracking(n, k, 1, path);
return res;
}
};
回溯算法是真的难啊!实在是抽象,很难准确地想象出它程序执行的过程,灵神的视频看到这里感觉他讲的实在是太快了,现在转战到[代码随想录]( 代码随想录的个人空间-代码随想录个人主页-哔哩哔哩视频 (bilibili.com) )了,他讲的真的很细节,并且它讲回溯算法总结了模板和技巧,狠狠支持一下他!首先看到这一题,它需要求出所有的组合,首先想到的是for循环,可是仔细一想,这种不定次数的循环不能简单的只用for循环来写,只能用递归里面写循环来写。这里的path数组可以将它定义为全局变量,也可以把它写入函数的参数里面,两种方法都可以。这里的path.pop_back()函数就体现了回溯的概念。举个例子像12记录完之后需要将2弹出来才能记录13,14,记录完14后需要将4和1都弹出来,将2放入然后录入23,24,所以pop函数十分重要,其次pop函数的位置也有要求,也只能在那个位置,最开始我放在了return前面,然后我发现记录14之后不能将1弹出,困扰了我很久。
由于蓝桥杯和力扣写代码的方式有所不同,所以我将main函数写法也贴在下面了
#include
using namespace std;
vector<vector<int> > v;
void backTracking(int k, int n, int startIndex, vector<int>& path)
{
if(k == path.size())
{
v.push_back(path);
return;
}
for(int i = startIndex; i <= n; i++)
{
path.push_back(i);
backTracking(k, n, i+1, path);
path.pop_back();
}
}
int main()
{
int n, k;
cin >> n >> k;
vector<int> path;
backTracking(k, n, 1, path);
for(int i = 0; i < v.size(); i++)
{
for(int j = 0; j < v[0].size(); j++)
{
cout << v[i][j] << " ";
}
cout << "\n";
}
return 0;
}
大致是一样的,都需要写一个回溯函数,主函数只需要遍历一下记录数据的二维数组就行。
剪枝操作
for(int i = startIndex; i <= n-(k-path.size())+1; i++)
替换for循环里的判断条件,可以达到剪枝的效果,举例n=4,k=3,它的长度为3,那么可以知道最多只能从2出发比如234,不可能从3出发,从3出发构不成长度为3的组合
[传送门]( 216. 组合总和 III - 力扣(LeetCode) )
class Solution {
public:
vector<vector<int>> res;
vector<int> v;
void backtracking(int n, int k, int startIndex)
{
if(v.size() == k)
{
if(accumulate(v.begin(), v.end(), 0) == n) res.push_back(v);
return;
}
for(int i = startIndex; i <= 9 && i+accumulate(v.begin(), v.end(), 0) <= n; i++)
{
v.push_back(i);
backtracking(n, k, i+1);
v.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n, k, 1);
return res;
}
};
这一题和上一题代码很相似,只有一点地方需要改。其他博客用的是sum记录值,而我用的是accumulate函数替代了sum的作用,然后在录入数据的时候只需要判断一下求和是否等于n,等于n就将它存入res数组,否则退出递归。
[传送门]( 17. 电话号码的字母组合 - 力扣(LeetCode) )
class Solution {
public:
string letterMap[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
string path;
vector<string> res;
void backtracking(string digits, int index)
{
if(path.size() == digits.size())
{
res.push_back(path);
return;
}
//完成数字对字符组合的映射
int curIndex = digits[index]-'0';//获取当前下标数字,并将其从字符转换到数字
string Curletter = letterMap[curIndex];//获取当前数字映射到的字符组合
for(int i = 0; i < Curletter.size(); i++)
{
path += Curletter[i];
backtracking(digits, index+1);
path.resize(path.size()-1);
}
}
vector<string> letterCombinations(string digits) {
if(digits.size() == 0) return {};
backtracking(digits, 0);
return res;
}
};
代码果然还是要一步步写清楚才容易理解,现在对回溯问题有了一定的理解了,能够大致分析出应该有什么参数,树的深度由digit的长度决定,宽度由for循环的长度决定。这道题卡住我的点子是没有很好的利用好字符数字对字符组合的映射关系,在表达方面有一定的缺陷。现在详细介绍一下实现过程:首先是index,这里的index与前两题的startIndex有所不同,前两题都是同一集合里进行组合,而这里是多个不同的集合进行组合,所以startIndex作用是排出前面已经选过了的元素,而index的作用是记录当前digit的下标指向哪个数字;然后我们需要取出这个数字对应的int curIndex = digits[index]-‘0’;取到数字之后就是要找到该数字对应的字符串,对应的代码就是string Curletter = letterMap[curIndex];这样就很好的将映射关系表达了出来。
[传送门]( 39. 组合总和 - 力扣(LeetCode) )
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& candidates, int target, int startIndex)
{
if(accumulate(path.begin(), path.end(), 0) == target)
{
res.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size() && accumulate(path.begin(), path.end(), 0) <= target; i++)
{
path.push_back(candidates[i]);
backtracking(candidates, target, i);
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0);
return res;
}
};
这道题顺利ac!!!我发现理解了模板 之后这个题目可真简单啊!这道题可以有重复的单个元素,所以递归的时候startIndex可以不需要+1,其他没有什么较大的改变。有的博主函数参数中还有sum,我这里用STL自带的算法accumulate求和,其实效果都是一样的。
[传送门]( 40. 组合总和 II - 力扣(LeetCode) )
自己写的,通过172/176个数据。错误:时间超限
class Solution {
public:
vector<vector<int>> res;
set<vector<int>> res1;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int startIndex)
{
if(accumulate(path.begin(), path.end(), 0) == target)
{
res1.insert(path);
return;
}
for(int i = startIndex; i < candidates.size() && candidates[i]+accumulate(path.begin(), path.end(), 0) <= target; i++)
{
path.push_back(candidates[i]);
backtracking(candidates, target, i+1);
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0);
res.assign(res1.begin(), res1.end());
return res;
}
};
看到这一题的时候我感觉和前面的相差不大,就是多了一个去重的条件,想到去重我第一时间想到的是set容器,因为set容器里面存储的数据不能是重复的,我就按照这个思路来做可是遇到某个测试案例超时了,只能说想法很不错,但是效果不好哈哈哈。
这是代码随想录给出的代码:用到了回溯算法中的去重
class Solution {
public:
vector<vector<int>> res;
set<vector<int>> res1;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int startIndex, vector<int>& visited)
{
if(accumulate(path.begin(), path.end(), 0) == target)
{
res.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size() && candidates[i]+accumulate(path.begin(), path.end(), 0) <= target; i++)
{
if(i != 0 && candidates[i] == candidates[i-1] && visited[i-1] == 0) continue;
path.push_back(candidates[i]);
visited[i] = 1;//表示已访问
backtracking(candidates, target, i+1, visited);
path.pop_back();
visited[i] = 0;//弹出后更新成未访问
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<int> visited(candidates.size(), 0);
backtracking(candidates, target, 0, visited);
return res;
}
};
首先他讲到去重分为两种:①树枝(纵向)去重(像测试案例中的116是符合条件的,因为给出的数组中有两个1,他们只是数值相同但其实是不同的数字,这种在题意中我们是不能去除掉)②树层(横向)去重(像测试案例中第一个1的17和第二个1的17是两相同的解,所以他们只能出现一次)。在这里我最开始想到的是在for循环内的第一行加上if(i != 0 && candidates[i] == candidates[i-1]) continue但是运行的结果连116都没有了,所以这一条语句会将树枝和树层都去重又达不到理想的效果。这里最佳的解法就是在函数参数中放一个长度为已知数组的长度、初始化为0 的visited数组表示他们都未访问,1则表示已访问,操作与path数组同步就能达到题目的要求。
[传送门]( 131. 分割回文串 - 力扣(LeetCode) )
class Solution {
public:
vector<string> path;
vector<vector<string>> res;
bool isSame(string s, int left, int right)
{
int i = left, j = right;
while(i <= right)
{
if(s[i] != s[j]) return false;
i++;
j--;
}
return true;
}
void backtracking(string s, int startIndex)//startIndex表示分割线
{
if(startIndex == s.size())
{
res.push_back(path);
return;
}
for(int i = startIndex; i < s.size(); i++)
{
if(isSame(s, startIndex, i))//表示它是回文字串,如果不是回文字串i就继续往后挪
{
string temp;
//截取[startIndex,i]区间的字符串,这里也可以用s.substr(startIndex, i-startIndex+1)
path.push_back(temp.assign(s, startIndex, i-startIndex+1));
backtracking(s, i+1);
path.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return res;
}
};
这一道题其实隐含的就是一道组合问题,但是我想不出来怎么表示它。难点就在表示回文串这里:首先是分割线用startIndex表示,当分割线指向字符后一个位置即startIndex == s.size()的时候表示递归到叶子结点,这个时候退出递归;然后就是需要自定义一个判断字串是否回文的函数isSame,参数分别是s,left和right这样才能截取到s的字串,通过两个指针遍历一遍字串就能判断出该字串是否回文;再就是如何拿到s的字串,同样是需要左右边界,利用s.substr(startIndex, i-startIndex+1)来截取该段字符,第一个参数表示截取开始的下标,第二个参数表示截取的长度。还有一种方法是定义一个string类型的temp,用temp.assign(s, startIndex, i-startIndex+1)存储字串。三个参数分别表示截取的目标字符串、截取的开始下标、截取长度。
[传送门]( 78. 子集 - 力扣(LeetCode) )
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums, int startIndex, vector<int>& visited)
{
if(visited[nums.size()-1] == 1)
{
return;
}
for(int i = startIndex; i < nums.size(); i++)
{
path.push_back(nums[i]);
visited[i] = 1;
res.push_back(path);
backtracking(nums, i+1, visited);
path.pop_back();
visited[i] = 0;
}
}
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> visited(nums.size(), 0);
backtracking(nums, 0, visited);
res.push_back({});
return res;
}
};
这一题还是相对来说比较简单的,通过话树图可以观察到我们在每个结点都需要将记录保存到path中,而之前做的组合题中都是在叶子结点进行的录入数据,所以只要将res.push_back移到for循环体内就行。
[传送门]( 90. 子集 II - 力扣(LeetCode) )
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums, int startIndex, vector<int>& visited)
{
if(visited[nums.size()-1] == 1)
{
return;
}
for(int i = startIndex; i < nums.size(); i++)
{
if(i != 0 && nums[i] == nums[i-1] && visited[i-1] == 0) continue;
path.push_back(nums[i]);
visited[i] = 1;
res.push_back(path);
backtracking(nums, i+1, visited);
path.pop_back();
visited[i] = 0;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<int> visited(nums.size(), 0);
backtracking(nums, 0, visited);
res.push_back({});
return res;
}
};
这一题和上一题很像,只不过是多了个去重的条件,什么?去重!?ok那我妙了,直接把子集的答案粘过来,然后在for循环内加一条if(i != 0 && nums[i] == nums[i-1] && visited[i-1] == 0) continue;最后在调用回溯函数之前对nums数组进行排序,这样这道题就ac了
[传送门]( 491. 非递减子序列 - 力扣(LeetCode) )
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int> nums, int startIndex)
{
if(path.size() > 1) res.push_back(path);
unordered_set<int> uset;//记录当前层是否用过的哈希表
for(int i = startIndex; i < nums.size(); i++)
{
if(!path.empty() && path.back() > nums[i] || uset.find(nums[i]) != uset.end()) continue;
path.push_back(nums[i]);
uset.insert(nums[i]);
backtracking(nums, i+1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return res;
}
};
逻辑思路上还是需要锻炼啊,我能分析出这道题与之前题目之间的区别,但是就是不知道怎么用代码表达出来,这种感觉真的好难受啊。我原本是用的visited数组放到回溯函数中的参数,但我发现好像并不能表现出题目的意思。相较于之前的题目,这道题多了几个条件:①每层不能用相同数值的元素②要是非递减的。这里就需要用到哈希表来记录每层使用的元素,记住:数组能干的事哈希表都能干,还有就是unordered_map和unordered_set的区别在之前提到过。这里为什么uset的定义在for循环之前而不是在函数参数中?—>因为这一题的意思是每层不能有相同值的元素,所以层与层之间是不会互相影响的,那么这里的uset在每层都是一个新的uset所以在回溯的过程中不需要uset.erase(nums[i]),如果添加了这一句输出的答案就不符合题意了。再就是需要记下来的是:当使用哈希表的时候就难免需要找是否存在该元素,这里就用到了uset.find(nums[i]) != uset.end();这一段代码表示uset中有该元素。还有if里面的判断语句不能写成
if(!path.empty() && (path.back() > nums[i] || uset.find(nums[i]) != uset.end()))
[传送门]( 46. 全排列 - 力扣(LeetCode) )
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
unordered_set<int> uset;
void backtracking(vector<int>& nums)
{
if(path.size() == nums.size())
{
res.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++)
{
if(uset.find(nums[i]) != uset.end() && !uset.empty()) continue;
path.push_back(nums[i]);
uset.insert(nums[i]);
backtracking(nums);
path.pop_back();
uset.erase(nums[i]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
backtracking(nums);
return res;
}
};
全排列问题可以说是我的一个心魔啊,这次直接10分钟之内妙了,爽了!
[传送门]( 47. 全排列 II - 力扣(LeetCode) )
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& nums, vector<int>& visited)
{
if(path.size() == nums.size())
{
res.push_back(path);
return;
}
unordered_set<int> uset;//管横向的
for(int i = 0; i < nums.size(); i++)
{
if(uset.find(nums[i]) != uset.end() && !uset.empty()) continue;
if(visited[i] == 1) continue;
path.push_back(nums[i]);
uset.insert(nums[i]);
visited[i] = 1;
backtracking(nums, visited);
path.pop_back();
visited[i] = 0;
//uset.erase(nums[i]);这里不需要,横向去重,每层的uset互不影响
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<int> visited(nums.size(), 0);
backtracking(nums, visited);
return res;
}
};
这一题与上一题的区别在横向去重,visited数组用来纵向去重,uset哈希表用来横向去重。
[传送门]( 455. 分发饼干 - 力扣(LeetCode) )
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
int count = 0;
if(g.empty() || s.empty()) return 0;
sort(g.begin(), g.end());
sort(s.begin(), s.end());
if(g[0] > s.back()) return count;
int index = s.size()-1;
for(int i = g.size()-1; i >= 0; i--)
{
if(index >= 0 && s[index] >= g[i])
{
index--;
count++;
}
}
return count;
}
};
最开始按照题目的意思一步一步翻译成代码,只能通过一部分的代码,这样其实并没有用到贪心算法的思路。题解的思路是:用max(g[i])去找max(s[j]),符合条件就将两个指针往前移,这里确实有点双指针的意思,毕竟用到两个相关联的数组。这样从局部最优解推到全局最优解并且没有反例,那么这就是这道贪心的解法了。
[传送门]( 53. 最大子数组和 - 力扣(LeetCode) )
解法一:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res = INT_MIN, sum = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
res = max(res, sum);
if(sum < 0) sum = 0;
}
return res;
}
};
这个解法主要想到的是正负数的区别,如果sum是负数,那么无论下一个是什么数它都会比原来那个数小,所以在sum加到小于0的时候就会更新sum值为0,然后用res = max(res,sum)更新res的值。
解法二:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res = nums[0], temp;
for(int i = 1; i < nums.size(); i++)
{
temp = nums[i] = max(nums[i-1]+nums[i], nums[i]);
if(res < temp) res = temp;
}
return res;
}
};
这个解法更接近于动态规划,它更改了原数组nums。
注意这一题是当和为负数的时候将它重置为0,而不是遇到负数就重置,这两者还是有点区别的。
[传送门]( 122. 买卖股票的最佳时机 II - 力扣(LeetCode) )
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for(int i = 1; i < prices.size(); i++) res += max(prices[i]-prices[i-1], 0);
return res;
}
};
解题思路:关键在股票可以当天买当天卖,那么每次求出相邻两天的股票利润,大于0就累加到res中,这里用到max函数也是直接将if替代,使得代码十分简洁。
[传送门]( 55. 跳跃游戏 - 力扣(LeetCode) )
class Solution {
public:
bool canJump(vector<int>& nums) {
int cover = 0;
if(nums.size() == 1) return true;
for(int i = 0; i <= cover; i++)
{
cover = max(cover, i+nums[i]);
if(cover >= nums.size()-1) return true;
}
return false;
}
};
这个题目还是打破了惯性的思维,cover表示覆盖范围,那么循环体也就只能在覆盖范围内遍历,所以for循环内的判断条件是i <= cover,而不是i <= nums.size。当然这一题也需要跳出思维方式,怎么跳跃其实并不重要,重要的是我能跳跃的范围,只要当范围超过了最后一个下标我就一定能跳到。