整数划分问题

整数划分问题是算法中的一个经典命题之一,有关这个问题的讲述在讲解到递归时基本都将涉及。所谓整数划分,是指把一个正整数 n 写成如下形式:
n=m1+m2+...+mi; (其中 mi 为正整数,并且 1 <= mi <= mi-1 <= … <= m1 <= 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)
       因此我们可以给出求出 f(n, m) 的递归函数代码如下(引用 Copyright Ching-Kuang Shene July/23/1989 的代码):

 

unsigned  long   GetPartitionCount( int  n,  int  max)
{
    
if  (n ==  1  || max ==  1 )
        
return  1 ;
    
else  if  (n < max)
        
return  compute(n, n);
    
else  if  (n == max)
        
return  1  + GetPartitionCount(n, max- 1 );
    
else
        
return  GetPartitionCount(n,max- 1 ) + GetPartitionCount(n-max, max);
}
我们可以发现,这个命 题的特征和另一个递归命题:
“上台阶”问题(斐波 那契数列)
相似,也就是说,由于树的“天然递归性”,使这类问题的解可以通过树来展现,每一个叶子节点的路径是一个解。因此把上面的函数改造一下,让所 有划分装配到一个 .NET 类库中的 TreeView 控件,相关代码( c# )如下:

 

         ///  <param name="root"> 树的根结点 </param>
        
///  <param name="n"> 被划分的整数 </param>
        
///  <param name="max"> 一个划分中的最大数 </param>
        
///  <returns> 返回划分数,即叶子节点数 </returns>
         private  int  BuildPartitionTree(TreeNode root,  int  n,  int  max)
        {
            
int  count= 0 ;
            
if ( n== 1 )
            {
                
//{n} 1 n
                root.Nodes.Add(n.ToString()); //{n}
                 return  1 ;
            }
            
else  if ( max== 1 )
            {
                
//{1,1,1,…,1}  n 1
                TreeNode lastNode=root;
                
for ( int  j= 0 ;j<n;j++)
                {
                    lastNode.Nodes.Add(
"1" );
                    lastNode=lastNode.LastNode;
                }
                
return  1 ;
            }
            
else  if (n<max)
            {
                
return  BuildPartitionTree(root, n, n);
            }
            
else  if (n==max)
            {
                root.Nodes.Add(n.ToString()); 
//{n}
                count=BuildPartitionTree(root, n, max- 1 );
                
return  count+ 1 ;
            }
            
else
            {
                
// 包含 max 的分割, {max, {n-max}}
                TreeNode node= new  TreeNode(max.ToString());
                root.Nodes.Add(node);
                count += BuildPartitionTree(node, n-max, max);

                
// 不包含 max 的分割,即所有 max-1 分割
                count += BuildPartitionTree(root, n, max- 1 );
                
return  count;
            }
        }
如果我们要输出所有 解,只需要输出所有叶子节点的路径即可,可以同样用递归函数来输出所有叶子节点(代码中使用了一个 StringBuilder 对象来接收所有叶子节点的路径):      

 

         private  void  PrintAllLeafPaths(TreeNode node)
        {
            
// 属于叶子节点 ?
             if (node.Nodes.Count== 0 )
                
this .m_AllPartitions.AppendFormat( "{0}/r/n" , node.FullPath.Replace( '//' , ',' ));
            
else
            {
                
foreach (TreeNode child  in  node.Nodes)
                {
                    
this .PrintAllLeafPaths(child);
                }
            }
        }
这个小例子的运行效果如下(源代码 都在文中,就不提供下载链接):
        整数划分问题 - 夜乡晨 - 夜乡晨
通过递归思路,我们给出了 n 的划分个数的算法,也把所有划分组装到一棵树中。好,关于递归思路我们就暂时介绍到这里。关于输出所有 划分的标准代码在这里省略了,我们有时间再做详细分析。
 
        ---------------------------------------------------------------------------------
                                                 (二)母函数法
        ---------------------------------------------------------------------------------
 
下面我们从另一个角度即“母函数”的角度来考虑这个问题。
所谓母函数,即为关于 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;
则关于多项式乘法的代码如下,其中数组 a 和数组 b 表示两个要相乘的多项式,结果存储到数组 c

 

#define  N 130
unsigned 
long  a[N]; /* 多项式 a 的系数数组 */
unsigned 
long  b[N]; /* 多项式 b 的系数数组 */
unsigned 
long  c[N]; /* 存储多项式 a*b 的结果 */

/* 两个多项式进行乘法,系数分别在 a b 中,结果保存到 ,项最大次数到 N */
/* 注意这里我们只需要计算到前 N 项就够了。 */
void  Poly()
{
    
int  i,j;
    memset(c,
0 , sizeof (c));
    
for (i= 0 ; i<N; i++)
            
for (j= 0 ; j<N-i; j++)  /*y<N-i   确保 i+j 不会越界 */
                  c[i+j] += a[i]*b[j];
}
         下面我们求出 G x )的展开结果, G x )是 n 个多项式连乘的结果:

 

