刷题的一些宗旨
1.首先自己尝试分析编写代码,自己没有思路就看代码随想录的思路,但是看完代码随想录的思路之后一定要自己尝试编写代码,保证自己对于思路的独立实现,可以参考代码随想录的代码简化自己的代码(主要指C++自带的模板和函数的使用)
2.后面刷题,一道题掌握一种方法就好,回来第二遍再来做拓展,这样快的话一天或许可以两道题,但是一种方法要掌握透。
9.7日任务:组合问题
思路和代码随想录一致,具体代码有不同,不过我觉得保证自己思考的独立性是要比看了参考代码之后尝试去写好很多的。
看了代码随想录的思路之后独立编写完成,整体来说还是比较简单,和二叉树递归相似非常大。
而且我的代码思路已经包含了剪枝优化。
class Solution {
public:
vector<vector<int>> result;
void backTracking(vector<int> vec, int leftIndex, int rightIndex, int k) {
//1.终止条件,不需要再找数值的时候
if (k == 0) {
result.push_back(vec);
return;
}
//顺序执行
for (int i = leftIndex; i <= rightIndex - k + 1; i++) {
vec.push_back(i);
backTracking(vec, i + 1, rightIndex, k-1);
vec.pop_back();
}
return;
}
vector<vector<int>> combine(int n, int k) {
//看了代码随想录的思路,还没看详细代码,想要自己尝试一下递归写法
//首先画一下树状图理一下思路
//自顶向下顺序执行
//自底向上回溯
//回溯,输入参数:数值区间,要取的树的个数,暂时不需要返回值
vector<int> tempvec;
backTracking(tempvec,1,n,k);
return result;
}
};
9.7日任务:组合综合三(当天第二道)
直接仿照前面的思路独立编写,但是显然剪枝做的不够。
class Solution {
public:
vector<vector<int>> result;
vector<int> tempresult;
int sum{0};
void backTracking(int startIndex, int endIndex, int k, int n) {
//终止条件
if (k==0) {
if (sum == n) result.push_back(tempresult);
return;
}
//顺序执行
for (int i = startIndex; i <= endIndex - k + 1; i++){
tempresult.push_back(i);
sum = sum + i;
backTracking(i + 1, endIndex, k - 1, n);
tempresult.pop_back();
sum = sum - i;
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
//和上一道组合题的区别不大,因为固定了数的数量
//回溯递归:输入参数:数的区间范围,要找的树的个数,数相加之和n
backTracking(1,9,k,n);
return result;
}
};
对剪枝进行精简
class Solution {
public:
vector<vector<int>> result;
vector<int> tempresult;
int sum{0};
void backTracking(int startIndex, int endIndex, int k, int n) {
//终止条件
if (k==0) {
if (sum == n) result.push_back(tempresult);
return;
}
//顺序执行
for (int i = startIndex; i <= endIndex - k + 1; i++){
tempresult.push_back(i);
sum = sum + i;
backTracking(i + 1, endIndex, k - 1, n);
tempresult.pop_back();
sum = sum - i;
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
//和上一道组合题的区别不大,因为固定了数的数量
//回溯递归:输入参数:数的区间范围,要找的树的个数,数相加之和n
backTracking(1,9,k,n);
return result;
}
};
上面代码里实际还多了一个无用的参数,即endIndex,其实可以替换成9.
底下的写法最简便
class Solution {
public:
vector<vector<int>> result;
vector<int> tempresult;
int sum{0};
void backTracking(int startIndex, int k, int n) {
//终止条件
if (sum > n) return;//这个算是剪枝
if (k==0) {
if (sum == n) result.push_back(tempresult);
return;
}
//顺序执行
for (int i = startIndex; i <= 9 - k + 1; i++){
tempresult.push_back(i);
sum = sum + i;
backTracking(i + 1, k - 1, n);
tempresult.pop_back();
sum = sum - i;
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
//和上一道组合题的区别不大,因为固定了数的数量
//回溯递归:输入参数:数的区间范围,要找的树的个数,数相加之和n
//因为是1-9,只取k个数
backTracking(1,k,n);
return result;
}
};
9.7日任务:电话号码的字母组合(当日第三题)
参考了代码随想录中构造号码数值与字符串的对应库,以及转化数字字母为数字的方法。回溯的思路之前在二叉树都有过,整体理解起来不是很难。
根据代码随想录的思路自行编写,思路很简单
class Solution {
public:
//这一步很重要,构建好数值与字母的对应
const 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 tempreult;
void backTracking(string digits, int currentIndex) {
//1.终止条件
if (currentIndex == digits.size()) {
result.push_back(tempreult);
return;
}
//顺序执行
int number = digits[currentIndex] - '0';
for (int i = 0; i < letterMap[number].size(); i++) {
tempreult += letterMap[number][i];
backTracking(digits, currentIndex + 1);
tempreult.pop_back();
}
return;
}
vector<string> letterCombinations(string digits) {
//先自己尝试走一下逻辑思路:
//这道题要用到字符串处理的一些东西,我虽然不会,先尝试把题目理解
//首先得拿到字符串顺序对应的数值顺序,然后做一个vector<vector<string>>的数组,然后进行回溯
//首先转数值并做好数值与字母的对应
//把数值转化为int的方法为int digit = digits[index] - '0';
//开始回溯递归,输入参数要有digits,当前对应的digits下标
if (digits.size() == 0) return result;
backTracking(digits,0);
return result;
}
};
9.8日任务:组合总和
按自己的思路走,这道题和前面的很类似,只要能画出解题的逻辑结构树状图,这个问题就很方便解决了。
class Solution {
public:
int sum{0};
vector<vector<int>> result;
vector<int> vec;
void backTracking(const vector<int> candidates, const int target, int leftIndex, int rightIndex) {
//终止条件
if (sum > target) return;
if (sum == target) {
result.push_back(vec);
return;
}
//顺序执行
for (int i = leftIndex; i <= rightIndex; i++) {
vec.push_back(candidates[i]);
sum += candidates[i];
backTracking(candidates, target, i, rightIndex);
sum -= candidates[i];
vec.pop_back();
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//这个问题前面已经刷过类似的了
//假如问题的逻辑可以画成树的结构,那么这道问题可以尝试用回溯法去求
//回溯函数输入参数: 整数数组candidates,目标整数,该级递归对应的元素索引,返回值暂时不需要
backTracking(candidates, target, 0, candidates.size()-1);
return result;
}
};
下面这种方法剪枝操作很棒:
1.对候选数组进行了排序
2.在循环过程中设立了终止条件,结合排序实现剪枝,减少了不必要的遍历操作。
class Solution {
public:
int sum{0};
vector<vector<int>> result;
vector<int> vec;
void backTracking(const vector<int> candidates, const int target, int leftIndex) {
//终止条件
if (sum > target) return;
if (sum == target) {
result.push_back(vec);
return;
}
//顺序执行
for (int i = leftIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
vec.push_back(candidates[i]);
sum += candidates[i];
backTracking(candidates, target, i);
sum -= candidates[i];
vec.pop_back();
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//这个问题前面已经刷过类似的了
//假如问题的逻辑可以画成树的结构,那么这道问题可以尝试用回溯法去求
//回溯函数输入参数: 整数数组candidates,目标整数,该级递归对应的元素索引,返回值暂时不需要
sort(candidates.begin(), candidates.end());
backTracking(candidates, target, 0);
return result;
}
};
9.9日任务:组合总和II
自己梳理思路自行编写
核心在于对于去重的理解,这道题并不难,只要能把解题逻辑的树状图画出来就很OK.
class Solution {
public:
vector<vector<int>> result;
vector<int> vec;
int sum{0};
void backTracking(const vector<int> candidates, const int target, int startIndex) {
if (sum == target) {
result.push_back(vec);
return;
}
//顺序执行
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
//下面这句代码就是用来去重的
if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
sum += candidates[i];
vec.push_back(candidates[i]);
backTracking(candidates, target, i + 1);
sum -= candidates[i];
vec.pop_back();
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
//这个问题的难点在于去重
//先排序
sort(candidates.begin(), candidates.end());
backTracking(candidates, target, 0);
return result;
}
};
9月10日任务:分割回文串
没见过,直接看代码随想录解析。拿到思路之后开始尝试独立编写。
1.回文串的判断(参考代码随想录的代码,很简洁)
2.回溯,只要问题分析清楚了,写出来非常方便。
3.如何定义一个string类型字符串,且这个字符串来自于一个长字符串的一部分?
左闭右开,注意一下
string tempString = string(s.begin() + startIndex, s.begin() + i + 1);//这句代码是一个左闭右开,得刻意注意一下
4.关于代码随想录给出的回文串提前计算法,提到了动态规划算法,所以这块应该和动态规划算法有相近之处,目前并不好理解,我根据代码把示意图简单画了下来。第二次回来可以重点看一下,这个方法一般情况下想不到这一点的。
只看了代码随想录的思路然后独立编写,对于回溯的理解更深了,回溯本身没有难度,关键是如何对问题进行拆解,只要问题拆解分析清楚了,就没有什么大问题了。
class Solution {
public:
vector<vector<string>> result;
vector<string> tempVec;
bool isPalindrome(const string s, int start, int end) {
//判断回文串的方案:双指针法,一个在前一个在后,从两边向中间聚拢
//start和end是左闭右闭
for(int i = start, j = end; i < j; i++,j--) {
if (s[i] != s[j]) return false;
}
return true;
}
void backTracking(const string s, int startIndex) {
//终止条件
if (startIndex > s.size() - 1) {
result.push_back(tempVec);
return;
}
//顺序执行
for (int i = startIndex; i < s.size(); i++) {
//首先判断是不是回文串
if (isPalindrome(s, startIndex, i)) {
//如何定义一个短字符串是从整长度字符串中的一部分,这句代码不确定对不对
string tempString = string(s.begin() + startIndex, s.begin() + i + 1);//这句代码是一个左闭右开,得刻意注意一下
tempVec.push_back(tempString);
backTracking(s,i+1);
tempVec.pop_back();
}
}
return;
}
vector<vector<string>> partition(string s) {
//两个难点:1.如何切割2.如何判断是否是回文串
//对于题目的分析理解:
//切割就是一个组合问题,第一切的方案是:不同长度的初始回文串
//先把判断回文串的函数写了
//然后用回溯,回溯输入参数:字符串s,要切分的字串初始索引
if (s.empty()) return result;
backTracking(s, 0);
return result;
}
};
还没完,回文串判断还可以精简,一会回来搞
上面代码在每一次回溯递归过程中要不断对每一小段字符串进行回文串判断,代码随想录给的快捷方式是提前计算好每一段字符串是否是回文串。
精简代码:
vector<vector<bool>> isPalindrome;
void computePalindrome(const string s) {
//初始化isPalindrome,全部初始化为false;
isPalindrome.resize(s.size(), vector<bool>(s.size(), false));
//
for (int i = s.size() - 1; i >= 0; i--) {
// 需要倒序计算, 保证在i行时, i+1行已经计算好了
for (int j = i; j < s.size(); j++) {
if (j == i) {isPalindrome[i][j] = true;}
else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);}
else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);}
}
}
return;
}
9月11日任务:复原IP地址
思路清晰,下午来编写代码,写了两个小时,搞错了,脑袋混乱,下午来找找状态.
下午又混乱了,解决问题了。
1.学习了在string中插入字符以及删除字符的方式,学习了insert,erase的方式。
2.自己在编写判定字符串IP地址合理方面考虑很难周全,需要反复试错时候才能完全正确。
这是根据自己思路编写的第一版代码,思路没有什么问题,代码终止条件漏掉了一个环节,即删掉了末尾的点,将正确的IP地址存放到result之后,并没有补充这个点,使得回溯过程中,多删了一个点,就这个bug卡了我非常之久,导致一直报错没有解决,最后是将代码移动到VisualStudio中一步一步debug找到的问题。
评价:对于回溯的细节理解还是不够到位,对于VisualStudio的debug使用也不够熟练,浪费掉了很多时间。
class Solution {
public:
vector<string> result;
string tempString;
bool isVaild(string s, int start, int end) {
//计算输入字符串的长度
//这个函数输入的字符串区间最大长度为3
int len = end - start + 1;
if (len > 3 || len <= 0) return false;
else {
if (len > 1 && s[start] == '0') return false;
if (len == 3 && ((s[start] - '0') * 100 + (s[start + 1] - '0') * 10 + (s[start + 2] - '0')) > 255) return false;
}
return true;
}
void backTracking(string s, int startIndex, int pointNum) {
//终止条件
if (pointNum == 4) {
if (startIndex == s.size()) {
tempString.pop_back();
result.push_back(tempString);
tempString += '.';
}
return;
}
//顺序执行
for (int i = startIndex; (i < startIndex + 3) && i < s.size(); i++) {
if (isVaild(s, startIndex, i)) {
pointNum++;
tempString += string(s.begin() + startIndex, s.begin() + i + 1) + '.';
backTracking(s, i + 1, pointNum);
int len = i - startIndex + 1 + 1;
for (int k = 0; k < len; k++) {
tempString.pop_back();
}
pointNum--;
}
}
return;
}
vector<string> restoreIpAddresses(string s) {
//先按照自己的思路来,和之前的回溯问题很相似
backTracking(s, 0, 0);
return result;
}
};
代码优化:优化思路是,当进入到IP地址的第四部分时,倘若长度大于3,直接返回就好
class Solution {
public:
vector<string> result;
string tempString;
bool isVaild(string s, int start, int end) {
//计算输入字符串的长度
//这个函数输入的字符串区间最大长度为3
int len = end - start + 1;
if (len > 3 || len <= 0) return false;
else {
if (len > 1 && s[start] == '0') return false;
if (len == 3 && ((s[start] - '0') * 100 + (s[start + 1] - '0') * 10 + (s[start + 2] - '0')) > 255) return false;
}
return true;
}
void backTracking(string s, int startIndex, int pointNum) {
//终止条件
//下面前两行代码是用来剪枝的,减少循环数量的,降低计算量的
if (pointNum == 2 && s.size() - startIndex > 6) return;
if (pointNum == 3 && s.size() - startIndex > 3) return;
if (pointNum == 4) {
if (startIndex == s.size()) {
tempString.pop_back();
result.push_back(tempString);
tempString += '.';
}
return;
}
//顺序执行
for (int i = startIndex; (i < startIndex + 3) && i < s.size(); i++) {
if (isVaild(s, startIndex, i)) {
pointNum++;
tempString += string(s.begin() + startIndex, s.begin() + i + 1) + '.';
backTracking(s, i + 1, pointNum);
int len = i - startIndex + 1 + 1;
for (int k = 0; k < len; k++) {
tempString.pop_back();
}
pointNum--;
}
}
return;
}
vector<string> restoreIpAddresses(string s) {
//先按照自己的思路来,和之前的回溯问题很相似
backTracking(s, 0, 0);
return result;
}
};
按照代码随想录的逻辑来,其实就是直接在原始字符串上加点,这个方法更犀利了。
这里也学到了新的在字符串中插入和删除点的方式。
class Solution {
public:
vector<string> result;
bool isVaild(string s, int start, int end) {
//首位为0的情况
if (start > end) return false;
if (s[start] == '0' && start != end) return false;
int len = end - start + 1;
if (len > 3) return false;
if (len == 3 && ((s[start] - '0') * 100 + (s[start + 1] - '0') * 10 + s[start + 2] - '0') > 255) return false;
return true;
}
void backTracking(string s, int startIndex, int pointNum) {
//终止条件
if (pointNum == 3) {
if (isVaild(s, startIndex, s.size() - 1)) {
result.push_back(s);
}
return;
}
//顺序条件
for(int i = startIndex; i < startIndex + 3 && i < s.size(); i++) {
if (isVaild(s,startIndex,i)) {
pointNum++;
s.insert(s.begin() + i + 1, '.');
backTracking(s, i + 2, pointNum);
s.erase(s.begin() + i + 1);
pointNum--;
}
}
return;
}
vector<string> restoreIpAddresses(string s) {
//我的思路和代码随想录的思路一致,但是代码随想录的思路和代码考虑更完整,我的瑕疵就很多
//首先排除不合理长度的字符串进行初步剪枝操作
if (s.size() < 4 || s.size() > 12) return result;
backTracking(s,0,0);
return result;
}
};
9.12日任务:子集
比较简单,前面的题目刷过之后,这个就太简单了
class Solution {
public:
vector<vector<int>> result;
vector<int> tempVec;
void backTracking(vector<int>& nums, int index) {
result.push_back(tempVec);
if (index > nums.size() - 1) return;
//顺序执行
for (int i = index; i < nums.size(); i++) {
tempVec.push_back(nums[i]);
backTracking(nums, i + 1);
tempVec.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
//和之前的题目没有什么区别,就是组合问题,只要能把树状图画出来就好了
if (nums.size() == 0) return result;
//先塞入一个空集
backTracking(nums,0);
return result;
}
};
9.12日任务:子集II(当日第二题)
逻辑比较简单,10分钟分析迅速编写完成
class Solution {
public:
vector<vector<int>> result;
vector<int> tempvec;
void backTracking(vector<int>& nums, int index) {
//终止条件
result.push_back(tempvec);
if (index > nums.size() - 1) return;
//顺序执行
for (int i = index; i < nums.size(); i++) {
if ( i > index && nums[i] == nums[i - 1]) continue;
tempvec.push_back(nums[i]);
backTracking(nums, i + 1);
tempvec.pop_back();
}
return;
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
//和上一个子集问题的区别是,这里给的整数数组中包含了重复元素,又是个去重问题
//也比较简单,只要画出树状图来就迎刃而解
sort(nums.begin(), nums.end());
backTracking(nums, 0);
return result;
}
};
9.12日任务:递增子序列(当日第三题)
代码随想录给的示例中分别用unordered_set和数组作为标记数组(查找表)
思路比较简单,通过组合画出树状图来就ok.
根据代码随想录的思路独立编写,注意nums[i]数值在-100-100,这是201个数,不是200个数,这里卡了我一会。
class Solution {
public:
vector<vector<int>> result;
vector<int> tempVec;
void backTracking(vector<int>& nums, int Index) {
//终止条件
if (tempVec.size() >= 2) result.push_back(tempVec);
if (Index > nums.size() - 1) return;
//顺序执行
bool flag[201] = {false};
for (int i = Index; i < nums.size(); i++) {
if (i > Index && flag[nums[i] + 100] == true) continue;
if (tempVec.empty() || nums[i] >= tempVec[tempVec.size() - 1]) {
flag[nums[i] + 100] = true;
tempVec.push_back(nums[i]);
backTracking(nums, i + 1);
tempVec.pop_back();
}
}
return;
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
//和上一个子集II问题的区别就是不能提前排序
//需要设立一个标记数组,可以看到前面出现了哪些数字,以免重复
//标记数组应该是一个可查找的数据类型
//如果nums[i]范围为-100-100,可以设立一个标记数组flag[200]
backTracking(nums, 0);
return result;
}
};
尝试用unordered_set去解决
只要知道unordered_set是用来存储不重复的元素,内部元素没有特定的顺序。
主要方法有:insert(value),erase(value),if(set.find(value) != set.end())(说明找到了该元素)
class Solution {
public:
vector<vector<int>> result;
vector<int> tempVec;
void backTracking(vector<int>& nums, int Index) {
//终止条件
if (tempVec.size() >= 2) result.push_back(tempVec);
if (Index > nums.size() - 1) return;
//顺序执行
unordered_set<int> set;
for (int i = Index; i < nums.size(); i++) {
if (i > Index && set.find(nums[i]) != set.end()) continue;
if (tempVec.empty() || nums[i] >= tempVec[tempVec.size() - 1]) {
set.insert(nums[i]);
tempVec.push_back(nums[i]);
backTracking(nums, i + 1);
tempVec.pop_back();
}
}
return;
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
//和上一个子集II问题的区别就是不能提前排序
//需要设立一个标记数组,可以看到前面出现了哪些数字,以免重复
//标记数组应该是一个可查找的数据类型
//如果nums[i]范围为-100-100,可以设立一个标记数组flag[200]
backTracking(nums, 0);
return result;
}
};
9.13日任务:全排列
按自己的思路编写代码,只要画出树状图把问题分析清楚,编写出代码没有什么大问题
class Solution {
public:
vector<vector<int>> result;
vector<int> tempVec;
int flag[21] = {0};
void backTracking(const vector<int>& nums, int depth) {
//终止条件
if (depth == nums.size()) {
result.push_back(tempVec);
return;
}
//顺序执行
//不含重复数字
for (int i = 0 ; i < nums.size(); i++) {
//如果这个数值出现过了,不用再进行处理
if (flag[nums[i]+10] == 1) continue;
else {
depth++;
flag[nums[i]+10] = 1;
tempVec.push_back(nums[i]);
backTracking(nums, depth);
tempVec.pop_back();
flag[nums[i]+10] = 0;
depth--;
}
}
return;
}
vector<vector<int>> permute(vector<int>& nums) {
//和前面的题目没有什么区别,只要能画出树状图把问题分析清楚就好
//这道题目可以用数组来记录,也可以用unordered_set来记录
//先按自己的方法来,用数组
//题目要求不存在空数组
backTracking(nums,0);
return result;
}
};
上面代码我的哈希记录的是数值,其实更为简单的是记录索引,下面代码为记录索引的代码,节约了空间。
class Solution {
public:
vector<vector<int>> result;
vector<int> tempVec;
bool flag[6] = {false};
void backTracking(const vector<int>& nums, int depth) {
//终止条件
if (depth == nums.size()) {
result.push_back(tempVec);
return;
}
//顺序执行
//不含重复数字
for (int i = 0 ; i < nums.size(); i++) {
//如果这个数值出现过了,不用再进行处理
if (flag[i] == true) continue;
else {
depth++;
flag[i] = true;
tempVec.push_back(nums[i]);
backTracking(nums, depth);
tempVec.pop_back();
flag[i] = false;
depth--;
}
}
return;
}
vector<vector<int>> permute(vector<int>& nums) {
//和前面的题目没有什么区别,只要能画出树状图把问题分析清楚就好
//这道题目可以用数组来记录,也可以用unordered_set来记录
//先按自己的方法来,用数组
//题目要求不存在空数组
backTracking(nums,0);
return result;
}
};
9.13日任务:全排列II(当日第二题)
题目比较简单,没什么难度,只要画出树状图,理清楚递归回溯的逻辑,就很简单就解决了。
纵向用索引哈希,横向用数值数组记录哈希。
class Solution {
public:
//横向用数组做哈希,纵向用下标做哈希
vector<vector<int>> result;
vector<int> tempVec;
//纵向哈希
int flag2[8] = {0};
void backTracking(const vector<int>& nums, int num) {
//终止条件
if (num == nums.size()) {
result.push_back(tempVec);
return;
}
//横向哈希
int flag1[21] = {0};
for (int i = 0; i < nums.size(); i++) {
if (flag1[nums[i] + 10] == 1 || flag2[i] == 1) continue;
else {
num++;
flag1[nums[i] + 10] = 1;
flag2[i] = 1;
tempVec.push_back(nums[i]);
backTracking(nums, num);
tempVec.pop_back();
flag2[i] = 0;
num--;
}
}
return;
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
//又是一个去重问题,但是依然,只要画好树状图,理清楚回溯逻辑之后就没有什么问题
backTracking(nums, 0);
return result;
}
};
也可以纵向用索引哈希,横向情况下先对整个数值进行排序,然后借助前后两个值相同进行去重,节省空间,这道题整体比较简单。
class Solution {
public:
//横向用数组做哈希,纵向用下标做哈希
vector<vector<int>> result;
vector<int> tempVec;
//纵向哈希
int flag[8] = {0};
void backTracking(const vector<int>& nums, int num) {
//终止条件
if (num == nums.size()) {
result.push_back(tempVec);
return;
}
//横向哈希
for (int i = 0; i < nums.size(); i++) {
if (flag[i] == 1 || (i > 0 && flag[i - 1] == 0 && nums[i] == nums[i - 1])) continue;
else {
num++;
flag[i] = 1;
tempVec.push_back(nums[i]);
backTracking(nums, num);
tempVec.pop_back();
flag[i] = 0;
num--;
}
}
return;
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
//又是一个去重问题,但是依然,只要画好树状图,理清楚回溯逻辑之后就没有什么问题
sort(nums.begin(),nums.end());
backTracking(nums, 0);
return result;
}
};
9.14日任务:重新安排行程(因为时间问题,9.14,9.15花了两天也没搞定,搞定已经是9.16上午了)
感悟:STL库真的很好用,题目刷完之后要对STL有一个较为系统的学习。
这道题先是自己思考磨了很久,最后还是参考代码随想录的思路自己编写代码。
这道题还是比较难的,独立编写的过程中非常考验自己对于细节的把握,值得二刷三刷。
下面是按照自己的想法编写的代码,但是也只是解决了题目中的两个示例,并没有完全解决问题。
这道题目的难点有两处:
1.路径合理即所有车票能顺利联通起来
2.合理路径中选择出距离较短的路径
在我的代码里,我在选择下一个路径时,会先比较不同目的地的距离排序,选择近的目的地作为下一目的地,但
是这种情况可能会导致路径不合理,当出现路径不合理时,就需要进行回溯,在这个时候,我不知道应该如何继续撰写代码,我选择向代码随想录参考代码求救.
更新思路二次尝试:先找出所有合理路径,再按照最短距离。这个想法最终卡住的难点是,如何根据所有的路径判断出最小的那条路径,如果合理的路径非常多的话,这个问题计算复杂度会非常高,这部分代码会非常难以编写。
下面这段代码可以解决问题,但是代码复杂度搞了写,最终超时了(在其中一个车票数较大的案例中),主要原因是每一次进入后都要排序,如果提前排序好所有的就没有这么多问题了。
我已经按照自己的思路尽力了,不过问题的复杂度太高了,还是向代码随想录求救吧。
class Solution {
public:
vector<string> result;
unordered_set<int> memory;
bool backflag = false;
static bool compare(const pair<int, string>& a, const pair<int, string>& b) {
for (int i = 0; i < 3; i++) {
if (a.second[i] < b.second[i]) return true;
else if (b.second[i] < a.second[i]) return false;
}
//没有考虑等于的情况,不用考虑,因为这个特定问题不会出现两张相同的机票,不用考虑等于是符合实际情况的。
return false;
}
void mySolution(vector<vector<string>>& tickets, string Loc, int depth) {
//终止条件
if (depth == tickets.size()) return;
//顺序执行
//寻找JFK,并且是所有JFK,因为要根据下一个地点的字母排序来确定到底去哪一个地点
vector<pair<int, string>> temp;
for (int i = 0; i < tickets.size(); i++) {
//需要对机票选项进行排序,从而筛选结合合理性筛选最优路径
//这个时候定义的不应该是是一个vector<int>,而应该是一个map,映射类型,key值为车票对应的下标索引,键值为下一地点,然后通过键值进行排序
if (memory.find(i) == memory.end() && tickets[i][0] == Loc) temp.push_back(pair(i,tickets[i][1]));
}
if (temp.empty()) {
backflag = true;
return;
}else backflag = false;
//路径不合理
sort(temp.begin(),temp.end(),compare);
for (int i = 0;i < temp.size(); i++) {
memory.insert(temp[i].first);
result.push_back(tickets[temp[i].first][1]);
mySolution(tickets, tickets[temp[i].first][1], ++depth);
if (backflag) {
memory.erase(temp[i].first);
result.pop_back();
depth--;
}
else break;
}
return;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
result.push_back("JFK");
mySolution(tickets, "JFK", 0);
return result;
}
};
代码随想录的代码思路,自己独立编写。
自己独立编写非常重要,独立编写的每一步,保证自己思考的独立性,可以保证代码的每一个环节都是清晰的。
经过独立编写代码,我发现自己前面尝试的思路是没有问题的,只是代码编写过程中,并没有map容器这么简便好用。
class Solution {
public:
vector<string> result;
unordered_map<string, map<string, int>> memoryMap;
bool mySolution(vector<vector<string>> tickets, string startLoc, int ticketNum) {
//终止条件,票数用光则为终止条件
if (ticketNum == tickets.size()) return true;
//顺序执行
for (pair<const string, int>& cur:memoryMap[startLoc]) {//这里必须是引用类型,因为要修改cur.second的值
if (cur.second > 0) {//说明这趟航班可以飞
cur.second--;
result.push_back(cur.first);
if (mySolution(tickets, cur.first, ++ticketNum)) return true;//++得放在ticketNum前面
//这里需要考虑一种情况,即选择这个目的地进行飞行,尽管按照字典排序它是最短距离,但是这个也有可能是一个不合理的行程,如果行程不合理就需要回溯,选择下一个可行的目的地
//需要添加一个标志位,什么标志位呢?得告诉我行程合不合理,可以把这个函数返回值设置位bool,如果行程合理,那么当所有机票使用完之后,就将true逐级回溯上去,如果行程不合理,就在不合理的地方将false传到上一级,从而调整行程,使得行程合理起来.或者也可以设置全局标志位来定义当前行程是否合理
//如果行程合理,直接跳出循环程序
//如果行程不合理,回溯并继续循环选择下一行程
cur.second++;
result.pop_back();
ticketNum--;
}
}
//因为,我们还要返回false来传递行程不合理,行程不合理的情况就是:ticketNum还没有到指定数量,但是startLoc对应的目的机场的机票被用完了。所以当mySolution(tickets, cur.first, ++ticketNum)为true时立刻传递到上一级。当for循环执行结束到当前这个位置的话,其实已经说明行程不合理了,此时应该return false;
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
//按照代码随想录的思路来
//1.对于其中的目的机场字典排序问题,我之前的方法是自己写compare函数然后利用sort进行排序,最后我的方法有一道测试用例超时,其他全部通过,然而map数据结构本身就有这个功能,所以使用map数据结构就非常高效,非常理所当然.STL库真的很重要,题目刷完之后要认真学习一波。
//先初始化我的memoryMap,记录了每个始发点,有哪些个目的点,以及同一个目的点重复了多少次
for (const vector<string>& ticket:tickets) {
memoryMap[ticket[0]][ticket[1]]++;
}
result.push_back("JFK");
mySolution(tickets, "JFK", 0);
return result;
}
};
9.15日任务:一个上午的时间结束代码随想录回溯篇,把map.set两个数据结构搞清楚(因为时间原因,计划失败,么得完成)
9.16日任务:结束代码随想录回溯篇
N皇后
题目比较复杂,二刷只要能做出来,并且有改进思路,能分析复杂度就ok.
自己独立分析编写完成,不过感觉还不够渐变,因为我每一行都需要重新计算哪一列不能放,我也没想到什么新方法。
letcode这道题目是困难,独立解决很开心。
我的方法计算复杂度太高了,代码随想录的方法计算复杂度更低
class Solution {
public:
//全局定义result;
vector<vector<string>> result;
//全局定义单个解决方案数组
vector<string> tempVec;
void backTracking(vector<int>& rowMemory, int depth) {
//输入参数,1个是棋盘checkerboard,给出了棋盘中可以摆放皇后的位置,以及需要摆放皇后的数量
//终止条件,棋子放置完毕
if (depth == rowMemory.size()) {
result.push_back(tempVec);
return;
}
//顺序执行
//首先更新哪一列可以放置
vector<int> colMemory(rowMemory.size(), 0);
if (depth > 0) {
for (int i = 0; i < depth; i++) {
colMemory[rowMemory[i]] = 1;
if (rowMemory[i] - (depth - i) >= 0) colMemory[rowMemory[i] - (depth - i)] = 1;
if (rowMemory[i] + (depth - i) < colMemory.size()) colMemory[rowMemory[i] + (depth - i)] = 1;
}
}
for (int i = 0; i < colMemory.size(); i++) {
//如果这一列可以放置
if (colMemory[i] == 0) {
rowMemory[depth] = i;
string temp(colMemory.size(), '.');
temp[i] = 'Q';
tempVec.push_back(temp);
depth++;
backTracking(rowMemory, depth);
depth--;
tempVec.pop_back();
rowMemory[depth] = 0;
}
}
return;
}
vector<vector<string>> solveNQueens(int n) {
//首先自己尝试分析问题,思路有了,就是代码编写的问题。
//就从回溯的角度去考虑,一般什么问题用回溯呢?就是那种需要试一下,试了之后不行再回去重试,这种问题就是比较典型的回溯问题
//我先试着将这个问题转化成树状图的形式
//基本思路就是:先把第一个皇后放在第一个格子里,然后将这个格子和其他点位全部置1,接着看为0的格子数量和剩余的皇后棋子数量是否匹配,不匹配就不合理,匹配的话就在为0的格子里,放入一个皇后,再将皇后的攻击点位全部置1,继续循环下去,遇到不匹配不合理的情况就进行回溯,再试,这样子不断重试,问题就是会被解决。
//接下来就是代码编写,不用STL的话,编写起来看起来会复杂很多,不要畏难,先按照自己的思路走
//思路更新,可以一行一行选,第一行选了某一列,下一行就不能选这一列,第三行就不能选前两行所在的列,依此类推
//题目中n的区间为1-9
//记录哪一行被哪一个位置被占领
vector<int> rowMemory(n, 0);
backTracking(rowMemory, 0);
return result;
}
};
代码随想录的方法比我更高效:因为我在遍历每一行时都需要循环遍历前面的行确定出当前行哪些列不能放置,这一段比较耗时,代码随想录中则是先放置后验证的方法,会更高效一点。
这个问题比较耗时,了解思路就可以了,不用再重新按照代码随想录的思路编写代码了,回溯掌握了就行,其他的就是思路的问题。
解数独
题目比较复杂,二刷只要能做出来,并且有改进思路,能分析复杂度就ok.
我觉得我根据思维去编写代码的能力目前还算可以,限制代码性能的原因还是在于思路,想到的思路越简单,程序也相应地会比较高效。
我的思路:从左至右,从上至下,一个点一个点去填,每填一个点都要判断有哪几种填法,按自己的思路先尝试一下.
按照自己的思路暴力求解,leetcode显示耗时非常长。
我觉得我根据思维去编写代码的能力目前还算可以,限制代码性能的原因还是在于思路,想到的思路越简单,程序也相应地会比较高效。
自己独立编写一道困难问题,感觉真不错
class Solution {
public:
bool mySolution(vector<vector<char>>& board, int count, vector<int> spaceRecord) {
//终止条件,整个board满了的话就终止了
if (count == spaceRecord.size()) return true;
int rowIndex = spaceRecord[count]/9;
int colIndex = spaceRecord[count]%9;
//顺序执行
//顺序执行默认讨论的是需要填充的空格,我如果只是用count的话,那么就会定位到一些不需要填充的点,所以我的count必须是空余位置对应的cout,而不是每个位置对应的count
//设立一个标志位,用来标志这个点有哪几个数字可以选择
vector<int> flag(9,0);
//先扫描所在行
for (int i = 0; i < 9; i++) {
if (i != colIndex && board[rowIndex][i] != '.') flag[board[rowIndex][i] - '1'] = 1;
}
//扫描所在列
for (int i = 0; i < 9; i++) {
if (i != rowIndex && board[i][colIndex] != '.') flag[board[i][colIndex] - '1'] = 1;
}
//扫描所在3*3的格子
for (int i = 3 * (rowIndex / 3); i < 3 * (rowIndex / 3) + 3; i++) {
for (int j = 3*9 * (colIndex / 3); j < 3 * (colIndex / 3) + 3; j++) {
if (i != rowIndex && j != colIndex && board[i][j] != '.') flag[board[i][j] - '1'] = 1;
}
}
for (int i = 0; i < 9; i++) {
if (flag[i] == 0) {
board[rowIndex][colIndex] = i + '1';
count++;
if (mySolution(board,count,spaceRecord)) return true;
count--;
board[rowIndex][colIndex] = '.';
}
}
//这个循环运行到这里其实已经说明没有探索下去,或者说夭折了
return false;
}
void solveSudoku(vector<vector<char>>& board) {
//就按照我的笨思路来,自左至右,自上至下,一个一个填
//首先统计哪些点是空格点,以及定位空格点的位置
vector<int> spaceRecord;
for(int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') spaceRecord.push_back(i*9+j);
}
}
mySolution(board, 0, spaceRecord);
}
};
代码随想录的思路耗时更短,什么原因呢?
1.我的思路在每填写一个数字的时候都要遍历整个行,列以及3*3区域的每一个数值,然而代码随想录的方法是在填写空格之后验证是否合理,只需要检测是否重复就ok了,不需要扫描行列的每一个元素
2.我的思路在前期还要遍历一遍9*9的格子,增加了耗时。
不用再重新按照代码随想录的思路编写代码了,重要的是思路,我根据思路编写代码的能力目前还算自信。