leetcode 93 复原ip地址
这一题和之前的分割回文串有异曲同工之妙,都是给一组数据让你判断分割成几组小数据,代码主要分成三部分,用来判断的函数,回溯函数,主函数;最好是在原数据上面进行操作,我一开始就是新开了一个字符串做起来反而困难
首先说判断函数,这个根据题目我们可以很容易就写出代码来,需要注意的是判断是否大于255时,我们要用num来记录整个字符串对应整数的大小,因此需要用num=10*num进行进位
其次是回溯函数,回溯函数我们都知道主体是backtracking包含for,for再包含backtracking,除此之外还有一个终止递归的条件,这里我们引进一个变量来记录逗号的数量,当逗号等于三的时候我们就可以判断第四段ip地址,以此选择是否加入结果集中
这里我们讲一下抽象的概念方便理解,工作指针i可以看作我们划的那条线,i前面的可以看作这一次需要判断的数据,如果符合要求则继续递归,不符合要求则进行横向遍历,以此保证每一段ip地址都是合法的,i后面的则是我们下一次需要遍历的数据,即startindex=i+1;
感觉理清楚了思路还是比较好写的,难就难在你判断函数代码怎么写好,能不能想到先处理前三段再处理第四段,能不能想到直接在原数据上面操作,这些就是需要我们思考的地方,做题量和思考缺一不可
leetcode 90 子集2
这又是一种新的题型了,感觉做回溯最重要的是分辨出是那种题型,现在已经涉及到组合,分割,子集三种类型了,组合是在数据中取一个数,剩下的数作为下一次的遍历的数据,整个树枝当作一组放入结果;分割和组合差不多,先分割一个/多个数据出去,判断这个数据,剩下的数据作为下一次遍历的数据;而子集是将每一次的判断的数据都放入结果集中,这是和上面两种不同的地方,因此每一次回溯
backtracking都有一次pushback操作
将每一个树支当作以该元素开头的子集就好容易理解了,例如123,取1就是以1开头的子集
允许结果重复and不允许结果重复
允许重复即不用去重了,因为数据里会有重复的数,在遍历的时候会有重复结果出现,例如1444,第一个4就已经包含了后面几个4的情况,允许重复的话就不用管直接遍历整个数组/字符串即可
如果不允许结果重复,即需要比较前一个和当前数据是否相同,相同则跳过continue,相当于进行了剪枝操作(需要先进行排序操作)
leetcode 491 递增子序列
这一题和上一题子集看起来差不多,直接套模板,然后就错了😂,专题刷题就是容易死背模板,然后缺少自己的思考
这一题的要求是找到所有递增子序列,长度大于1,看起来和上一题要求差不多,但是做起来的时候有很大的差别,主要有两点
首先是要求递增,而子集那道题是不要求递增的,排序之后相邻去重即可,本题给出的数据不一定是递增的,因此无法相邻去重也就用不了nums[i]==nums[i-1]去判断了;可以排序再相邻去重吗?答案当然是不可以的,你把数据都改变了相当于把题改了答案也都不一样了
其次是去重方式,上面说到了无法使用相邻去重,那么如何进行判断呢(path路径上上一个节点的下标和当前下标不一定相邻,例如416,46下标不相邻),那就需要用到uset记录每一层中使用了的数据,就可以避免在同一层中重复(例如4717,同一层中先选择了第一个7,第二个7就可以跳过了,避免出现两次47),这样也就实现了隔项去重了
为什么uset不是记录同一树枝上面的值呢?因为数据允许数据重复,即4477是容许的
理解去重方式不同是解决本题的关键,相邻去重的方式只适合顺序数据
leetcode 47 全排序2
今天又引入了回溯的另一种新题型-排序,截至目前我们已经学过了组合,分割,子集,排序四种类型了,其中组合分割排序都是在树枝末尾取结果,只有子集问题是在每一个节点都取结果
排序问题和其他问题一样,数据有分重复和不重复(不重复简单,不需要考虑去重),结果也有分重复和不重复(一般是不重复的)
46全排序这一题就是数据不重复,只需要把数据的各种组合类型罗列出来即可
如何罗列所有的组合类型呢?我们要优先考虑在原数组上面进行操作,利用下标而不是新建数组,因此每一次层序遍历都是在原数组上面进行的,这样也带来了一个问题就是可能会出现取自身的情况(即原数组123,取111)
为了解决这种情况,我们需要一个数组记录元素的使用情况,也就是我们的vector<bool>used,如果该元素使用过则将其设为true,在for循环的时候将所有为true的元素跳过,注意进行backtrack之后必需要进行回溯操作,不然会影响后面的元素排序结果
以上就是排序问题的关键-用一个数组统计每一个元素的使用情况,这是解决排序的通用方法
回到本题,本题要求结果不能重复,而数据中可能重复,这也就造成了潜在的重复可能,也就说我们需要进行去重操作
前面我们也说过了如何进行去重,一般是分为隔项去重和相邻去重,相邻去重在本题中也可行,只不过是需要先将数据进行排序;隔项去重就是利用一个uset记录每一层中使用过的元素,如果uset中查找到有相同元素,则可以在for循环中跳过该元素啦(这一步就是完成了从112到12的变化,前提是我们已经把1取出来当做path中一员)
如果你想不到同层去重,你可以把这棵抽象树画出来,很容易就可以知道需要排除同一层中的元素啦
used[i-1]==false和used[i-1]==true
相邻去重分为同层去重,同树枝去重
在写相邻去重的时候除了nums[i]==nums[i-1]我们还经常会写上面那两个条件语句,会发现无论写哪个我们都可以通过,他们之间到底有什么关系呢
used[i-1]==false是进行同层去重,used[i-1]==true是进行同树枝去重
借用一下卡哥的两张图
false情况,同层去重
true情况,同树枝
可以看出来同层去重的效率比同树枝效率要高,因为同树枝往往是要到最后一种情况才找到结果
有些同学可能一开始看不懂为什么要这样写,借助上面两张图我们可以知道,同层去重的时候除了第一个树枝,后面的都不需要考虑因为前一个元素必然是没选取的false(除了第一个元素),第一个元素后面的都是重复的,直接排除即可
而在同树枝去重中至少有n个子树,还要向下遍历,好处是比较容易理解,符合人类的逻辑,相同即跳过
leetcode 51 N皇后
很久之前就听说过N皇后这个经典的问题,今天终于做到了,自己写了一个普通的算法好在是通过了,问题解决了,自靠自己写出来hard还是挺有成就感的
vector<vector<string>>res;
vector<string>path;
unordered_set<int>used;
vector<int>lastindex;
string makestring(int n)
{
string s;
for(int i=0;i<n;i++)
{
s+='.';
}
return s;
}
bool isvalue(vector<int>&lastindex,int i)
{
if(lastindex.size()==0)
{
return true;
}
for(int j=0;j<lastindex.size();j++)
{
if(i==lastindex[j]+path.size()-j||i==lastindex[j]-path.size()+j)
{
return false;
}
}
return true;
}
void backtracking(int n,vector<string>&path,vector<int>&lastindex)
{
if(path.size()==n)
{
res.push_back(path);
return;
}
string level=makestring(n);
for(int i=0;i<n;i++)
{
if(used.find(i)==used.end()&&isvalue(lastindex,i))level[i]='Q';
else
{
continue;
}
path.push_back(level);
used.insert(i);
lastindex.push_back(i);
backtracking(n,path,lastindex);
path.pop_back();
used.erase(i);
lastindex.pop_back();
level[i]='.';
}
}
vector<vector<string>> solveNQueens(int n) {
backtracking(n,path,lastindex);
return res;
}
我的想法是一层一层构造棋盘,用数组和集合进行去重,一道题做下来感觉难处在于如何处理斜线去重,我个人的想法是构造出斜线的函数,判断目标节点是否在斜线上从而进行去重,感觉是比较简单的一种方法
贴一个卡哥的解法,看了一下感觉去重方式差不多,他是在棋盘上面进行操作,而我是构造棋盘,他的方法需要对数组下标的使用有更高的要求,好处是效率高占用内存少,更具有模板性
class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
if (row == n) {
result.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++) {//遍历每一列
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);//下一行
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
std::vector<std::string> chessboard(n, std::string(n, '.'));
backtracking(n, 0, chessboard);
return result;
}
};
总结:棋盘的宽度(列)就是for循环的长度,递归的深度(行)就是棋盘的高度