/* 计算出前 N 项系数!即 g(x,1) g(x,2)... g(x,n) 的展开结果 */
void  Init()
{
    
int  i,k;
    memset(a,
0 , sizeof (a));
    memset(c,
0 , sizeof (c));
    
for (i= 0 ;i<N;i++) a[i]= 1 /* 第一个多项式: g(x, 1) = x^0 + x^1 + x^2 + x^3 + … */
    
for (k= 2 ;k<N;k++)
    {
        memset(b,
0 , sizeof (b));
        
for (i= 0 ;i<N;i+=k) b[i]= 1 ; /* k 个多项式: g(x, k) = x^0 + x^(k) + x^(2k) + x^(3k) +…*/
        Poly(); 
/*  多项式乘法: c= a*b */
        memcpy(a,c,
sizeof (c));  /* 把相乘的结果从 c 复制到 a 中: c=a; */
    }
}
通过以上的代码,我们就计算出了 G x )的展开后的结果,保存到数组 c 中。此时有: f(n,n)=c[n]; 剩 下的工作只是把相应的数组元素输出即可。
问题到了这里已经解决完毕。但我们发现,针对该问题, g(x,k) 是一个比较特殊的多项式,特点是只有 k 的整数倍的索引位置有项,而其他位置都为 0 ,具有项 稀疏 的特点,并且项次分 布均匀(次数跨度为 k )。这样我们就可以考虑在计算多项式乘法时,可以减少一些循环。因此可以对 Poly 函数做这样的一 个改进,即把 k 作为参数传递给 Poly

 

/* 两个多项式进行乘法,系数分别在 a b 中,结果保存到 ,项最大次数到 N */
/* 改进后,多项式 a 乘以一个有特殊规律的多项式 b ,即 b 中只含有 x^(k*i) 项, i=0,1,2,…*/
/* 如果 b 没 有规律,只需要把 k 设为 1 , 即与原来函数等效 */
void  Poly2( int  k)  /* 参数 k 的含义:表示 b 中只有 b[k*i] 不为 0 */
{
    
int  i,j;
    memset(c,
0 , sizeof (c));
    
for (i= 0 ; i<N; i++)
            
for (j= 0 ; j<N-i; j+=k)
                  c[i+j] += a[i]*b[j];
}
这样,原有的函数可以认为是 k=1 的情况(即多项式 b 不具有上诉规律)。相应的,在上面的 Init 函数中的调用改为 Poly2(k) 即可。
 
---------------------------------------------------------------------------------
参考资料:
1 )关于 递归 部分的代码,参考了 Ching-Kuang Shene July/23/1989 的代码;
2 )关于 母函数 部分,参考了《 Acm 程序设计》(刘 春英)( PPT 文档);
3 母函数 方法的 Init Poly 的代码,参考 了某位教师的代码   : ymc 2008/09/25 其中多项式乘法的改 进是我提出的建议。

 

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值