前面讨论的“石子合并”问题也属于划分问题,同时前面还讨论了“前10个阶乘所能构成和为n的方法数”这样的问题,已经基本属于整数划分的问题了,本小节,根据碰到的一些整数划分问题给出算法分析与解决。
一.基本划分问题
问题描述:给定一个正整数n,求将其划分为m份的不同方法数。
问题分析:
(1) 首先,何谓不同方法总数?即两个不同的划分的元素不能完全相同,eg:10 = 1+2+3+4,是10的一个划分,那么10 = 2+1+3+4也应该是10的一个划分,但是由于这两个划分的元素完全相同,所以实际只能算同一种划分。
(2) 搞清楚不同方法数之后,那么如果我们在划分时只要按照不下降排列的方式进行划分就不会有重复,这样进行下去就可以得到最后的方法总数
算法设计:
(1) 首先看原始的动态规划算法设计:考虑划分过程中的最大数 k,令f[i][j][k]表示将整数j划分成 i 份,而最后一个数最大为 k的划分方案总数。那么可见:该状态(i, j, k)可由前面“i-1”份的“j-k”加上“一份k”而来,因此当前状态的方案总数就等于“j-k”划分为i-1份的所有最大加数小于等于k的方案数至和。得到状态转移方程:f[i][j][k] = SUM{ f[i-1][j-k][l] | 0 <= l <= k }
DP算法1: O(n^3*m)
Set all f[i][j][k] = 0;
f[0][0][0] = 1;
for(i = 1; i <= m; i++)
for(j = i; j <= n; j++)
for(k = 1; k <= j; k++)
for(l = 0; l <= k; l++)
f[i][j][k] += f[i-1][j-k][l];
for(i = 1, cnt = 0; i <= n; i++) cnt += f[m][n][i];
return cnt;
可见,这是一个O(n^3*m)的算法,仅适合规模小的情况
(2) 改进这个规划算法,减少第3维的状态,i,j的状态含义有改变,它们此时的含义就是:将整数i划分为j份的方案数。根据类似于“堆积木”的思维,当前状态实际上就是现在最下一行各摆上“1”块积木接下来就是把“i-j”块积木放上去并保持阶梯状,实际就是“i-j”拆分成“0~k(k <= j)”份的方案总数之和,所以有:f[i][j] = SUM{ f[i-j][k] | 0 <= k <= j}
for (i = 1; i <= n; i++)
for (j = 1; j <= (i > m ? m : i); j++)
for (k = 0; k <= j; k++)
f[i][j] += f[i-j][k]
return f[n][m];
可见,这是一个O(n*m^2)的算法,仍然不怎么适用于大规模的n, m
(3) 是否可以继续化简呢?可以从方程入手,首先我们要有一个概念,那就是:如果DP中要用到对前面状态求和,那么要么可以O(n^2)预处理sum[i][j],要么就可以简化DP方程,使得(i, j)仅仅与(i-1, j)或者(i, j-1) 有关。这里我们从方程入手:f[i-1][j-1] = SUM{ f[i-1-j+1][k] | 0<=k<=j-1}
= SUM { f[i-j][k] | 0<=k<=j-1 }
观察下标k的变化,方程可简化为:f[i][j] = f[i-1][j-1] + f[i-j][j] --- (*)
for(i = 1; i <= n; i++)
for(j = 1; j <= (i > m ? m : i);j++)
f[i][j] = f[i-1][j-1] + f[i-j][j];
return f[n][m];
可见,时间复杂度降为Min{O(n^2), O(nm)}
小节:至此,该基本问题划分算法的动态规划算法设计完成,2次方的时间复杂度基本可以满足较大规模的计算,不过计算的时候要注意,随着n, m增大,f值增加的非常之快,所以大规模时,f选用double才是正确的。最后,如果想输出所有可能的划分方案,上面规划算法所记录的信息太少了。更为变态的是,方案数随着n, m成指数增长,可以说巨大无比,对于大规模n,m时输出不要太多。
二.一些变形的划分问题
1.划分为不重复出现的元素的方案数。问题简述:N个物体,价值分别为P[i],求组成价值为S的方案总数,这里就是一个不能重复使用的类似于背包的问题,本质上也就是我前面那个帖子中“前10个阶乘所能构成和为n的方法数”问题的扩展。具体以:http://acm.zju.edu.cn的1163题为例简单的说一下:将数字N分成2份以上.使用的数字不可重复.例如5 = 1+4 = 2+3,就只有两种拆分的方式。请输出拆分的种数。
分析设计:首先,这里N个物体非常特殊了,也就简单不少,价值就是自己序号本身。然后,由于这里所求划分并非是单一限定了多少份,而是整个划分数的方案总数,所以如果采用第一部分的分析来做,则至少需要O(n^3),而且还需要考虑不能重复,设计起来比较麻烦了。所以我们这里需要另外挑选状态了,类似于“阶乘”中的处理方法,令f[i]表示组成和为i的方案总数,那么f[i+j]就可以由所有的f[j]加上i而来,因此很简单的得到递推公式:
f[i+j] += f[j],0 <= j <= MAXN – i;仍然需要注意的是,由于该递推式是前面决定后面的,所以递推时倒过来即可:
f[0] = f[1] = 1; //组成0, 1的方法只有 1种
for (i = 2; i <= MAXN; i++) // 递推
for (j = MAXN – i; j >= 0; j--)
f[i+j] += f[j];
实际上,对于此类非重复组合问题,实际上就是装背包问题,只不过这里不是限制背包容量去求最优价值,而是倒过来使用某些物体去装背包,使得背包的容量为特定值。这样形式的问题,采用如上的方法再好不过了,实际上可能这种方法不应该叫规划算法了,应该叫做“生成函数”,即主动由一个状态去生成它所有可以生成的状态(注意保证不重复)。不过我还是习惯叫他DP,觉得还是与状态有关。
2.变形的整数划分缩减版:
以http://acm.zju.edu.cn的1738为例,这是一个四完全平方数理论。
问题描述:四完全平方数理论理论:任何一个正数都能表示为至多四个完全平方数之和。现希望你能写出对于任一个正数,它有多少种这样的表示方式,eg:
25 = 1+4+4+16= 9+16= 25
共3种。也是不考虑顺序关系,但是可以重复使用。
算法设计:当时做这题时,没有怎么研究整数划分算法,所以一看到“至多4个完全平方数”,立马就想到了“至多4重循环”枚举建表,再对应输出即可。还作了些优化:例如建立好200以内的平方表,枚举i,j,k,l时剪掉超过范围的,最后也可以在0.5 S内解决掉。但是实际上有效的解决这个问题的方法仍然是使用动态规划算法(也是主动生成法)。
(1) 首先看状态:ans[i][k]表示整数 i可以表示成k个完全平方数形式的方案总数,那么先对 i = 0,给出特殊的边界条件:ans[0][1] = ans[0][2] = ans[0][3] = ans[0][4] = 1;即0总是可以表示成1个,2个,3个,4个 0^2 之和。
(2) 其次,进行递推填充:
A. 先填充 k = 1:ans[i*i][1]++;即所有的 i*i为构成k = 1的情况
B. 对于k > 1:ans[j+i*i][k+1] += ans[j][k],0 <= j < N - i*i; 1 <= k < 4;含义就是数j可以表示成k个完全平方数方案总数,那么j再加上i*i。则表示数j+i*i的k+1个完全平方数方案总数的一部分---这意义是很明显的
预先计算好ans[i][k]数组之后,那么对于每一次的输入n,输出ans[n][k]即可。可见,这题仍然是由已有状态去生成可以生成的状态。因此可以简单的小结为一个规律:对于求划分方案数的问题,可以首先考虑生成。其实前面的基本问题所推的算法也是基于生成思想的.
3.求划分因子乘积最大的一个划分及此乘积
问题简述:给定一个正整数n, 则在n所有的划分中, 求因子乘积最大的一个划分及此乘积。例如:8 = {8}, {7, 1}, {6, 2}, {5, 3}, {4, 4}, {3, 3, 2}, {2, 2, 2, 2} 等,那么在这些当中,3 * 3 * 2 的乘积最大,所以输出整个划分 和 这个乘积 18。
算法分析:这是我在某个论坛上看到的问题,以及别人针对此问题的数学分析,现简单的整理如下:
(1)对于任意大于等于4的正整数m, 存在一个划分m = m1+m2, 使 m1*m2 >= m证: 令m1 = int(m/2), 则 m1 >= 2
m2 = m-m1; 那么m2 > 2,并且 m2 >= m/2 >= m1;
m1*m2 >= 2*m2 >= m; 证毕;
该证明简单的来说就是:对于一个大于等于4的正整数m,存在一个2块划分的因子,这两个因子的乘积总是不小于原数m本身。
(2)由(1)知此数最终可以分解为 2^r * 3^s。现证明 r <= 2;
证:若r > 2, 则至少有3个因子为2, 而2*2*2 < 3*3;
所以可以将3个为2的因子,换为两个因子3;积更大;证毕。
综合(1),(2),则有:任何大于4的因子都可以有更好的分解, 而4可以分解为2*2。 所以:此数应该分解为 2^k1 * 3^k2。而且可以证明 k1>=0 并且 k1 <= 2,因此:
A.当n = 3*r 时, 分解为 3^r
B.当n = 3*r+1时, 分解为 3^(r-1)*2*2
C.当n = 3*r+2时, 分解为 3^r*2
剩下编程处理,那就是太简单了,首先是处理 <= 4的特殊情况,再对>4的情况进行模3的3种情况的判断,最后一一输出。可见,数学在整数划分问题上有太强的功能。谁叫这个问题叫整数划分呢,不与数学密切才怪! ^_^。