7215:简单的整数划分问题

题目链接:http://bailian.openjudge.cn/practice/4117/

总时间限制: 100ms 内存限制: 65536kB
描述
将正整数n 表示成一系列正整数之和,n=n1+n2+…+nk, 其中n1>=n2>=…>=nk>=1 ,k>=1 。
正整数n 的这种表示称为正整数n 的划分。正整数n 的不同的划分个数称为正整数n 的划分数。

输入
标准的输入包含若干组测试数据。每组测试数据是一个整数N(0 < N <= 50)。
输出
对于每组测试数据,输出N的划分数。
样例输入
5
样例输出
7
提示
5, 4+1, 3+2, 3+1+1, 2+2+1, 2+1+1+1, 1+1+1+1+1

 

注意对比题目:

(1)数的划分(把n分为k份,有点类似于放苹果,但放苹果这道题目里面盘子可以为空,它不允许为空。)

(2)复杂的整数划分 

 

算法(一)递归法

把一个正整数n写成如下形式: 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和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)

 1 #include <stdio.h>
 2 long long GetPartitionCount(int n,int max)
 3 {
 4     if(n==1||max==1) return 1;
 5     else if(n<max) return GetPartitionCount(n,n);
 6     else if(n==max) return 1+GetPartitionCount(n,max-1);
 7     else return GetPartitionCount(n,max-1)+GetPartitionCount(n-max,max);
 8 }
 9 int main(int argc, char *argv[])
10 {
11     int n;
12     while(scanf("%d",&n)!=EOF)
13     {
14         printf("%lld\n",GetPartitionCount(n,n));
15     }
16     return 0;
17 }

 

算法(二)动态规划

这道题就是整数的划分,其实定义好了状态,就是简单的动态规划的递推。

我们定义dp[n][k]表示将n进行划分,最大的数不超过k的方案有多少种,那么我们可以得到如下的递推方案:

dp[n][k] = dp[n][k-1] + dp[n-k][k];

其中的dp[n][k-1]便是将n进行进行整数的划分,最大的数不超过k-1的方案数;dp[n-k][k]表示拿出一个k后,剩下的数被不超过k的数的表示的方案数。

其中当k>n的时候,则和dp[n][n]的值相同,下面通过递推的方式求出所有的解,然后对应的输入n,输出dp[n][n]就行了。 

 1 #include<iostream>
 2 #include<cstring>
 3 #include<cstdlib>
 4 #include<cstdio>
 5 using namespace std;
 6 
 7 const int MAX = 122;  
 8 int dp[MAX][MAX];
 9 
10 void Dynamic()
11 {
12     for(int i=1; i<MAX; i++)
13     {
14         dp[i][1] = dp[1][i] = dp[0][i] = 1;
15     }
16     for(int i=2; i<MAX; i++)
17     {
18         for(int j=2; j<MAX; j++)
19         {
20             if(j<=i) dp[i][j] = dp[i][j-1] + dp[i-j][j];
21             else dp[i][j] = dp[i][i];
22         }
23     }
24 }  
25 int main()  
26 {  
27     int n;
28     Dynamic();
29     while(scanf("%d",&n)!=EOF)
30     {
31         printf("%d\n",dp[n][n]);
32     }
33     return 0;  
34 }

 这道题的动规思路,下面的论述比较清晰一些:

 

整数划分问题 
数 n 的划分是将 n 表示成多个正整数之和的形式,划分可以分为两种情况:
第一种情况:划分的多个正整数中,正整数的数量是任意的。

