算法题之回溯算法

1、组合总和

题目

在这里插入图片描述

分析

回溯

这题需要使用回溯的方法来解题,对这个所谓回溯算法,我个人的理解是依次尝试,要配合递归,在递归前把数据放入vector,递归结束后再将加入vector的数据取出,然后进行下一次循环。

剪枝

这里还用到了剪枝,即在递归前先判断下当前的数是否还符合条件,如不符合就直接跳过,这样可以省去一些步骤。

去重

还有一个问题就要去重,比如如果candidates中包含2、3,就会同时出现{2,3}和{3,2},这就造成了重复,而我们现在以不减的顺序进行探测,这就避免了重复发生。

代码如下:

    void dfs(const vector<int>& candidates,vector<vector<int>> &ret,vector<int> &a,
             int pos,const int &n,int cur,const int &target){
        if(pos>=n||cur>target) return;
        if(target==cur){
            ret.push_back(a);
            return;
        }

        for(int i=pos;i<n;++i){
            a.push_back(candidates[i]);
            dfs(candidates,ret,a,i,n,cur+candidates[i],target);
            a.pop_back();
        }

        return;
    }

复杂度

时间复杂度:O(n^2)
空间复杂度:O(n)

2、全排列&第K个排列

题目

在这里插入图片描述在这里插入图片描述

分析

全排列
这题可以说是回溯算法的教材。
主函数中,将每次遍历的头部数先确定好,book记录用过的数

        for(int i=0;i<n;++i){
            vector<int> ret;
            book[i]=true;
            ret.push_back(nums[i]);
            dfs(nums,book,ret);
            book[i]=false;
        }

深度遍历

    void dfs(vector<int>& nums,int *book,vector<int> &ret){
        if(ret.size()==nums.size()){
            out.push_back(ret);
            return;
        } 
        for(int i=0;i<nums.size();++i){
            if(book[i]==true) continue;
            ret.push_back(nums[i]);
            book[i]=true;
            dfs(nums,book,ret);
            ret.pop_back();
            book[i]=false;
        }
        return;
    }

注:其实上面主函数中的遍也可以写进dfs中。

第K个排列
参考了link
这题的解是基于上一题,是进阶的一道题,这题的难点在于要明白剪枝该怎样处理,如下所示:
在这里插入图片描述在这里插入图片描述
因为题目中说“给定 n 的范围是 [1, 9]”,故这里的做法是在一开始就将 0 到 9的阶乘计算好,放在一个数组里,后面直接取用即可,如下:

        int* fac=new int[n+1];
        memset(fac,0,sizeof(fac));

        fac[0]=1;
        for(int i=1;i<n+1;++i){
            fac[i]=fac[i-1]*i;
        }

这里注意 fac[0]=1 这步操作,它表示了没有数可选的时候,即表示到达叶子结点了,排列数只剩下 1 个,例如n=1,k=1时。
随后进行DFS遍历,如下:

    void dfs(int cur,int n,int k,int *fac,vector<int>& used,vector<int>&count){
        if(cur==n) return;

        //还未确定的数字的全排列的个数,第 1 次进入的时候是 n - 1
        int cnt=fac[n-1-cur];
        for(int i=1;i<n+1;++i){
            if(used[i]) continue;

            if(cnt<k){
                k-=cnt;
                continue;
            }
            count.push_back(i);
            used[i]=true;
            dfs(cur+1,n,k,fac,used,count);
        }
    }

这里cur从0开始,注意这里之所以能够以 if(cur==n) return; 作为结束判断的原因在于剪枝过程已经将前k-1个排列都给剪掉了。
cnt是当前节点的一个子分支的总叶子数,因为刚进入时是头结点,所以这里cnt一开始取 fac[n-1] 。

3、子集

题目

在这里插入图片描述

分析

这题算是上面全排列的一种变体,不需要使用book数组,而是从左往右进行遍历,如下:

    vector<vector<int>> subsets(vector<int>& nums) {
        int n=nums.size();
        vector<vector<int>> ret;
        vector<int> a;
        dfs(nums,ret,a,0,n);
        return ret;
    }

    void dfs(vector<int>& nums,vector<vector<int>> &ret,vector<int> &a,int pos,int n){      
        ret.push_back(a);
        if(pos>=n) return;

        for(int i=pos;i<n;++i){
            a.push_back(nums[i]);
            dfs(nums,ret,a,i+1,n);
            a.pop_back();
        }
        return;
    }

这里注意下要在遍历的一开始就将a装进ret中。

4、分割回文串

题目

在这里插入图片描述

分析

参考:link

回归树如下所示:
在这里插入图片描述
步骤:
1)将字符串截取一段a
2)判断a是否是回文串,如果是就将剩下的部分进行下一次递归
如下:

    void dfs(vector<vector<string>> &ret,vector<string>& a,const string &s,int pos,const int &n,const vector<vector<int>> &dp){
        if(pos>=n){
            ret.push_back(a);
            return;
        }

        for(int i=pos;i<n;++i){
            if(isPalindrome(b)){
                string b=s.substr(pos,i-pos+1);
                a.push_back(b);
                dfs(ret,a,s,i+1,n,dp);
                a.pop_back();
            }
        }

        return;
    }

