n=m1+m2+...+mi; (其中mi为正整数,并且1 <= mi <= n),则{m1,m2,...,mi}为n的一个划分。
如果{m1,m2,...,mi}中的最大值不超过m,即max(m1,m2,...,mi)<=m,则称它属于n的一个m划分。这里我们记n的m划分的个数为f(n,m);
例如但n=4时,他有5个划分,{4},{3,1},{2,2},{2,1,1},{1,1,1,1};
注意4=1+3 和 4=3+1被认为是同一个划分。
该问题是求出n的所有划分个数,即f(n, n)。下面我们考虑求f(n,m)的方法;
递归法:
根据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)
unsigned long GetPartitionCount(int n, int max)
{
if (n == 1 || max == 1)
return 1;
else if (n < max)
return GetPartitionCount(n, n);
else if (n == max)
return 1 + GetPartitionCount(n, max-1);
else
return GetPartitionCount(n,max-1) + GetPartitionCount(n-max, max);
}
DP:
1: 将n划分成若干正整数之和的划分数。
2: 将n划分成k个正整数之和的划分数。
3: 将n划分成最大数不超过k的划分数。
4: 将n划分成若干个 奇正整数之和的划分数。
5: 将n划分成若干不同整数之和的划分数。
一:
1.若划分的多个整数可以相同
设dp[i][j]为将i划分为不大于j的划分数
(1) 当i<j时,i不能划分为大于i的数,所以dp[i][j]=dp[i][i];
(2) 当i>j时,可以根据划分中是否含有j分为两种情况。若划分中含有j,划分方案数为dp[i-j][j];若划分数中不含j,相当于将i划分为不大于j-1的划分数,为dp[i][j-1]。所以当i>j时dp[i][j]=dp[i-j][j]+dp[i][j-1];
(3) 当i=j时,若划分中含有j只有一种情况,若划分中不含j相当于将i划分为不大于j-1的划分数。此时dp[i][j]=1+dp[i][j-1]。
dp[n][n]可以解决问题1,dp[n][k]表示将n划分为最大数不超过k的划分数,可以解决问题3。
2.若划分的正整数必须不同
设dp[i][j]为将i划分为不超过j的不同整数的划分数
(1) 当i<j时,i不能划分为大于i的数,所以dp[i][j]=dp[i][i];
(2) 当i>j时,可以根据划分中是否含有j分为两种情况。若划分中含有j,则其余的划分中最大只能是j-1,方案数为dp[i-j][j-1];若划分中不含j,相当于将i划分为不大于j-1的划分数,为dp[i][j-1]。所以当i>j时dp[i][j]=dp[i-j][j-1]+dp[i][j-1];
(3) 当i=j时,若划分中含有j只有一种情况,若划分中不含j相当于将i划分为不大于j-1的划分数。此时dp[i][j]=1+dp[i][j-1]
dp[n][n]表示将n划分为不同整数的划分数,可以解决问题5.
二 将n划分为k个整数的划分数
设dp[i][j]为将i划分为j个整数的划分数。
(1) i<j为不可能出现的情况,dp[i][j]=0;
(2) 若i=j,有一种情况:i可以划分为i个1之和,dp[i][j]=1;
(3) 若i>j,将i划分成j个数的划分法:
dp[i][j]= dp[i-j][j]+ dp[i-1][j-1];
方法可以分为两类:
第一类: i 份中不包含 1 的分法,为保证每份都 >= 2,可以先拿出 j个 1 分
到每一份,然后再把剩下的 i- j 分成 j 份即可,分法有: dp[i-j][j]
第二类: i 份中至少有一份为 1 的分法,可以先那出一个 1 作为单独的1份,剩
下的 i- 1 再分成 j- 1 份即可,分法有:dp[i-1][j-1]
dp[i][j]为将i划分为j个整数的划分数,可解决问题2。
三 将n划分为若干正奇数之和的划分数
设f[i][j]为将i划分为j个奇数之和的划分数,g[i][j]为将i划分为j个偶数之和的划分数。
使用截边法,将g[i][j]的j个划分都去掉1,可以得到f[i-j][j],所以
g[i][j] = f[i-j][j]。
f[i][j]中有包含1的划分方案和不包含1的划分方案。对于包含1的划分方案,可以将1的划分除去,转化为“将i-1划分为j-1个奇数之和的划分数”,即f[i-1][j-1];对于不包含1的划分方案,可以使用截边法对j个划分每一个都去掉一个1,转化为“将i-j划分为j个偶数之和的划分数”,即g[i-j][j]。
所以f[i][j]=f[i-1][j-1]+g[i-j][j]。
f[n][0]+f[n][1]+……+f[n][n]为将n划分为若干奇数的划分数,为问题4的答案。
问题扩展1(转帖):将正整数划分成连续的正整数之和
如15可以划分成4种连续整数相加的形式:
15
7 8
4 5 6
1 2 3 4 5
思想:首先考虑一般的形式,设n为被划分的正整数,x为划分后最小的整数,如果n有一种划分,那么结果就是x,如果有两种划分,就是x和 x + 1,如果有m种划分,就是 x 、x + 1 、x + 1 、x + 2 、... 、x + m - 1,将每一个结果相加得到一个公式(i * x + i * (i - 1) / 2) = n,i为当前划分后相加的正整数个数。
满足条件的划分就是使x为正整数的所有情况。
如上例,当i = 1时,即划分成一个正整数时,x = 15
当i = 2时, x = 7。当i = 3时,x = 4
当i = 4时,4/9,不是正整数
因此,15不可能划分成4个正整数相加。当i = 5时,x = 1。
这里还有一个问题,这个i的最大值是多少?不过有一点可以肯定,它一定比n小。我们可以做一个假设,假设n可以拆成最小值为1的划分,如上例中的1 2 3 4 5。这是n的最大数目的划分。如果不满足这个假设,那么 i 一定比这个划分中的正整数个数小。因此可以得到这样一个公式i * (i + 1) / 2 <= n,(本质就是从1开始累加到i)即当i满足这个公式时n才可能被划分。
问题扩展2(转帖):一个数分解为N个数的和,使这N个数的积为最大
把n分成若干个3和若干个2的和,其中3尽量多,2只能是0个、1个或2个(尽量少),则他们的积为最大,具体方法为:
1)n=3k时,把n分解成k个3,此时3^k最大
2)n=3k+1=3(k-1)+2+2,把n分解成k-1个3和2个2,此时乘积3^(k-1)*4最大
3)n=3k+2,把n分解成k个3和1个2,此时乘积3^k*2最大
2的个数不多于2个,这是因为假设有3个2,但6=3+3,2^3<3^2,所以此时分解成2个3乘积更大
一般性的结论为:把自然数n分解成若干数之和,则当每一数接近无理数e时积最大。
因为e=2.71828……更接近3,所以要求3尽量多,2尽量少。
下面给个证明:
由均值不等式知道,把n平分成m份时乘积比非平均分大,所以设把n平均分成x份,乘积为y,
即有y=(n/x)^x
y'=[(n/x)^x]*(lnn-lnx-1)
令y'=0得lnn-lnx-1=0,x=e^(lnn-1)=n/e,所以有每一份为n/x=e .
===================================================================================================================
另外的补充,可能有的概念和上面的说法不一,意会就好!
主要是一些定理,技巧性较高:
归类(转载):
概念:
1.n的划分:把n写成几个自然数和的形式称作n的一个划分
2.n的r划分:把n写成r个自然数和的形式称作n的一个r划分
3.n的划分数:n的不同划分的个数,为了理论上的方便这里的划分包括了这个数自己
4.n的r划分数:n的不同r划分的个数,同上一条
5.n的不可重复划分:把n写成几个不相同的自然数和的形式称作n的一个不可重复划分,这里提到这个是为了说明,n的
划分是允许重复自然数的
定理:
(这些定理一定正确,这里不做证明)
1.n的划分数=2n的n划分数
2.n的2划分数=n/2
3.n的3划分数=(n*n+3)/12
4.n的不超过k部分的划分数=n+k的k划分
5.n的最大部分不超过k的划分数=n+k的k划分
问题:
现在试图用解决对给定的n求n的划分数问题
编程实例:
#include <cstdlib>
#include <iostream>
using namespace std;
int Split(int nNum, int max)//nNum不超过max的可重复划分数 直接递归算法
{
if(max<=0)return 0;
if(max==1)return 1;
if(nNum==1)return 1;
if(nNum==0)return 1;
int sum=0;
for(int i=1;i<=max&&i<=nNum;i++)
{
sum+=Split(nNum-i,i);
}
return sum;
}
int p1(int r,int n)//n的r划分数 递推公式 1
{
if(r>n)return 0;
if(r==1)return 1;
if(r==n)return 1;
if(r==2)return n/2;
if(r==3)return (n*n+3)/12;
int sum=0;
for(int i=1;i<=n/r;i++)
{
sum+=p1(r-1,n-r*i+r-1);
}
return sum;
}
int p2(int r,int n)//n的r划分数 递推公式2
{
if(r>n)return 0;
if(r==1)return 1;
if(r==n)return 1;
if(r==2)return n/2;
if(r==3)return (n*n+3)/12;
int sum=0;
for(int i=1;i<=r;i++)
{
sum+=p2(i,n-r);
}
return sum;
}
int p(int n)//n的划分数
{
int a[10000];
for(int i=0;i<n;i++)a[i]=0;
a[0]=1;
a[1]=1;
a[2]=2;
if(n<0)return 0;
for(int i=3;i<=n;i++)
{
int sum=0,sign=-1;
for(int k=1;(3*k*k-k)/2<=i;k++)
{
sign=-sign;
if(i-(3*k*k+k)/2>=0)sum+=sign*(a[i-(3*k*k-k)/2]+a[i-(3*k*k+k)/2]);
else sum+=sign*a[i-(3*k*k-k)/2];
//printf("sum=%d\n",sum);
}
a[i]=sum;
//printf("a[%d]=%d\n",i,a[i]);
}
return a[n];
}
int main(int argc, char *argv[])
{
cout<<Split(6,6)<<endl;
cout<<p(6)<<endl;
cout<<p1(2,6)<<endl;
cout<<p2(2,6)<<endl;
system("pause");
return 0;
}
扩展思想:http://hi.baidu.com/hundeng/blog/item/f0c2eccaf37f8747f21fe78c.html
用f (a,b)表示把b做任意份剖分,其中最大的一个部分等于a的方案总数,用g(a,b)表示把b做任意份划分,其中最大的
一个部分不大于a的方案总数,则有:
f (a,b)=g (a,b-a);
g(a,b)=f(1,b)+f(2,b)+...f(a,b);
因为:
f(1,b)+f(2,b)+...f(a,b) =f(1,b)+...f(i,b) +f(a,b) (1<=i<=a-1)
f(1,b)+f(2,b)+..f(a-1,b) =g(a-1,b)
所以:
g(a,b)=f(1,b)+...f(i,b)+f(a,b)=g(a-1,b)+g(a,b-a)(1<=i<=a-1)
当b<a时,根据g(a,b)的含义,g(a,b-a)无意义。
当a=1时,显然 g(1,b)=1.
于是,根据新模型求解得到下列递推公式:
g (a,b)=g (a-1,b) b<a
g (a-1,b)+g(a,b-a) b>=a.
g(1,b)=1.
最后的g (k,n-k)即为所求。
问题延伸:对整数n划分m份,分别输出这m份的各个组合(转载csdn上的高手的程序,思路极其巧妙,编程技巧极高)
#include<stdio.h>
#include<stdlib.h>
int n; //你表示要拆分的元素个数
//M是要拆分的数,N是要拆分的元素个数,tmp[]用于存放元素
//divide函数每运行一次填充一个tmp[]位置,存放的时候为了方便,从最后一个开始存放,当存放到tmp[1]时打印所
有的分解值
//tmp【0】中存放的是每次分解的最小基数,tmp【0】要及时更新。同时分解的序列的值是逐步增加的,和if(M-i<i)
一起保证不会发生有重叠的发生
void divide(int M, int N, int* tmp)
{
int i;
if(1 == N)
{
tmp[N] = M;
for(i=n; i>0; i--)
{
printf("%d ",tmp[i]);
}
printf("\n");
return;
}
else
{
for(i = tmp[0]; i<=M-N+1; i++)
{
if(M-i<i) return;
tmp[N]=i;
tmp[0]=tmp[N];
divide(M-i,N-1,tmp);
}
}
}
int main()
{
int M;
int *tmp;
printf("input M n:");
scanf("%d",&M);
scanf("%d",&n);
if (n<=0) return 0;
tmp = (int*)malloc(sizeof(int)*(n+1));//tmp[1]--tmp[n]存放分解后的值
tmp[0] = 1; //分解的最小基数
divide(M,n,tmp);
system("pause");
return 0;
}
总结:为什么要从最后一个存放:由于递归,每次都有一个参数N,适合把数存好,省去了从前到后下标计算的麻烦。
为什么设置N[0]为最小基数:1 是小表从后向前对应存储方便,所以设置了一个0下标而多设置了一个空间
。
2 基下标存储基数每次都动态变化,目的是以后进行的数据
存储都在基数之上,保
证了递增,从而避免了重复现象。