这又可以分为划分的正整数中,正整数可以相同与不同两类

 1.  划分的多个正整数可以相同, 递推方程可以表示为:
     (1)   dp[n][m]= dp[n][m-1]+ dp[n-m][m]
           分析dp[n][m]表示整数 n 的划分中,每个数不大于 m 的划分数。则划分数可以分为以下两种情况:
           a. 划分中每个数都小于 m, 相当于每个数不大于 m- 1, 故划分数为 dp[n][m-1]. 
           b. 划分中有至少一个数为 m. 那就在 n中减去 m , 剩下的就相当于把 n-m 进行划分, 故划分数为 dp[n-m][m];
      (2)   dp[n][m]= dp[n][m+1]+ dp[n-m][m]
            其中dp[n][m]表示整数 n 的划分中,每个数不小于 m 的划分数。同理可证明该式。
 
 2.  划分的多个正整数互不相同,递推方程可以表示为:
     (1)    dp[n][m]= dp[n][m-1]+ dp[n-m][m-1]
            分析: dp[n][m]表示整数 n 的划分中,每个数不大于 m 的划分数。同样划分情况分为以下两种情况:
             a.  划分中每个数都小于 m, 相当于每个数不大于 m- 1,划分数为 dp[n][m-1].
             b.  划分中有一个数为 m. 在 n 中减去 m, 剩下相当对n- m 进行划分,并且每一个数不大于 m- 1,故划分数为 dp[n-m][m-1]
     (2)    dp[n][m]= dp[n][m+1]+ dp[n-m][m]
             其中dp[n][m]表示整数 n 的划分中,每个数不小于 m 的划分数。

第二种情况:划分的多个正整数中,正整数的数量是固定的
   把一个整数 n 无序划分成 k 份互不相同的正整数之和的方法总数。   方程为: dp[n][k]= dp[n-k][k]+ dp[n-1][k-1];
   证明方法参考: http://www.mydrs.org/program/html/0369.htm
   另一种理解,总方法可以分为两类:
   第一类: n 份中不包含 1 的分法,为保证每份都 >= 2,可以先拿出 k 个 1 分
   到每一份,然后再把剩下的 n- k 分成 k 份即可,分法有: dp[n-k][k]
   第二类: n 份中至少有一份为 1 的分法,可以先那出一个 1 作为单独的1份,剩
   下的 n- 1 再分成 k- 1 份即可,分法有:dp[n-1][k-1]

类似问题:

M个小球装N个盒子,或者苹果装盘问题。比如:把M个球放到N个盒子,允许有空的盒子(不放球),有多少种放法?这些都属于典型的DP问题。

用F(m,n)表示有多少种放法:
    如果m=0 或者 m=1 , F = 1
    如果n=0 或者 n=1   , F =1
    既F(0,0) = F(0,1) = F(1,0) = F(1,1) = 1
    否则 F = F(m-n,n) + F(m,n-1)这就是DP的解空间递归解

 

 

关于整数的质因子和分解

【问题描述】
歌德巴赫猜想说任何一个不小于6的偶数都可以分解为两个奇素数之和。对此问题扩展,如果一个整数能够表示成两个或多个素数之和,则得到一个素数和分解式。对于一个给定的整数,输出所有这种素数和分解式。注意,对于同构的分解只输出一次(比如5只有一个分解2 + 3,而3 + 2是2 + 3的同构分解式)。

例如,对于整数8,可以作为如下三种分解:
(1) 8 = 2 + 2 + 2 + 2
(2) 8 = 2 + 3 + 3
(3) 8 = 3 + 5

【算法分析】
由于要将指定整数N分解为素数之和,则首先需要计算出该整数N内的所有素数,然后递归求解所有素数和分解即可。

原作者的C++代码:(感觉其实就是回溯的思路)

 1 #include <iostream>   
 2 #include <vector>   
 3 #include <iterator>   
 4 #include <cmath>   
 5 using namespace std;   
 6   
 7 // 计算num内的所有素数(不包括num)   
 8 void CalcPrimes(int num, vector<int> &primes)   
 9 {   
10     primes.clear();   
11     if (num <= 2)   
12         return;   
13        
14     primes.push_back(2);   
15     for (int i = 3; i < num; i += 2) 
16     {   
17         int root = int(sqrt(i));   
18         int j = 2;   
19         for (j = 2; j <= root; ++j)
20         {   
21             if (i % j == 0)   
22                 break;   
23         }   
24         if (j > root)   
25             primes.push_back(i);   
26     }   
27 }   
28   
29 // 输出所有素数组合(递归实现)   
30 int PrintCombinations(int num, const vector<int> &primes, int from, vector<int> &numbers)   
31 {   
32     if (num == 0) 
33     {   
34         cout << "Found: ";   
35         copy(numbers.begin(), numbers.end(), ostream_iterator<int>(cout, " "));   
36         cout << '\n';   
37         return 1;   
38     }   
39        
40     int count = 0;   
41        
42     // 从第from个素数搜索,从而避免输出同构的多个组合   
43     int primesNum = primes.size();   
44     for (int i = from; i < primesNum; ++i)
45     {   
46         if (num < primes[i])   
47             break;   
48         numbers.push_back(primes[i]);   
49         count += PrintCombinations(num - primes[i], primes, i, numbers);   
50         numbers.pop_back();   
51     }   
52        
53     return count;   
54 }   
55   
56 // 计算num的所有素数和分解   
57 int ExpandedGoldbach(int num)   
58 {   
59     if (num <= 3)   
60         return 0;   
61        
62     vector<int> primes;   
63     CalcPrimes(num, primes);   
64        
65     vector<int> numbers;   
66     return PrintCombinations(num, primes, 0, numbers);   
67 }   
68   
69 int main()   
70 {   
71     for (int i = 1; i <= 20; ++i)
72     {   
73         cout << "When i = " << i << ":\n";   
74         int count = ExpandedGoldbach(i);   
75         cout << "Total: " << count << "\n\n";   
76     }   
77 }
View Code

