整数划分系列问题(动态规划)
2015-09-15 17:45
399 查看
今天上算法分析与设计课时,提到整数划分问题,但是因为之前没有很好地理解这一系列的问题。当被老师问到你们做算法的应该会这个问题吧,我当时也不太记得,但是只能硬头皮试着去写了下,用的是brute force,老师果断说我的方法跑不出正确的解,但是课堂上我也没有去解释了。课后自己验证了一下,没有问题,但是看了一下更好的动态规划的解法,顺便把几种整数划分的类型都理解清楚。
原题:一个整数划分为多个数的和,有几种划分的组合。
加了记忆化后,n稍微大点还是跑不出来。
但是学会整数的划分动归求解,就可以在O(n*n)的时间复杂度内求解!!
下面进入正题。
整数划分问题变型比较多,但是有句话叫做万变不离其宗,这是看了两个贴吧大神(飞机)的讲解后领悟的,所以索性5个整数划分的问题全部列出来,自己也整理一下。
一: 将n划分成若干正整数之和的划分数。
二: 将n划分成k个正整数之和的划分数。
三: 将n划分成最大数不超过k的划分数。
四: 将n划分成若干奇正整数之和的划分数。
五: 将n划分成若干不同整数之和的划分数。
题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=571
引用一下9楼的解释,结合7楼的代码。
先做一个定义:
定义:dp(i, j)表示整数i被划分为j个数的划分数。
那么有dp(i, j) = dp(i - j, j) + dp(i - 1, j - 1)
上述划分数可以分解为两类情况:
1、分解的数含1, 即dp(i-1, j-1);
2、分解的数不含1, 那么先将分成的j个数减1,然后得到i-j,划分为j个数。即dp(i - j, j)。
问题二:将n划分成k个正整数之和的划分数 解决, 程序为:
问题三:将n划分成最大数不超过k的划分数。
即可以转化为:∑ dp(n, i) (1<=i<=k)
答案是有的
但是需要换一个思路:dp2[i][j]数组的定义为i划分最大数不超过j的划分数。
那么dp2[i][j] = dp2[i-j][j] + dp2[i][j-1]
dp2[i][j-1]表示划分数所有数都小于j的情况数。
dp2[i-j][j]表示每种情况都含至少一个最大数j, 先保留一个j,剩下的i-j进行划分,最大数仍为j。
比如dp2[9][4],先分一个4,然后dp2[5][4]还可以再分一个4出来,因此划分出来值是可以重复的。
问题一:将n划分成若干正整数之和的划分数。
只要让问题3中的∑ dp(n,
i) (1<=i<=n) 或者dp2
问题五:将n划分成若干不同整数之和的划分数
上面我写了这么一句话:
“dp2[i-j][j]表示每种情况都含至少一个最大数j,
先保留一个j,剩下的i-j进行划分,最大数仍为j。
比如dp2[9][4],先分一个4,然后dp2[5][4]还可以再分一个4出来,因此划分出来值是可以重复的。”
所以要保证不重复,只需要dp2[i-j][j-1]即可。代码这一处改了就不写了。
问题四:将n划分成若干奇正整数之和的划分数。
dp3[i][j] = dp3[i-j][j] + dp[i][j-2],其中j为奇数,发现其实和问题1一样的,只是在循环控制的时候,保证j+=2就可以了,然后讨论一下几个特殊情况,看最后的代码吧。
如果类似由问题1变型为问题5也只需要将dp3[i-j][j]变为dp3[i-j][j-2]即可。
至此,五个问题均得到解决,参考贴吧7楼的代码,发现还是没有得到想要的答案还是按照自己的思路写吧。另外如果觉得循环写不好,记忆化搜索也是一种选择。
附上前面链接的题目的代码,代码写的很普通:
原题:一个整数划分为多个数的和,有几种划分的组合。
// 对num划分,最大的数不超过k int divide(int num, int k) { if(num == 0) { return 1; } int res = 0; for (int i = k; i > 0; i --) { if(num - i >= 0) { res += divide(num - i, i); } } return res; }这个写法就是枚举了,效率肯定十分慢,不加记忆化的递归也快不到哪去。
加了记忆化后,n稍微大点还是跑不出来。
int d[1000][1000]; // 对num划分,最大的数不超过k int divide(int num, int k) { if(num == 0) { return 1; } if(d[num][k] > 0) { return d[num][k]; } int res = 0; for (int i = k; i > 0; i --) { if(num - i >= 0) { d[num-i][i] = divide(num - i, i); res += d[num-i][i]; } } return res; }
但是学会整数的划分动归求解,就可以在O(n*n)的时间复杂度内求解!!
下面进入正题。
整数划分问题变型比较多,但是有句话叫做万变不离其宗,这是看了两个贴吧大神(飞机)的讲解后领悟的,所以索性5个整数划分的问题全部列出来,自己也整理一下。
一: 将n划分成若干正整数之和的划分数。
二: 将n划分成k个正整数之和的划分数。
三: 将n划分成最大数不超过k的划分数。
四: 将n划分成若干奇正整数之和的划分数。
五: 将n划分成若干不同整数之和的划分数。
题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=571
引用一下9楼的解释,结合7楼的代码。
先做一个定义:
定义:dp(i, j)表示整数i被划分为j个数的划分数。
那么有dp(i, j) = dp(i - j, j) + dp(i - 1, j - 1)
上述划分数可以分解为两类情况:
1、分解的数含1, 即dp(i-1, j-1);
2、分解的数不含1, 那么先将分成的j个数减1,然后得到i-j,划分为j个数。即dp(i - j, j)。
问题二:将n划分成k个正整数之和的划分数 解决, 程序为:
void divide2(int n, int k) { for (int i = 1; i <= n; i ++) { for (int j = 1; j <= i && j <= k; j ++) { if(j == 1) dp[i][j] = 1; else dp[i][j] = dp[i-j][j] + dp[i-1][j-1]; } } }
问题三:将n划分成最大数不超过k的划分数。
即可以转化为:∑ dp(n, i) (1<=i<=k)
int divide3(int n, int k) { // after divide2 int res = 0; for (int i = 1; i <= k; i ++) { res += dp [i]; } return res; }但是这么做就需要先求出问题二的dp数组了,有没有其他方法呢?
答案是有的
但是需要换一个思路:dp2[i][j]数组的定义为i划分最大数不超过j的划分数。
那么dp2[i][j] = dp2[i-j][j] + dp2[i][j-1]
dp2[i][j-1]表示划分数所有数都小于j的情况数。
dp2[i-j][j]表示每种情况都含至少一个最大数j, 先保留一个j,剩下的i-j进行划分,最大数仍为j。
比如dp2[9][4],先分一个4,然后dp2[5][4]还可以再分一个4出来,因此划分出来值是可以重复的。
void divide3(int n) { for (int i = 1; i <= n; i ++) { for (int j = 1; j <= n; j ++) { if(i < j) { dp2[i][j] = dp2[i][i]; } if(i == j) { dp2[i][j] = dp2[i][j-1] + 1; // 1即为i自身,结合下面dp式理解。 } if(i > j) { // 意义不同于divide2,不要去对比。 dp2[i][j] = dp2[i-j][j] + dp2[i][j-1]; } } } }
问题一:将n划分成若干正整数之和的划分数。
只要让问题3中的∑ dp(n,
i) (1<=i<=n) 或者dp2
问题五:将n划分成若干不同整数之和的划分数
上面我写了这么一句话:
“dp2[i-j][j]表示每种情况都含至少一个最大数j,
先保留一个j,剩下的i-j进行划分,最大数仍为j。
比如dp2[9][4],先分一个4,然后dp2[5][4]还可以再分一个4出来,因此划分出来值是可以重复的。”
所以要保证不重复,只需要dp2[i-j][j-1]即可。代码这一处改了就不写了。
问题四:将n划分成若干奇正整数之和的划分数。
dp3[i][j] = dp3[i-j][j] + dp[i][j-2],其中j为奇数,发现其实和问题1一样的,只是在循环控制的时候,保证j+=2就可以了,然后讨论一下几个特殊情况,看最后的代码吧。
如果类似由问题1变型为问题5也只需要将dp3[i-j][j]变为dp3[i-j][j-2]即可。
至此,五个问题均得到解决,参考贴吧7楼的代码,发现还是没有得到想要的答案还是按照自己的思路写吧。另外如果觉得循环写不好,记忆化搜索也是一种选择。
附上前面链接的题目的代码,代码写的很普通:
#include <cstring> #include <cstdio> #include <queue> using namespace std; const int maxn = 100; int dp[maxn][maxn]; // 将i划分为最大不超过j的整数 int dp2[maxn][maxn]; // 将i划分为j个整数 int dp3[maxn][maxn]; int dp4[maxn][maxn]; int N, K; // include or not include j void solve13(int n) { for (int i = 0; i <= n; i ++) { for (int j = 1; j <= n; j ++) { if(i < j) { dp[i][j] = dp[i][i]; } if(i == j) { dp[i][j] = 1 + dp[i][j-1]; } if(i > j) { dp[i][j] = dp[i-j][j] + dp[i][j-1]; } } } } // include or not include 1 // 为什么上面不写循环条件 j<=i, 因为i-j < j时dp[i-j][j]不是0!!! void solve2(int n) { for (int i = 1; i <= n; i ++) { for(int j = 1; j <= i; j ++) { if(j == 1) dp2[i][j] = 1; else dp2[i][j] = dp2[i-j][j] + dp2[i-1][j-1]; } } } void solve4(int n) { for (int i = 1; i <= n; i ++) { for(int j = 1; j <= n; j += 2) { if(i > j) { dp3[i][j] = dp3[i-j][j] + dp3[i][j-2]; } if(i == j) { dp3[i][j] = dp3[i][j-2] + 1; } if(i < j) { dp3[i][j] = dp3[i][i-!(i&1)]; } } } } void solve5(int n) { for (int i = 1; i <= n; i ++) { for (int j = 1; j <= n; j ++) { if(i < j) { dp4[i][j] = dp4[i][i]; } if(i == j) { dp4[i][j] = 1 + dp4[i][j-1]; } if(i > j) { dp4[i][j] = dp4[i-j][j-1] + dp4[i][j-1]; } } } } void init() { solve13(50); solve2(50); solve4(50); solve5(50); } int main() { init(); int res1, res2, res3, res4, res5; while (~scanf("%d%d", &N, &K)) { res1 = dp ; res2 = dp2 [K]; res3 = dp [K]; res4 = dp3 [N-!(N&1)]; res5 = dp4 ; printf("%d\n%d\n%d\n%d\n%d\n", res1, res2, res3, res4, res5); } return 0; }
相关文章推荐
- Ray Caster Maze
- UIViewController/旋转
- 新贵(NEWMEN)魔键 KB-835U 爽手有线键盘 29.9元
- SortedDictionary
- 【C++】合并两个排序的链表,要求合并后仍然是有序的
- java命名规则
- 二进制求子集
- Linq入门详解(Linq to Objects)
- ie from表单中 a 内部有input 不会跳转
- 脚本加密与解密
- 了解隐式接口和编译期的多态(Effective C++_41)
- iOS开发之 各种传值总结
- BZOJ 1008 [HNOI2008]越狱 (组合数 简单公式)
- 【Linux探索之旅】第三部分第三课:监视系统活动,滴水不漏
- Android之Animation动画的介绍及用法
- 安卓的学习之路Fragment的创建的几种方式
- eclipse mar 创建rest风格webservice ---->针对cxf(未完善)
- 单例模式(一)
- 黑马程序员---Java基础---函数和数组
- 第57讲:Scala中Dependency Injection实战详解