递推递归与排列组合

递推递归与排列组合

说明

排列组合

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

我们在此记个数为N

  • 情况一:打印n个数的全排列:

N = n ! N = n! N=n!

  • 情况二:打印n个数中任意m个数的全排列

N = A n m = n ! ( n − m ) ! N = A_{n}^{m} = \frac{n!}{(n-m)!} N=Anm=(nm)!n!

  • 情况三:打印n个数中任意m个数的组合

N = C n m = n ! m ! ( n − m ) ! N = C_{n}^{m} = \frac{n!}{m!(n-m)!} N=Cnm=m!(nm)!n!

递推与递归

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

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

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

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

我们以斐波那契数列距离,其数学递推公式如下(无论递推还是递归,一般都从递推公式开始分析)
$$
\begin{cases}f(n) = f(n-1) + f(n-2)
\f(1) = f(2) = 1
\n>2,n\in {N}^{+}

\end{cases}
$$
接下来我们利用代码来分别示范一遍两种方法

  • 斐波那契——递归
#include <iostream>
#include <ctime>

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: " << endl;
    cin >> n;
    
    //输出结果和所用时间,单位:时钟单位
    clock_t start,end;
    start = clock();    //开始
    cout << func_fib(n) << endl;
    end = clock();      //结束
    cout << "计算所用时间为: " << (double)(end-start) << endl;
    
    return 0;
}


  • 斐波那契——递推
#include <iostream>
#include <ctime>

using namespace std;

//同样,n过大会溢出
int func_fib(unsigned n)
{
    int fib = 0;
    int fib_n = 1;
    for(int i=0; i<n; i++)
    {
        int temp = fib_n;
        fib_n = fib + fib_n;
        fib = temp;
    }
    return fib;
}

int main()
{
    cout << "请输入所求斐波那契数列项的项数n: " << endl;
    unsigned int n;
    cin >> n;
    
    //输出结果和所用时间
    clock_t start,end;
    start = clock();    //开始
    cout << func_fib(n) << endl;
    end = clock();      //结束
    cout << "计算所用时间为:" << (double)(end-start) << endl;
    
    return 0;
}

算法

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

情况一

输出全排列

STL方法:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

//输出序列
void printSequence(vector<int> num)
{
    vector<int>::iterator it;   //迭代器
    cout << "当前序列: ";
    //遍历
    for(it=num.begin(); it!=num.end(); it++)
        cout << *it << " ";
    cout << endl;
}

int main()
{
    /*
     * 注意,使用sort()与next_permutation()需要包含algorithm头文件
     */
    
    //初始化序列
    vector<int> num{2,1,5,3,4};
    //排序(正序)
    sort(num.begin(), num.end());
    //输出全排列
    do{
        printSequence(num);
    }while(next_permutation(num.begin(), num.end()));
    
    return 0;
}

递归方法:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void printSequence(vector<int> num)
{
    vector<int>::iterator it;   //迭代器
    cout << "当前序列: ";
    //遍历
    for(it=num.begin(); it!=num.end(); it++)
        cout << *it << " ";
    cout << endl;
}

/*
 *这里num必须用引用。
 *否则迭代器指向的num是原实例,而传入的num是拷贝的实例,输出结果都是原序列。
 *详细情况可以调用下方的test尝试下(该函数已被注释)
 */
void Perm(vector<int> &num, vector<int>::iterator begin, vector<int>::iterator end)
{
    if(begin == end-1)
        printSequence(num); //打印,此处还可以统计个数
    else{
        vector<int>::iterator it;
        for(it=begin; it<end; it++)
        {
            swap(*begin, *it);
            Perm(num, begin+1, end);
            swap(*begin, *it);
        }
    }
}

//void test(vector<int> num, vector<int>::iterator it)
//{
//    num[0] = 10;
//    cout << *it << " " << num[0] << endl;
//}

int main()
{
    vector<int> num{1,2,3};
    Perm(num, num.begin(), num.end());
    
    return 0;
}

情况二

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

打印序列函数printSequence

void printSequence(vector<int> num, vector<int>::iterator begin, vector<int>::iterator end)
{
	vector<int>::iterator it;   //迭代器
	cout << "当前序列: ";
	//遍历
	for (it = begin; it != end; it++)
		cout << *it << " ";
	cout << endl;
}

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

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

情况三

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