C语言代码如下:

 1 #include<stdio.h>
 2 #include<math.h>
 3 #include<string.h>
 4 
 5 #define maxNum 1000
 6 int primeNum[maxNum]={0},ansArr[maxNum]={0};
 7 int indexForPrimeNum,indexForAnsArr;
 8 
 9 int work(int num);//输出num的所有素数和分解的方案并返回其总方案数
10 int CalcPrimes(int num);//计算num内的所有素数(不包括num),结果保存在primeNum[]. 
11 
12 int PrintAns(int num,int from);
13 //回溯法的思想:从primeNum[]的第from个元素开始选择元素来累加构造num。
14 //构造结果放在ansArr[]中。寻找到一个构造方案后输出该方案.
15 //最终返回总的方案数
16 
17 int main(int argc, char *argv[])
18 {
19     for (int i = 1; i <= 20; ++i)
20     {   
21         printf("When i = %d:\n",i);   
22         int count = work(i);   
23         printf("Total: %d\n\n",count);   
24     }
25     return 0;
26 }
27 //计算num内的所有素数(不包括num),结果保存在primeNum[]. 返回数组元素个数 
28 int CalcPrimes(int num)
29 {
30     int i,j,root,k=0;
31     
32     if(num<2) return k;
33     memset(primeNum,0,sizeof(primeNum));
34     
35     primeNum[k++]=2;
36     for(i=3;i<num;i++)
37     {
38         root=sqrt(i);
39         for(j=2;j<=root;j++)
40         {
41             if(i%j==0) break;
42         }
43         if(j>root) primeNum[k++]=i;
44     }
45     return k;
46 }
47 //调用PrintAns()输出num的所有素数和分解的方案,返回其总方案数 
48 int work(int num)
49 {
50     if(num<=3) return 0;
51     
52     indexForPrimeNum=CalcPrimes(num);
53     
54     memset(ansArr,0,sizeof(ansArr));
55     indexForAnsArr=0;
56     return PrintAns(num,0);
57 }
58 
59 //回溯法的思想:从primeNum[]的第from个元素开始选择元素来累加构造num。
60 //构造结果放在ansArr[]中。寻找到一个构造方案后输出该方案.
61 //最终返回总的方案数
62 int PrintAns(int num,int from)
63 {
64     int i;
65     if(num==0)
66     {
67         printf("Found: ");
68         for(i=0;i<indexForAnsArr;i++) printf(" %d",ansArr[i]);
69         printf("\n");
70         return 1;
71     }
72     
73     int totalCount = 0;
74     //从第from个素数搜索,从而避免输出同构的多个组合
75     for(i=from;i<indexForPrimeNum;i++)
76     {
77         if(num<primeNum[i]) break;
78         ansArr[indexForAnsArr++]=primeNum[i];
79         totalCount+=PrintAns(num-primeNum[i],i);
80         indexForAnsArr--;    
81     }
82     return totalCount;
83 }
View Code

 

 

参考:

http://www.cppblog.com/superKiki/archive/2010/05/27/116506.html

http://blog.csdn.net/geniusluzh/article/details/8118683

 

posted on 2017-08-06 17:18  华山青竹  阅读(4011)  评论(0编辑  收藏  举报

导航