首页 > 代码库 > 利用递归和动态规划来求解组合数

利用递归和动态规划来求解组合数

组合数定义:从m个不同元素中,任取n(n≤m)个元素并成一组,叫做从m个不同元素中取出n个元素的一个组合;从m个不同元素中取出n(n≤m)个元素的所有组合的个数,叫做从m个不同元素中取出n个元素的组合数。

下面是一种比较通俗的计算公式:


其递归公式为:

c(n,m)=c(n-1,m-1)+c(n-1,m)

下面是c++实现该递归算法:

#include <iostream>
#include <stdlib.h>
#define EXIT -1
using namespace std;
/************************************************************************/
/* m	组合数下标
   n    组合数上标
/************************************************************************/
int combine(int m,int n){
	if(m<n){
		cout<<"输入组合数无效"<<endl;
		exit(EXIT);
	}
	if(n==0||m==n){
		return 1;
	}else{
		return combine(m-1,n-1)+combine(m-1,n);
	}
	return EXIT;
}

int main(void){

	cout<<"请输入你要求解的组合数的上标和下标m为上标,n为下标"<<endl;
	int m,n;
	cout<<"m = ";
	cin>>m;
	cout<<"n = ";
	cin>>n;
	cout<<"您要求解的组合数的值为:"<<combine(m,n)<<endl;
	system("pause");
	return 1;
}

java版:

package cn.demo;

import java.util.Scanner;

public class Combine {
	static int combine(int m,int n){
		if(m<n){
			System.out.println("输入组合数无效");
		}
		if(n==0||m==n){
			return 1;
		}else{
			return combine(m-1, n-1)+combine(m-1, n);
		}
	}
	public static void main(String[] args) {
		@SuppressWarnings("resource")
		Scanner in = new Scanner(System.in);
		System.out.println("请输入你要求解的组合数,m为下标,n为上标");
		int m,n;
		System.out.print("m=");
		m = in.nextInt();
		System.out.print("n=");
		n = in.nextInt();
		int result = combine(m, n);
		System.out.println("您要求解的组合数结果为:"+result);
	}
}

但是在反复递归调用的时候,我们发现重复计算了一些值,可能这么看是看不出什么,下面用一张图来说明其调用过程:



我只画了部分,如果该组合数足够大的话,那么在递归调用的过程中则会有很多重复计算,效率是十分低的,因此在求类似问题的时候,我们都会去实用动态规划来求解.那什么是动态规划呢?

动态规划算法,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

因此,在实用动态规划的算法中,这个临时线性表是非常重要的,该表是一个二维数组;

下面是c++版:

#include <iostream>
#include <stdlib.h>
using namespace std;
int combine(int m,int n){
	if(m<n){
		cout<<"组合数无效"<<endl;
	}
	if(n==0||n==m){
		return 1;
	}
	int **temporary = new int *[m];//申请m个一维数组,或动态的二维数组,之所以不直接实用二维数组,因为避免下标问题
	for (int i=0;i<m;i++)
	{
		temporary[i] = new int[i+2];//二维数组的每一行长度都自动加2
		temporary[i][0] = 1;//二维数组的每一行的首尾值都是1
		temporary[i][i+1] = 1;//二维数组的每一行的末尾值也为1
		int j;          //二维数组的列
		for (j=1;j<=i;j++)///因为上述的每行的第一列都设置为1,因此从第2列开始
		{
			temporary[i][j] = temporary[i-1][j-1]+temporary[i-1][j];//计算第(i+1)行第(j+1)列的值
		}
		//打印出二维数组
		for (j=0;j<=i+1;j++)
		{
			cout<<temporary[i][j]<<"  ";
		}
		cout<<"\n";
	}
	return temporary[m-1][n];
}
int main(void){
	cout<<"请输入你要求解的组合数:m为下标,n为下标"<<endl;
	int m,n;
	cout<<"m = ";
	cin>>m;
	cout<<"n = ";
	cin>>n;
	int result = combine(m,n);
	cout<<"你要求的组合数的值为:"<<result<<endl;
	system("pause");
	return 1;
}

java版:

package cn.demo;

import java.util.Scanner;

public class Combine {
	static int combine(int m,int n){
		if(m<n){
			System.out.println("输入组合数无效");
		}
		if(n==0||m==n){
			return 1;
		}
		int temp[][] = new int [m][];//申请而为数组
		for (int i = 0; i < temp.length; i++) {
			temp[i] = new int[i+2];//动态增加数组长度
			temp[i][0] = 1;//每行首尾为1
			temp[i][i+1] = 1;//每行末尾为1
			int j;
			for (j = 1; j <= i; j++) {
				temp[i][j] =temp[i-1][j-1]+temp[i-1][j];
			}
			for(j=0;j<=i+1;j++){
				System.out.print(temp[i][j]+" ");
			}
			System.out.println();
		}
		return temp[m-1][n];
	}
	public static void main(String[] args) {
		@SuppressWarnings("resource")
		Scanner in = new Scanner(System.in);
		System.out.println("请输入你要求解的组合数,m为下标,n为上标");
		int m,n;
		System.out.print("m=");
		m = in.nextInt();
		System.out.print("n=");
		n = in.nextInt();
		int result = combine(m, n);
		System.out.println("您要求解的组合数结果为:"+result);
	}
}

从上面的程序可以看出,使用动态规划的时候,我们必须对所求问题的子结构了解的很透彻,在分解组合数的时候,它的两个子结构之间并不像分治算法那样相互独立,之间基本上是相互包含的,因此,如果使用递归,则其子结构在调用的时候,另一个子结构则会重复该子结构中已经存在的过程,如果m和n足够大的时候,重复率我们无法接受,因此这个使用使用动态规划则能很好的解决问题,尽管在算法的实现上有些麻烦,但是在很大程度上减少空间和时间复杂度,这一点,还是很让人值得尝试.

使用动态规划的核心是我们必须有一个容器来保存计算过程中产生的结果,而且这个容器是随着问题域大小二动态变化的.有时候会是一个栈,有时候是一个线性表,如果问题更复杂,则会是一个动态树或图.