leetcode每日一题复盘(回溯)

leetcode 93 复原ip地址 

a02929024d9f4c2185c09a23c0929f7f.png

这一题和之前的分割回文串有异曲同工之妙,都是给一组数据让你判断分割成几组小数据,代码主要分成三部分,用来判断的函数,回溯函数,主函数;最好是在原数据上面进行操作,我一开始就是新开了一个字符串做起来反而困难

首先说判断函数,这个根据题目我们可以很容易就写出代码来,需要注意的是判断是否大于255时,我们要用num来记录整个字符串对应整数的大小,因此需要用num=10*num进行进位

其次是回溯函数,回溯函数我们都知道主体是backtracking包含for,for再包含backtracking,除此之外还有一个终止递归的条件,这里我们引进一个变量来记录逗号的数量,当逗号等于三的时候我们就可以判断第四段ip地址,以此选择是否加入结果集中

这里我们讲一下抽象的概念方便理解,工作指针i可以看作我们划的那条线,i前面的可以看作这一次需要判断的数据,如果符合要求则继续递归,不符合要求则进行横向遍历,以此保证每一段ip地址都是合法的,i后面的则是我们下一次需要遍历的数据,即startindex=i+1;

感觉理清楚了思路还是比较好写的,难就难在你判断函数代码怎么写好,能不能想到先处理前三段再处理第四段,能不能想到直接在原数据上面操作,这些就是需要我们思考的地方,做题量和思考缺一不可

leetcode 90 子集2 

9996857b08f14131a69a2c8be714634e.png

这又是一种新的题型了,感觉做回溯最重要的是分辨出是那种题型,现在已经涉及到组合,分割,子集三种类型了,组合是在数据中取一个数,剩下的数作为下一次的遍历的数据,整个树枝当作一组放入结果;分割和组合差不多,先分割一个/多个数据出去,判断这个数据,剩下的数据作为下一次遍历的数据;而子集是将每一次的判断的数据都放入结果集中,这是和上面两种不同的地方,因此每一次回溯
backtracking都有一次pushback操作

将每一个树支当作以该元素开头的子集就好容易理解了,例如123,取1就是以1开头的子集

允许结果重复and不允许结果重复

允许重复即不用去重了,因为数据里会有重复的数,在遍历的时候会有重复结果出现,例如1444,第一个4就已经包含了后面几个4的情况,允许重复的话就不用管直接遍历整个数组/字符串即可

如果不允许结果重复,即需要比较前一个和当前数据是否相同,相同则跳过continue,相当于进行了剪枝操作(需要先进行排序操作)

leetcode 491 递增子序列

c09a997ed73f44729a6b7b918f484ae3.png

这一题和上一题子集看起来差不多,直接套模板,然后就错了😂,专题刷题就是容易死背模板,然后缺少自己的思考

这一题的要求是找到所有递增子序列,长度大于1,看起来和上一题要求差不多,但是做起来的时候有很大的差别,主要有两点

首先是要求递增,而子集那道题是不要求递增的,排序之后相邻去重即可,本题给出的数据不一定是递增的,因此无法相邻去重也就用不了nums[i]==nums[i-1]去判断了;可以排序再相邻去重吗?答案当然是不可以的,你把数据都改变了相当于把题改了答案也都不一样了

其次是去重方式,上面说到了无法使用相邻去重,那么如何进行判断呢(path路径上上一个节点的下标和当前下标不一定相邻,例如416,46下标不相邻),那就需要用到uset记录每一层中使用了的数据,就可以避免在同一层中重复(例如4717,同一层中先选择了第一个7,第二个7就可以跳过了,避免出现两次47),这样也就实现了隔项去重了

为什么uset不是记录同一树枝上面的值呢?因为数据允许数据重复,即4477是容许的

理解去重方式不同是解决本题的关键,相邻去重的方式只适合顺序数据

leetcode 47 全排序2

c4d4559bfb5b4d5a9bd244a5eb8e49fe.png

今天又引入了回溯的另一种新题型-排序,截至目前我们已经学过了组合,分割,子集,排序四种类型了,其中组合分割排序都是在树枝末尾取结果,只有子集问题是在每一个节点都取结果

排序问题和其他问题一样,数据有分重复和不重复(不重复简单,不需要考虑去重),结果也有分重复和不重复(一般是不重复的)

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是进行同树枝去重

借用一下卡哥的两张图

a49f994f1e4245c7af3311fde79ee269.png

 false情况,同层去重

f7c58dd75ced49e7ac134dc96a445752.png

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循环的长度,递归的深度(行)就是棋盘的高度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值