算法(一)——全排列与全组合

全排列

不带重复的全排列

互不相同的N个字符组成一个长度为N的字符串,输出全部可能组成的字符串

一、递归

递归实现比较简单,对于N个字符的排列来讲,其全排列可以取出任意一个字符X,加到除了X以外的N-1个字符的全排列的首部,当取完全部N个字符做首字符后就得到了N个字符的全排列。将以上思路直接以递归方式实现得到如下形式:

#include <iostream>
#include <string>
#include <vector>
using namespace std;
void perm(string& input,vector<string>& res,string& t)
{
    if(input.length()==1)
    {
        t.push_back(input[0]);
        res.push_back(t);
        t.pop_back();
    }
    else
    {
        for(int i=0;i<input.length();i++)
        {
            string tmp=input;
            t.push_back(tmp[i]);
            tmp.erase(i,1);
            perm(tmp,res,t);
            t.pop_back();
        }
    }
}

测试一下

int main()
{
    string t,input="123";
    vector<string> res;
    perm(input,res,t);
    for(int i=0;i<res.size();i++)
    {
        cout<<res[i]<<endl;
    }
    return 0;
}

输出为

123
132
213
231
312
321

可以看到结果是正确的

利用交换避免拷贝

通过递归的方式我们实现了算法的需求,可以得到正确的全排列,但是这个算法有一个问题,每一步都需要拷贝一次input,然后再删除其中一个元素,这个过程相当耗时,而且有额外的空间消耗。

我们考察全排列可以发现对于上述的递归过程的每一个子问题也可以看成是将当前长度为N的字符串的首位与串中所有字符进行交换,并对除首字符之外的剩下N-1个字符做全排列。

例如对于字符串:123
先将1和1交换,即先不做交换,这时剩下23,再交换23,得到123和132
然后1和2交换,这时数组变成213,然后再交换13,得到213和231
最后1和3交换,得到321和312
这样就可以得到123的全排列。

这里我们需要解决的一个问题是如何保证当N-1的全排列完成后能还原到最开始交换前的N。在递归实现的过程中其实这个操作其实很容易完成,只需要在每次完全求解完子问题后再把求解当前问题进行过的交换再换回来即可,具体代码如下:

void perm(string& input,vector<string>& res,int l,int r)
{
    if(l>=r)
    {
        res.push_back(input);
    }
    else
    {
        for(int i=l;i<r;i++)
        {
            swap(input[l],input[i]);
            perm1(input,res,l+1,r);
            swap(input[l],input[i]);
        }
    }
}

测试一下

int main()
{
    string t,input="123";
    vector<string> res;
    perm(input,res,0,input.length());
    for(int i=0;i<res.size();i++)
    {
        cout<<res[i]<<endl;
    }
    return 0;
}

执行结果为

123
132
213
231
312
321

可以看到同样得到了正确的结果,这样我们就避免了每一次的拷贝

带重复的全排列

当原本序列中存在重复元素的时候要进行全排列则需要去重。为了找到重复的原因这里,用不带重复的全排列程序处理2113,结果为

2113
2131
2113
2131
2311
2311

1213
1231
1123
1132
1312
1321

1123
1132
1213
1231
1321
1312

3112
3121
3112
3121
3211
3211

这里重复的排列有很多,但是我们先观察13-18这6个重复排列。分析一下可以发现,根据前面提到的不带重复的全排列的处理方法。要把每一个数和第一个数交换,然后再求剩余的全排列,因此1-6是以2为首+剩余3个数字的全排列,7-12是以1为首+剩余3个数字的全排列,13-18还是以1为首+剩余3个数字的全排列,我们发现在这之前1已经做过首字符了,这时再交换1为首字符自然会重复。而其他重复其实和这个是相同的问题,因为求解是递归的,当子问题里依然有两个1时必然会存在相同的情况。

那么为了避免这种因为重复字符引起的两个相同的字符被多次做为首字符,我们就需要考虑一下怎么判断。首先从前面的分析可以看出,如果当前要交换至首字符的字符与首字符之间已经出现过了,那么该字符必然已经做过首字符了,再次交换就会产生重复。因此在每次交换之前判断一下,这个交换到首字符的字符与首字符之间是否已经出现过了了,如果有就说明这个字符已经做过一次首字符了,这次交换重复了,可以跳过。