我们按如下记集合A,其中n为其元素的个数
A = { x 0 , x 1 , x 2 , . . . , x n − 1 } A = \left \{ x_0,x_1,x_2,...,x_{n-1} \right \} A={x0,x1,x2,...,xn1}
其子集有
ϕ , { x 0 } , { x 1 } , . . . , { x 0 , x 1 , . . . , x n − 1 } \phi ,\left \{ x_0 \right \} ,\left \{ x_1 \right \},...,\left \{ x_0,x_1,...,x_{n-1}\right \} ϕ,{x0},{x1},...,{x0,x1,...,xn1}
不妨设子集个数为N,则有
N = 2 n N = 2^{n} N=2n
这个式子很容易跟二进制联系起来,我们不由得会去想子集跟二进制之间的关系

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

以n=3时的集合为例,此时有
A = { x 0 , x 1 , x 2 } A = \left \{ x_0,x_1,x_2 \right \} A={x0,x1,x2}
则其子集与二进制数对应关系如下

子集空集x0x1x0,x1x2x2,x0x2,x1x2,x1,x0
二进制数000001010011100101110111

此外我们还需要知道:

  • 位运算
1<<n	//该位运算等价于2的n次幂
  • 与运算

10001 & 101 = 10001 & 00101 = 00001 10001 \& 101 = 10001 \& 00101 = 00001 10001&101=10001&00101=00001

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

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

void print_subset(int n)
{
	for (int i = 0; i < (1 << n); i++)	//从000到2^n-1对应的二进制,一共2^n个子集
	{
		//将每个子集都取出,对应数字i
		//在子集i的情况下,不断尝试取元素个数为j,其中每个元素的值跟其序号相同都为j
		for (int j = 0; j < n; j++)	//如果子集i中含多个数,此处的for则打印多个数,若i仅1个数此处for也只寻找并打印仅含的这一个数j
			if (i & (1 << j))		//将2的j次幂转化为二进制形式,通过与运算,去测试仅包含一个十进制数j的集合是不是该子集的子集
				cout << j << " ";	//如果是,则说明子集i中包含十进制数j,此时打印j。
		cout << endl;
	}
}

那么基础知识已经介绍完了,我们回到情况三,对照子集对应二进制数的方法。我们知道一个子集对应唯一的一个二进制数,那么一个有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 < (1 << n); i++)
	{
		int cnt = 0, kk = i;
		while (kk)
		{
			kk = kk & (kk - 1);
			cnt++;
		}
		if (cnt == m)
		{
			for (int j = 0; j < n; j++)	//此处跟上方print_subset函数同理
				if (i & (1 << j))
					cout << j << " ";
			cout << endl;
		}
	}
}

参考

  • 罗永军,郭卫斌. 算法竞赛入门到进阶[M]. 北京 : 清华大学出版社,2019.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数学·统计学系列:组合数学 作者:陈景润 编 出版时间:2012年版 内容简介   《数学·统计学系列:组合数学》为组合数学的经典教材,共分为六章。书中列举了大量组合问题和例题,并尽可能使用初等方法来解决它们,以使广大读者能够掌握组合论的思想和方法。本书内容丰富,叙述由浅入深,每章都有习题,另附习题解答。本书对初学组合论的读者是一本较好的入门书,对于中学教师、大学理工科学生和广大的工程技术人员以及从事科学研究的工作者也是一本较好的参考书。 目录 第一章 引言 §1 洛书的传说和构成 §2 关于斐波那契数列 §3 哥尼斯堡的七桥问题 §4 计数趣谈 §5 数学归纳法 习题 第二章 排列与组合 §1 排列 §2 组合 §3 (n),和(n/y)的取值范围的扩充 §4 二项式定理和它的应用 §5 多项式定理 习题 第三章 抽屉原则 §1 抽屉原则的最简形式 §2 抽屉原则的一般形式 §3 关于Ramsey定理 §4 置换 习题 第四章 容斥原理 §1 集合的基本知识 §2 关于容斥原理 §3 容斥原理的应用 §4 更列 §5 几个基本概念 习题 第五章 递推关系与母函数 §1 几个例子 §2 线性递归关系式的解 §3 第一类Stirling数 §4 母函数 §5 第二类Stirling数 §6 Bernourlli数 习题 第六章 关于杨辉一高斯级数 §1 引言 §2 杨辉一高斯级数的推广 §3 差分表 §4 我们的新计算方法 习题 习题解答 第一章 第二章 第三章 第四章 第五章 第六章 编辑手记

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值