跟着编程之美学算法——数组分割
对于这个问题,首先按照《编程之美》中的分析对这个问题进行一定的简化。从2n个数中找n个元素,有三种可能:大于Sum/2,小于Sum/2以及等于Sum/2。而大于Sum/2与小于等于Sum/2没区别,故可以只考虑小于等于Sum/2的情况。
动态规划第一步,分析子问题:
这里我们用一个三维数组F[][][]表示子问题,F[i][j][k]表示前i个元素中选取j个元素,使得其和不超过k且最接近k。这个子问题可以根据第i个元素是否选择来进行分析:
如果我们想回溯找到一组合理的分割方式,那么在子问题的求解过程中,就要记录有效的路径,这样我们再用一个三维数组path[][][]来记录。
伪代码如下所示:
1 F[][][] = 0 2 path[][][] = 1 3 4 for i = 1 to 2*N 5 nLimit = min(i, N) 6 for j = 1 to nLimit 7 for k = 1 to sum/2 8 F[i][j][k] = F[i-1][j][k] 9 if k > A[i] && F[i-1][j-1][k-A[i]] + A[i] > F[i][j][k] 10 F[i][j][k] = F[i-1][j-1][k-A[i]] + A[i] 11 path[i][j][k] = 1 12 13 return F[2N][N][sum/2], path[][][]
按照这样思路,该算法的时间复杂度为O(n^2*sum),空间复杂度也为O(n^2*sum)。
一下的分析,是针对上面的算法,进一步优化。优化的方向为降低空间复杂度。
优化的思路借鉴了0-1背包问题的思路,最外层循环是对元素的顺序遍历,这一步可以省略,为了保证每次计算时,问题F[i][j][k]的第i-1步为上次的状态,那么我们选择倒叙遍历变量j,这样可以保证变量i-1状态为上一步的状态。
这样改进之后的伪代码是:
1 F[][] = 0 2 path[][][] = 0 3 4 for i = 1 to 2*N 5 nLimit = max(i, N) 6 for j = nLimit to 1 7 for k = 1 to sum/2 8 if k > A[i] && F[j-1][k-A[i]] + A[i] > F[j][k] 9 F[j][k] = F[j-1][k-A[i]] + A[i] 10 path[i][j][k] = 1 11 12 return F[N][sum/2], path[][][]
改进后,不保存路径的空间复杂度为O(n*sum),时间复杂度不变。
下面是根据记录好的path[][][]回溯可行路径的伪码:
1 F[][] = 0 2 path[][][] = 0 3 4 for i = 1 to 2*N 5 nLimit = min(i, N) 6 for j = nLimit to 1 7 for k = 1 to sum/2 8 if k > A[i] && F[j-1][k-A[i]] + A[i] > F[j][k] 9 F[j][k] = F[j-1][k-A[i]] + A[i] 10 path[i][j][k] = 1 11 12 return F[N][sum/2], path[][][]
最后是我对算法复杂度为O(n*sum)的C++实现:
1 #include <iostream> 2 3 using namespace std; 4 5 int Min(int a, int b) 6 { 7 if(a > b) 8 return b; 9 else 10 return a; 11 } 12 13 int Split_Array(int *array, int nLength) 14 { 15 int sum = 0; 16 for(int i = 0; i < nLength; i++) 17 sum += array[i]; 18 cout<<"sum: "<<sum<<endl; 19 20 int F[nLength/2+1][sum/2+1]; 21 int path[nLength+1][nLength/2+1][sum/2+1]; 22 for(int i = 0; i <= nLength/2; i++) 23 for(int j = 0; j <= sum/2; j++) 24 F[i][j] = 0; 25 for(int i = 0; i <= nLength; i++) 26 for(int j = 0; j <= nLength/2; j++) 27 for(int k = 0; k <= sum/2; k++) 28 path[i][j][k] = 0; 29 30 for(int i = 1; i <= nLength; i++) 31 { 32 int nLimit = Min(nLength/2, i); 33 for(int j = nLimit; j > 0; j--) 34 { 35 for(int k = array[i]; k <= sum/2; k++) 36 { 37 if(F[j][k] < F[j-1][k-array[i]] + array[i]) 38 { 39 F[j][k] = F[j-1][k-array[i]] + array[i]; 40 path[i][j][k] = 1; 41 } 42 } 43 } 44 } 45 int result = F[nLength/2][sum/2]; 46 47 int i = nLength; 48 int j = nLength/2; 49 int k = sum/2; 50 while(i > 0 && j > 0 && k >0) 51 { 52 if(path[i][j][k] == 1) 53 { 54 cout<<array[i]<<" "; 55 k -= array[i]; 56 j--; 57 } 58 i--; 59 } 60 cout<<endl; 61 return result; 62 } 63 64 int main() 65 { 66 int array[10] = {1,5,7,8,9,6,3,11,20,17}; 67 cout<<Split_Array(array, 10)<<endl; 68 return 0; 69 }