原地交换法实现全排列

在这里插入图片描述

全排序题目属于深度优先最常用的题目之一,当时遇到的时候对它的递归理解不了。
随着对递归的熟练度的增加,递归部分的理解比初次遇到快了很多。

            swap(s[step],s[i]);
            dfs(s,step+1);
            swap(s[step],s[i]);

记得当时的疑问很蠢:递归的过程中真的能保证s[step]和s[i]不会变化吗,如果变化了swap回来不会乱掉嘛…
现在想想其实很好理解,因为递归每次调用都有这三行代码,因为最后一次递归的时候直接return,而回溯至倒数第二次递归,s[step]和s[i]经过两次swap还是原来的样子,再往前回溯也会经历两次swap后才回去,所以是正确的。
而这次二刷主要的疑问在于
1.深度优先实现全排列的思路(第一次其实就应该解决了
2.剪枝避免重复的思路

对于第一点,还是从数字的全排列开始逐渐延伸到所有的全排列问题,这里还是以最基础的啊哈算法的数字全排列来分析。这段代码实现了数字1-n的全排列。

void dfs(int step) {
    if(step == n + 1) {
        for(int i = 1; i <= n; i ++)
            printf("%d%c",array[i],i == n ?'\n':' ');
        return;
    }
    for(int i = 1; i <= n; i ++){
        if(book[i] == 0){
            array[step] = i;
            book[i] = 1;
            dfs(step + 1);
            book[i] = 0;
        }
    }
    return;
}

思路:对于每个step,遍历卡片直到可以得到一张能放置的卡,固定该数字后放入之后进入下一个盒子再进行同样的操作,回溯的时候再收回卡牌。

-----相比于每次都遍历全部元素,我们使用交换元素的办法来完成全排列会省去很多多余步骤
所以修改for循环部分

   for(int i=step;i<s.size() ;i++)
        {
            swap(s[step],s[i]);
            dfs(s,step+1);
            swap(s[step],s[i]);
        }

思路:

对于每个step,我们使s[step]元素与从之后的所有元素交换,交换之后固定进入下一个step,直到回溯的时候换回来。

递归过程

这里来直接手动解析递归的过程。
设最后一个step为n

  1. 对于每个dfs(step),我们建立一个for循环, 第一个for会循环s.size()次,step不断+1,for循环次数往后依次递减一次直到step==s.size()得到答案回溯。
for(int i=step;i<s.size() ;i++)
  1. 每个dfs(step)开启第一遍循环后等待后序dfs(step-1)完成回溯。
  2. 于是一直递归到最后一个元素与自身交换,这时候直接完成dfs(n)。
  3. 于是回溯dfs(n-1),dfs(n-1)换回元素之后进入for循环第二次,交换自己与下一个位置的元素,再次进入dfs(n)。
  4. 同样的最后一个元素与自身交换,完成dfs(n)。然后回溯到dfs(n-1)。
  5. dfs(n-1)的for循环结束,回溯至dfs(n-2)。
  6. 同理dfs(n-2)拥有两个循环,交换元素后进入dfs(n-1)。排列结束之后再回溯
  7. 不断经过上述步骤直到全排列。

时间空间复杂度

通过以上描述,我们能看出这种做法的思路,其实有点类似于动态规划,对于第step拆分成s.size()-step个第step-1步来得到其全排列。不断扩展又不断回溯。

且通过递归的思路分析可以得知时间复杂度为nx(n-1)x (n-2)…x 2 x 1 =O(n!)
空间复杂度为n+(n-1)+(n-2)+…+2+1=(n+1)*n/2 = O(n^2)

剪枝

但这还不够完美,因为全排列出现相同的元素的时候,可能出现两次的结果是相同的,
eg s=“abb” 结果有 abb abb bab bba bab bba,
那么这个时候为了得到 abb abb bab bba bab bba
我们就需要剪枝了,那么该怎么剪呢,根据我们上述递归的思路,出现重复可能的原因是在某个step步骤中,s[step]与s[i]进行交换,s[i]可能跟step~i之间的某个元素一样,这样交换就没有意义,因为我们for循环的i是从step开始往后的,所以在前面的交换过程中已经将这种情况包括了。
所以可以写出剪枝的函数:

//寻找s[i]到s[j-1]之间有没有元素和s[j]相同,有则返回true
bool cut(vector<int> &s , int i, int j) {
	for(int k = i; k < j; k ++) 
		if(s[k] == s[j]) return true;
	return false;
}

所以我们能得到全排列代码

class Solution {
public: 
    vector<string> ans;
    vector<string> permutation(string s) {
       dfs(s,0);
       return ans;   
    }
    bool cut(string&s,int be,int en)
    {
        for(int i=be;i<en;i++)
            if(s[i]==s[en])
            return true;
        return false;
    }
    void dfs(string& s,int step)
    {
        if(step>=s.size())
        {
            ans.push_back(s);
            return;
        }
        for(int i=step;i<s.size();i++)
        {
            if(cut(s,step,i))//剪枝
            	continue;
            swap(s[step],s[i]);
            dfs(s,step+1);
            swap(s[step],s[i]);
        }
    }

};

结论

所以基本不可能临时想出来啊!直接记住!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值