[日记]LeetCode算法·九——回溯①

文章介绍了回溯算法的基本思想,通过穷举并进行剪枝优化来解决排列、组合等问题。具体应用包括LeetCode上的组合问题,如组合总和III,电话号码的字母组合,以及考虑重复元素的组合总和II,展示了如何通过排序和记录选择状态进行去重。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 回溯问题

回溯算法的本质就是穷举,回溯是为了更好地进行穷举,并且在必要的时候进行剪枝操作。

回溯算法适用于排列、组合、棋盘、分割、子集等排列组合问题。

并且回溯算法都可以看成一个树遍历问题,但由于事实上往往没有树这一数据结构进行辅助,需要注意回溯算法的终止条件。

理论上,我们是用for循环对同层节点进行push进路径,采用递归进行DFS。

其标准模板为

void backTrack(参数)
{
    if(终止条件 且 不满足付标准)
        return;
    if(终止条件 且 满足标准)
    {
        本次路径 加入 结果集合;
        return;
    }
    for(循环)
    {
        本层元素加入路径;
        backtrack(下一层元素);
        元素移出路径(回溯);
    }
}

2 组合

LeetCode:组合

对于n取k的组合问题,首先关注终止条件,即组合元素达到了k,即可加入结果集,之后回溯并取下一个元素即可。

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backTrack(int n,int k,int startIndex)
    {
        if(path.size()==k)
        {
            result.push_back(path);
            return;
        }
        for(int i=startIndex;i<=n;i++)
        {
            path.push_back(i);
            backTrack(n,k,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backTrack(n,k,1);
        return result;
    }
};

另外,我们还可以注意到,是存在剪枝的可能性的。如果取了元素i组成了路径path后,如果剩余的元素全部加上,长度依然达不到k,那么可以提前进行剪枝处理。

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backTrack(int n,int k,int startIndex)
    {
        if(path.size()==k)
        {
            result.push_back(path);
            return;
        }
        //此处i<=n-(k-path.size())+1进行了剪枝
        for(int i=startIndex;i<=n-(k-path.size())+1;i++)
        {
            path.push_back(i);
            backTrack(n,k,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backTrack(n,k,1);
        return result;
    }
};

3 组合总和III

LeetCode:组合总和III

相比上一题,只多出了一个和为target的要求,将这一要求转化为剩下数所需要的和进行递归即可。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backTrack(int n, int k, int startIndex)
    {
        if(path.size()==k)
        {
            if(n==0)
            {
                result.push_back(path);
            }
            return;
        }
        for(int i=startIndex;i<=9-(k-path.size())+1 && n>0;++i)
        {
            path.push_back(i);
            backTrack(n-i,k,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTrack(n,k,1);
        return result;
    }
};

4 电话号码的字母组合

LeetCode:电话号码的字母组合

本质上依然没有过多的区别,唯一需要注意的是此时需要取出新的集合进行新的元素选取。

class Solution {
public:
    vector<string> result;
    string path="";
    const string map[8]={
        "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
    };

    string getMap(char number)
    {
        int index=number-'2';
        return map[index];
    }

    void backTrack(string& digits,int index)
    {
        int length=digits.size();
        if(path.size()==length)
        {
            result.push_back(path);
            return;
        }
        string letters=getMap(digits[index]);
        for(int i=0;i<letters.size() && index<length;i++)
        {
            path+=letters[i];
            backTrack(digits,index+1);
            //回溯
            path.pop_back();
        }
    }

    vector<string> letterCombinations(string digits) {
        if(digits.size()==0)return result;
        backTrack(digits,0);
        return result;
    }
};

5 组合总和

LeetCode:组合总和

这道题最值得注意的是,阐明了一个重要的方法:排序+剪枝

在数组有序之后,可以有效地判断之后的元素是否超出标准,从而轻松地剪枝减少重复无用的计算量,毕竟事实上快排的复杂度并不高。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;

    void backTrack(vector<int>& candidates,int startIndex,int target)
    {
        if(target==0)
        {
            result.push_back(path);
        }
        for(int i=startIndex;i<candidates.size() && candidates[i]<=target;i++)
        {
            path.push_back(candidates[i]);
            backTrack(candidates,i,target-candidates[i]);
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        //此处剪枝需要排序支持
        sort(candidates.begin(),candidates.end());
        backTrack(candidates,0,target);
        return result;
    }
};

6 组合总和II

LeetCode:组合总和II

这道题首次出现有重复元素集合中的去重要求,因此我们必须要在回溯的过程中进行去重。

从思路上讲,我们是允许使用了前面元素的情况下,重复使用后面相同元素的。
但不允许不使用了前面元素的情况下,重复使用后面相同元素
这意味着我们允许某一路径在不同层次下使用相同值的元素,但不允许在同一层使用相同值的元素

以排序后的集合进行讨论,如果[1A,1B,2,3,5,…],如果1B与之后的元素可以满足条件,意味着在回溯过程中1A配合类似的元素组成了结果,这时1B就没有独立价值,理应被去重。

在排序之后,去重思路大致有三种:

1 回溯后跳过所有一样的元素,直到出现不同元素为止。
2 利用记录列表used,记录目前路径下哪些元素已经被使用,在used[i-1]=false时,意味着这一层次i-1元素已经完成了回溯,如果i元素与i-1一致,就应该被去重。
3 顺承2的思路,利用startIndex,重复元素只有在startIndex内允许与i-1相同(这意味着这是在i-1已被选择的情况下进行的,即不同层次,否则会被上一层循环直接跳过),其余相同元素只有在首次出现才能被允许,其余都应该被去重。

思路1:回溯后跳过

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    
    void backTrack(vector<int>& candidates,int startIndex,int target)
    {
        if(target<0)
            return;
        if(target==0)
        {
            result.push_back(path);
            return;
        }
        for(int i=startIndex;i<candidates.size() && target>0;i++)
        {
            path.push_back(candidates[i]);
            backTrack(candidates,i+1,target-candidates[i]);
            path.pop_back();
            //去重应该在回溯之后进行,因为排序后前面已完成的结果,必然包括了后面重复元素的所有可能
            //跳过所有同一层次下的重复,不同层次间的重复意味着不同的元素是可以允许的
            while(i<(candidates.size()-1)&&candidates[i]==candidates[i+1])
            {
                ++i;
            }
        }
    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        backTrack(candidates,0,target);
        return result;
    }
};

思路2:记录选择情况

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    
    void backTrack(vector<int>& candidates,int startIndex,int target,vector<bool>& used)
    {
        if(target<0)
            return;
        if(target==0)
        {
            result.push_back(path);
            return;
        }
        for(int i=startIndex;i<candidates.size() && target>0;i++)
        {
            //used[i-1]=false,代表这一次循环中candidates[i-1]没有被使用
            //也就意味着与candidates[i]一致的candidates[i-1]已经完成了回溯
            //那么candidates[i]自然无法再candidates[i-1]不参与的情况下,实现独一无二的价值,应该被去重
            if(i>0 && used[i-1]==false &&candidates[i-1]==candidates[i])
                continue;

            path.push_back(candidates[i]);
            used[i]=true;

            backTrack(candidates,i+1,target-candidates[i],used);

            path.pop_back();
            used[i]=false;
        }
    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        vector<bool> used(candidates.size(),false);
        backTrack(candidates,0,target,used);
        return result;
    }
};

思路3:利用startIndex判断层次

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    
    void backTrack(vector<int>& candidates,int startIndex,int target)
    {
        if(target<0)
            return;
        if(target==0)
        {
            result.push_back(path);
            return;
        }
        for(int i=startIndex;i<candidates.size() && target>0;i++)
        {
            //在这一层循环中,candidates[i]再不是作为整个序列第一个出现的情况下
            //还有之前元素数值一致,意味着没有独立价值,理应被去重
            //包括[1,1,2,2,....]中的第二个2也应该被去重
            if(i>startIndex && candidates[i-1]==candidates[i])
                continue;

            path.push_back(candidates[i]);
            backTrack(candidates,i+1,target-candidates[i]);
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        backTrack(candidates,0,target);
        return result;
    }
};

7 总结

终于开始探索未知的领域——回溯算法了,感觉还是很快进入了状态,是因为和树有异曲同工之妙吗?明后天争取把回溯算法全部过了。

今天上了英语课,虽然全部是Listening和Speaking,还每周Dictation的课程对于我这种fw而言是一种很新很痛苦的折磨,但如果能借此机会提升自己也是好事。

不知道自己今晚还不能赶出GAN的博客,不行的话就摆了!

——2023.2.23

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值