整数的划分

题目:整数的划分(即求一个数N由小于等于N的数相加所得到的所有组合)
比如整数6的划分有11种方案:
6
5+1
4+2,4+1+1
3+3,3+2+1,3+1+1+1,
2+2+2,2+2+1+1,2+1+1+1+1
1+1+1+1+1+1
 
下面给出3种求解方案数目的方法,其中第三个方法也实现输出各个方案的功能。

-----------------------------------------------------------------------------------------
(一)递归法
-----------------------------------------------------------------------------------------
根据 n m 的关系,考虑以下几种情况:
1. n=1 时,不论 m 的值为多少( m>0) ,只有一种划分即 {1}
2. m=1 时,不论 n 的值为多少,只有一种划分即 n 1 {1,1,1,...,1}
3. n=m 时,根据划分中是否包含 n ,可以分为两种情况:
a) 划分中包含 n 的情况,只有一个即 {n}
b) 划分中不包含 n 的情况,这时划分中最大的数字也一定比 n 小,即 n 的所有 (n-1) 划分。
因此 f(n,n) =1 + f(n,n-1)
4. n<m 时,由于划分中不可能出现负数,因此就相当于 f(n,n)
5. n>m 时,根据划分中是否包含最大值 m ,可以分为两种情况:
a) 划分中包含 m 的情况,即 {m, {x1,x2,...xi}}, 其中 {x1,x2,... xi} 的和为 n-m ,可能再次出现 m ,因此是( n-m )的 m 划分,因此这种划分个数为 f(n-m, m)
b) 划分中不包含 m 的情况,则划分中所有值都比 m 小,即 n (m-1) 划分,个数为 f(n,m-1)
因此 f(n, m) = f(n-m, m)+f(n,m-1)
综合以上情况,我们可以看出,上面的结论具有递归定义特征,其中 1 2 属于回归条件, 3 4 属于特殊情况,将会转换为情况 5 。而情况 5 为通用情况,属于递推的方法,其本质主要是通过减小 m 以达到回归条件,从而解决问题。其递推表达式如下:
    
      f(n, m)= 1; (n=1 or m=1)
  f(n, n); (n<m)
  1+ f(n, m-1); (n=m)
  f(n-m,m)+f(n,m-1); (n>m)
因此我们可以给出求出f(n, m)的递归函数代码如下:
int PartitionCount(int n, int m)
{
	if(n==1 || m==1)
		return 1;
	else if(n < m)
		return PartitionCount(n,n);
	else if(n==m)
		return 1+PartitionCount(n,n-1);
	else
		return PartitionCount(n,m-1)+PartitionCount(n-m,m);
}

---------------------------------------------------------------------------------
(二)母函数法
---------------------------------------------------------------------------------

下面我们从另一个角度即“母函数”的角度来考虑这个问题。

        所谓母函数,即为关于x的一个多项式G(x):

        有 G(x)= a0 + a1*x + a2*x^2 + a3*x^3 + ...

        则我们称G(x)为序列(a0,a1,a2,...)的母函数。关于母函数的思路我们不做更多分析。

        我们从整数划分考虑,假设n的某个划分中,1的出现个数记为a1,2的个数记为a2,..., i的个数记为ai,

        显然: ak<=n/k; (0<= k <=n)

        因此n的划分数f(n,n),也就是从1到n这n个数字中抽取这样的组合,每个数字理论上可以无限重复出现,即个数随意,使他们的总和为n。显然,数字i可以有如下可能,出现0次(即不出现),1次,2次,..., k次,等等。把数字i用(x^i)表示,出现k次的数字i用 x^(i*k)表示, 不出现用1表示。例如数字2用x^2表示,2个2用x^4表示,3个2用x^6表示,k个2用x^2k表示。

         则对于从1到N的所有可能组合结果我们可以表示为:

         G(x) = (1+x+x^2+x^3+...+x^n) (1+x^2+x^4+...) (1+x^3+x^6+...) ... (1+x^n)

                 = g(x,1) g(x,2) g(x,3) ... g(x, n)

                 = a0 + a1* x + a2* x^2 + ... + an* x^n + ... ;  (展开式)

        上面的表达式中,每一个括号内的多项式代表了数字i的参与到划分中的所有可能情况。因此该多项式展开后,由于x^a * x^b=x^(a+b),因此 x^i 就代表了i的划分,展开后(x^i)项的系数也就是i的所有划分的个数,即f(n,n)=an (上式中g(x,i)表示数字i的所有可能出现情况)。

        由此我们找到了关于整数划分的母函数G(x);剩下的问题是,我们需要求出G(x)的展开后的所有系数。

        为此我们首先要做多项式乘法,对于我们来说并不困难。我们把一个关于x的一元多项式用一个整数数组a[]表示,a[i]代表x^i的系数,即:

        g(x) = a[0] + a[1]x + a[2]x^2 + ... + a[n]x^n;

        具体实现源码:

      多项式相乘:

/*
	多项式相乘,第二个多项式有点特殊,只有x的k的倍数项。另外,其项的系数为1。
	所以,程序中第二个循环中的跳跃步数为k。
     */
    void PolyMul(int*& a, int n, int k)
   {
	int i,j;
	int num = n+1;
	int* c = new int[n+1]();
	for (i=0;i<num;++i)
	{
		for(j=0;j<num-i;j+=k)
			c[i+j] += a[i]; // 等价于c[i+j] += a[i]*1;
	}

	for (i=0;i<num;++i)    // 把最终结果保存到a中,用于与下一个多项式相乘
		a[i]=c[i];
   }

      n的整数划分个数:

   

// x^n的系数,即为n的整数划分的个数
    int CalFactorN(int n)
   {
	int num = n+1;          // x^0 ~ x^n , 总共n+1个。
	int* a = new int[num]();

	for (int i=0;i<num;++i) // 第一个多项式
		a[i] = 1;

	for (int k=2;k<=n;++k)
	{
		PolyMul(a,n,k);     // 与第k个多项式相乘
	}

	return a[n];  // n的整数划分的个数
    }

  

---------------------------------------------------------------------------------
(三)普通方法
---------------------------------------------------------------------------------

     在题目给的例子中,我们可以发现两个规律:

     (1)除了第一行只有一个数6之外,从上到下,在第二个加数为1时,下一个方案第一个加数必定减1,转到下一行。

     (2)从左到右,右边的加数一定不大于左边的加数。在同一行中,上一个方案与下一个方案的不同在于,与加数为1相邻的左边那个不为1的加数分解出一个1得到下一方案。

      基于以上思路,源码:

/*
  整数的划分,输入n,输出划分方案数
*/
int IntPartition(int n)
{
	int* a = new int[n]();

	// n 和 n-1 1 特殊处理
	a[0] = n;
	cout<<a[0]<<endl;
	a[0] = n-1;
	a[1] = 1;
	cout<<a[1]<<a[0]<<endl;

	int count = 2;

	//第一个加数为1时,后面的所有加数必定都为1,即为最后一个方案。
	while(a[0]>1)
	{
		// 第一种情况,第二个加数等于1.
		if(a[1]==1)
		{
			a[0]--;
			int i = 1;
			int rest = n-a[0];
			while(rest>a[0])
			{
				a[i++] = a[0];
				rest -= a[0];
			}
			a[i] = rest;

			int j=i+1;
			while(j<n)
			{
				if(a[j]==0) 
					break;
				a[j++]=0;
			}

			// 输出当前方案
			while(i) cout<<a[i--];
			cout<<a[0]<<endl;

			// 计数
			count++;
		}
		// 第二种情况,第二个加数不为1,那么必定大于1
		else if(a[1]>1)
		{
			int i = 1;
			while(a[i]>1) i++;
			a[i-1]--;
			while(a[i]!=0) i++;
			a[i] = 1;

			// 输出当前方案
			while(i)
				cout<<a[i--];
			cout<<a[0]<<endl;
			
			// 计数
			count++;
		}
	}

	return count;
}





 




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值