这里的结束条件是已截取的前缀部分的长度等于原s的长度,所以要用pos和n来分别记录当前的位置和s的总长度。

优化:
这里有种空间换时间的优化算法,上述解法中的 isPalindrome 是从两边往中间遍历检查是否为回文串。
基于LK第五题,我们可以先用一个dp数组把所有的回文串给记录下来,如下:

    vector<vector<string>> partition(string s) {
        int n=s.size();
        vector<vector<string>> ret;
        vector<string> a;

        vector<vector<int>> dp(n,vector<int>(n,0));
        for(int i=0;i<n;++i) dp[i][i]=1;
        for(int i=0;i<n;++i){
            for(int j=0;j<i;++j){
                if(s[i]==s[j]){
                    if(i==j+1){
                        dp[i][j]=1;
                        continue;
                    }
                    if(dp[i+1][j-1]==1) dp[i][j]=1;
                    else dp[i][j]=-1;
                }
            }
        }

        dfs(ret,a,s,0,n,dp);
        return ret;
    }

这是isPalindrome 可以替换为

if(dp[pos][i]==1)

5、复原IP地址

题目

在这里插入图片描述

分析

代码如下:

    void dfs(string s,vector<string> &ret,string ssub,int pos,int count){
        if(count==4) {
            if(pos==s.size()){
                ret.push_back(ssub);
            }
            return;
        }

        string ss;
        ++count;
        for(int i=1;i<4;++i){
            if(pos+i>s.size()) break;
            string b=ssub;
            ss=s.substr(pos,i);
            if(i>1&&ss[0]=='0') break;
            int ssnum=atoi(ss.c_str());
            if(ssnum>255) break;
            b+=ss;
            if(count<4) b+='.';
            dfs(s,ret,b,pos+i,count);
        }
        return;
    } 

关于这题,因为IP地址要分成四层,然后每层的子串最长为3,所以这里维护了一个count用来记录遍历的层数,当 count=4 时遍历结束,检查当前位置是否到了s的末尾以判断 ssub 是否有效。
在遍历过程中要注意三种不成立的情况,即:

if(pos+i>s.size()) break;
if(i>1&&ss[0]=='0') break;
if(ssnum>255) break;

第一个和第三个好理解,这里来说下第二种,当 s 为 “010010” 时,如果 ss=“010” 也是无效的,但 ‘0’ 是可以的,所以这里还要判断 i>1。
同时注意,第一个中必须是大于,如,考虑pos现在指向最后一个位置,然后 i 为1时刚好得出一个可行解。

6、括号生成

题目

在这里插入图片描述

分析

这里我想的是使用动态规划来做,但是对于转移方程没什么思路。
看了解析,发现使用DFS和BFS更好。但这里需要对题目做个简单的数学建模,就是维护两个数,left和right分别表示左括号和右括号数目,如下图所示:
在这里插入图片描述

    void dfs(vector<string> &out,string s,int left,int right){
        //剪枝
        if(left>right) return;
        if(right==0){
            out.push_back(s);
            return;
        }
        else if(left==0){
            string s1=s;
            while(right>0){
                s1+=')';
                --right;
            }
            out.push_back(s1);
            return;
        }
        else{
            string s1,s2;
            s1=s+')';
            dfs(out,s1,left,right-1);
            s2=s+'(';
            dfs(out,s2,left-1,right);
        }
    }

而使用DP法的话也可以,转移方程如下:
在这里插入图片描述

复杂度

时间复杂度:O(n^2)
空间复杂度:O(n)

7、单词拆分II

题目

在这里插入图片描述

分析

参考:link
这里前面的步骤与单词拆分相同,在将dp数组处理完成后就开始进行DFS了,具体分析要结合图和实例才好懂,这里直接上代码了,

    vector<string> wordBreak(string s, vector<string>& wordDict) {
        int n=s.size();
        unordered_set<string> books;
        for(auto a:wordDict) books.insert(a);

        vector<int> dp(n+1,0);
        dp[0]=1;
        for(int i=1;i<=n;++i){
            for(int j=i-1;j>=0;--j){
                string b=s.substr(j,i-j);
                if(dp[j]&&books.count(b)){
                    dp[i]=1;
                    break;
                }
            }
        }

        if(dp[n]==0) return {};
        vector<string> ret;
        string a;
        dfs(s,dp,books,ret,n,a);
        return ret;
    }

    void dfs(const string& s,const vector<int> &dp,const unordered_set<string> &books,vector<string> &ret,int pos,string &a){
        if(pos<=0){
            a.pop_back();
            ret.push_back(a);
            return;
        }

        for(int i=0;i<pos;++i){
            if(dp[i]&&books.count(s.substr(i,pos-i))){
                string b=s.substr(i,pos-i)+' '+a;
                dfs(s,dp,books,ret,i,b);
            }
        }

        return;
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值