例如2113在2与第2个1交换时我们发现这之间已经有一个1了,那么说明2和1已经交换过了,1已经做过首字符了,这样这次如果再交换1又会再做一次首字符,就会产生重复,所以这次交换就直接跳过(continue)。具体代码如下

void perm(string& input,vector<string>& res,int l,int r)
{
    if(l>=r)
    {
        res.push_back(input);
    }
    else
    {
        for(int i=l;i<r;i++)
        {
            bool p=false;
            for(int j=l;j<i;j++)
            {
                if(input[j]==input[i]) 
                {
                    p=true;
                    break;
                }
            }
            if(p) continue;
            swap(input[l],input[i]);
            perm1(input,res,l+1,r);
            swap(input[l],input[i]);
        }
    }
}

这次我们再用2113测试一下,结果为

2113
2131
2311
1213
1231
1123
1132
1312
1321
3112
3121
3211

显然这次重复就全都被去掉了。

全组合

不带重复的全组合

全组合同样是一个递归问题。求N个字符中取k个的全组合,首先取第一个字符,这时问题就变成了第一个字符加上剩余N-1个字符中取k-1个字符的全组合问题了。而当取第一个字符的全组合都找到之后,全组合中剩余的组合必然不会再包含第一个字符了。这时问题又变成了2到N这N-1个字符取k的全组合了。依次计算到最后只剩最后k个字符,则只剩最后一组全组合,即找到了全部全组合。

实际上使用k重循环就可以直接实现全组合,例如从1234中取2个数进行组合,那么通过如下循环即可得到全部组合

string tmp,input="1,2,3,4";
for(int i=0;i<3;i++)
{
	tmp+=input[i];
	for(int j=i;j<4;j++)
	{
		tmp+=input[j];
		cout<<tmp<<'\n'
		tmp.pop_back();
	}
	tmp.pop_back();
}

所以实际上需要几个数字的全组合就使用几重循环即可完成,但这里存在一个问题,就是事先我们可能不知道需要几个数字的全组合,也无法确定需要写几重循环,而且当循环特别多的时候写的也很麻烦,这里直接使用递归来实现多重循环。

void comb(string &input,string &tmp,vector<string> &res,int l,int k)
{
    if(k<1) return;
    for(int i=l;i<input.size()-k+1;i++)
    {
        tmp+=input[i];
        if(k==1) res.push_back(tmp);
        comb(input,tmp,res,i+1,k-1);
        tmp.pop_back();
    }
}

测试一下

int main()
{
    string input="12345",tmp;
    vector<string> res;
    comb(input,tmp,res,0,3);
    for(auto p=res.begin();p!=res.end();p++)
    {
        cout<<*p<<'\n';
    }
    return 0;
}

结果为

123
124
125
134
135
145
234
235
245
345

可以看到结果并没有什么问题,所以这是可行的。

带重复的全组合

对于带重复的全组合则同样存在一个去重的问题。全组合去重就十分简单了,首先我们将序列排一次序。这样重复的数就集中到一起了,这时每一层循环对于相同的数只取一次即可保证不会出现重复组合。

void comb(string &input,string &tmp,vector<string> &res,int l,int k)
{
    if(k<1) return;
    if(l==0) sort(input.begin(),input.end());
    for(int i=l;i<input.size()-k+1;i++)
    {
        if(i>l&&input[i]==input[i-1]) continue;
        tmp+=input[i];
        if(k==1) res.push_back(tmp);
        comb(input,tmp,res,i+1,k-1);
        tmp.pop_back();
    }
}

测试一下

int main()
{
    string input="1112345",tmp;
    vector<string> res;
    comb(input,tmp,res,0,2);
    for(auto p=res.begin();p!=res.end();p++)
    {
        cout<<*p<<'\n';
    }
    return 0;
}

结果为

11
12
13
14
15
23
24
25
34
35
45

可以看到结果是正确的

发布了22 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/artorias123/article/details/100573994
今日推荐