递推递归与排列组合

递推递归与排列组合

说明

排列组合

排列组合问题在暴力枚举的情况一般有3种情况

我们在此记个数为N

  • 情况一:打印n个数的全排列:
[N = n!
]

  • 情况二:打印n个数中任意m个数的全排列
[N = A_{n}^{m} = frac{n!}{(n-m)!}
]

  • 情况三:打印n个数中任意m个数的组合
[N = C_{n}^{m} = frac{n!}{m!(n-m)!}
]

递推与递归

递推是按照一定的规律来计算序列中的每个项,通常是通过计算前面的一些项来得出序列中的指定项的值。其思想是把一个复杂的庞大的计算过程转化为简单过程的多次重复,该算法利用了计算机速度快和不知疲倦的机器特点。

所谓递归,实际上是一种把大问题逐步缩小成最小同类问题的过程,其中最小问题的解是已知的,往往是所给条件。

比较两者,可清楚的发现递归是逆向的递推。递推是从最小问题出发一步步向前求解,递归则是从最大问题开始不断去调用自己达到最小问题的解然后再逐层归来。

所以一般来讲递推的效率大于递归,并且在递归中问题的n是在计算前就已经知道的,而递推可以在求解中确定。

我们以斐波那契数列距离,其数学递推公式如下(无论递推还是递归,一般都从递推公式开始分析)

[begin{cases}f(n) = f(n-1) + f(n-2)
\f(1) = f(2) = 1
\n>2,nin {N}^{+}

end{cases}
]

接下来我们利用代码来分别示范一遍两种方法

  • 斐波那契——递归
#include 
#include 

using namespace std;
//由于数列发散快,使用int类型当n较大时可能会溢出,此处仅为举例
int func_fib(unsigned int n)
{
    if(n==0)    //尽管递推公式给出n不会等于零,但为了以防万一添加了n为0的情况
        return 0;
    if(n==1 || n==2)
        return 1;
    else
        return func_fib(n-1) + func_fib(n-2);
}

int main()
{
    unsigned int n = 0;
    cout > n;
    
    //输出结果和所用时间,单位:时钟单位
    clock_t start,end;
    start = clock();    //开始
    cout 
  • 斐波那契——递推
#include 
#include 

using namespace std;

//同样,n过大会溢出
int func_fib(unsigned n)
{
    int fib = 0;
    int fib_n = 1;
    for(int i=0; i> n;
    
    //输出结果和所用时间
    clock_t start,end;
    start = clock();    //开始
    cout 

算法

我们对上述排列组合对三种情况写代码

情况一

输出全排列

STL方法:

#include 
#include 
#include 

using namespace std;

//输出序列
void printSequence(vector num)
{
    vector::iterator it;   //迭代器
    cout  num{2,1,5,3,4};
    //排序(正序)
    sort(num.begin(), num.end());
    //输出全排列
    do{
        printSequence(num);
    }while(next_permutation(num.begin(), num.end()));
    
    return 0;
}

递归方法:

#include 
#include 
#include 

using namespace std;

void printSequence(vector num)
{
    vector::iterator it;   //迭代器
    cout  &num, vector::iterator begin, vector::iterator end)
{
    if(begin == end-1)
        printSequence(num); //打印,此处还可以统计个数
    else{
        vector::iterator it;
        for(it=begin; it num, vector::iterator it)
//{
//    num[0] = 10;
//    cout  num{1,2,3};
    Perm(num, num.begin(), num.end());
    
    return 0;
}

情况二

想要打印n个数中任意m个数的全排列其实很简单,只需要在上述代码中稍作修改即可

打印序列函数printSequence

void printSequence(vector num, vector::iterator begin, vector::iterator end)
{
	vector::iterator it;   //迭代器
	cout 

获取全排列的函数Perm(增加了计数功能,需传入变量cnt)

/*
 *这里num必须用引用。
 *否则迭代器指向的num是原实例,而传入的num是拷贝的实例,输出结果都是原序列。
 *详细情况可以调用下方的test尝试下(该函数已被注释)
 *参数:num:原序列,begin:vector的begin迭代器, end:vector的end迭代器,m:从n个数中打印m个数的全排列中的m,cnt:用于统计最后全排列个数
 */
void Perm(vector& num, vector::iterator begin, vector::iterator end, unsigned int m, int &cnt)
{
	if (begin == num.begin() + m)
	{
		printSequence(num, num.begin(), num.begin() + m); //打印
		++cnt;	//统计个数
	}
	else {
		vector::iterator it;
		for (it = begin; it 

情况三

组合不同于排列,排列有序而组合无序。所以,我们先来研究其子集的生成。

我们按如下记集合A,其中n为其元素的个数

[A = left { x_0,x_1,x_2,...,x_{n-1} right }
]

其子集有

[phi ,left { x_0 right } ,left { x_1 right },...,left { x_0,x_1,...,x_{n-1}right }
]

不妨设子集个数为N,则有

[N = 2^{n}
]

这个式子很容易跟二进制联系起来,我们不由得会去想子集跟二进制之间的关系

我们不妨构造一个n位的二进制数,每一位都对应集合中的一个元素,当子集中包含该元素,则对应的二进制数的该位上的值就为1否则为0,那么每个子集都对应一个独一无二的n位二进制数了。

以n=3时的集合为例,此时有

[A = left { x_0,x_1,x_2 right }
]

则其子集与二进制数对应关系如下

子集 空集 x0 x1 x0,x1 x2 x2,x0 x2,x1 x2,x1,x0
二进制数 000 001 010 011 100 101 110 111

此外我们还需要知道:

  • 位运算
1
  • 与运算
[10001 & 101 = 10001 & 00101 = 00001
]

此处与运算仅举例,当两个二进制数位数不一致则再前面补0然后对应位置做与运算

接下来我们以打印集合{0,1,2,..,n-1}的子集为例,则有如下函数

void print_subset(int n)
{
	for (int i = 0; i 

那么基础知识已经介绍完了,我们回到情况三,对照子集对应二进制数的方法。我们知道一个子集对应唯一的一个二进制数,那么一个有k个元素的子集它对应二进制数一定有k个1。所以找查子集的问题就变成了找查含k个1的二进制数。

这里介绍个技巧(kk为一个名为kk的二进制数)

kk = kk & (kk-1)

它可以跳过0消除数中最后一个1,这样我们执行此操作的次数就是二进制数中含1的个数

则针对情况三有如下代码

//打印n个数中取m个数的组合
void print_set(int n, int m)
{
	for (int i = 0; i 

参考

  • 罗永军,郭卫斌. 算法竞赛入门到进阶[M]. 北京 : 清华大学出版社,2019.

文章来源于互联网:递推递归与排列组合

THE END
分享
二维码