《编程之美》——数组分割

问题:
将一个无序、元素个数为2N的正整数数组分割为元素个数为N的两个数组,并使两个子数组的和最接近。

分析与解法:
【解法一】
动态规划的0-1背包问题。将heap[M](M表示从2N中所有可能的M个元素和组成的集合),从下到上(m->1…->N)最终求的完整的heap[N]。要点:可以想成求不大于sum/2的最接近集合,以下为分析:

假设数组A[1..2N]所有元素的和是SUM。模仿动态规划解0-1背包问题的策略,令S(k, i)表示前k个元素中任意i个元素的和的集合。显然:
S(k, 1) = {A[i] | 1<= i <= k}
S(k, k) = {A[1]+A[2]+…+A[k]}
S(k, i) = S(k-1, i) U {A[k] + x | x属于S(k-1, i-1) }
按照这个递推公式来计算,最后找出集合S(2N, N)中与SUM/2最接近的那个和。

代码:

#include<iostream> 
using namespace std;  

//有一个没有排序,元素个数为2N的正整数数组。要求把它分割为元素个数为N的两个数组,并使两个子数组的和最接近。 
int arr[] = {0,1,5,7,8,9,6,3,11,20,17};  
const int N=5;  
const int SUM = 87;  

// 模仿动态规划解0-1背包问题的策略 
int solve1()  
{  
    int i , j , s;  
    int dp[2*N+1][N+1][SUM/2+2];  

    /* 用dp(i,j,c)来表示从前i个元素中取j个、且这j个元素之和不超过c的最佳(大)方案,在这里i>=j,c<=S 状态转移方程: 限第i个物品       不取   dp(i,j,c)=max{dp(i-1,j-1,c-a[i])+a[i],dp(i-1,j,c)} dp(2N,N,SUM/2+1)就是题目的解。 */  
    //初始化 
    memset(dp,0,sizeof(dp));  

    for(i = 1 ; i <= 2*N ; ++i)  
    {  
        for(j = 1 ; j <= min(i,N) ; ++j)  
        {  
            for(s = SUM/2+1 ; s >= arr[i] ; --s)  
            {  
                dp[i][j][s] = max(dp[i-1][j-1][s-arr[i]]+arr[i] , dp[i-1][j][s]);  
            }  
        }  
    }  

    //因为这为最终答案 dp[2*N][N][SUM/2+1]; 
    i=2*N , j=N , s=SUM/2+1;  
    while(i > 0)  
    {  
        if(dp[i][j][s] == dp[i-1][j-1][s-arr[i]]+arr[i])   //判定这个状态是由哪个状态推导出来的 
        {  
            cout<<arr[i]<<" ";    //取中arr[i] 
            j--;  
            s -= arr[i];  
        }     
        i--;  
    }  
    cout<<endl;  
    return dp[2*N][N][SUM/2+1];  
} 

时间复杂度为O(2^N)

【解法二】
由于对两个子数组和最接近的判断不太直观,我们需要对题目进行适当转化。我们知道当一个子数组之和最接近原数组之和sum的一半时,两个子数组之和是最接近的。所以转化后的题目是:从2n个数中选出任意个数,其和尽量接近于给定值sum/2。

这个问题存储的是从前k个数中选取任意个数,且其和为s的取法是否存在dp[k][s]。之所以将选出的数之和放在下标中,而不是作为dp[k]的值,是因为那种做法不满足动态规划的前提——最优化原理,假设我们找到最优解有k个数p1p2…pk(选出的这k个数之和是最接近sum/2的),但最优解的前k-1个数p1p2…pk-1之和可能并不是最接近sum/2的,也就是说可能在访问到pk之前有另一组数q1q2….qk-1其和相比p1p2…pk-1之和会更接近sum/2,即最优解的子问题并不是最优的,所以不满足最优化原理。因此我们需要将dp[k]的值作为下标存储起来,将这个最优问题转化为判定问题,用带动态规划的思想的递推法来解。

外阶段:在前k1个数中进行选择,k1=1,2…2*n。
内阶段:从这k1个数中任 意选出k2个数,k2=1,2…k1。
状态:这k2个数的和为s,s=1,2…sum/2。
决策:决定这k2个数的和有两种决策,一个是这k2个数中包含第k1个数, 另一个是不包含第k1个数。

dp[k][s]表示从前k个数中取任意个数,且这些数之和为s的取法是否存在。

代码:

#include <iostream>
#include <algorithm>

using namespace std;

#define MAXN 101
#define MAXSUM 100000
int A[MAXN];
bool dp[MAXN][MAXSUM];

// dp[k][s]表示从前k个数中去任意个数,且这些数之和为s的取法是否存在
int main()
{
    int n, i, k1, k2, s, u;
    cin >> n;
    for (i=1; i<=2*n; i++)
        cin >> A[i];
    int sum = 0;
    for (i=1; i<=2*n; i++)
        sum += A[i];
    memset(dp,0,sizeof(dp));
    dp[0][0]=true;
    // 外阶段k1表示第k1个数,内阶段k2表示选取数的个数
    for (k1=1; k1<=2*n; k1++)           // 外阶段k1
    {
        for (k2=k1; k2>=1; k2--)        // 内阶段k2
            for (s=1; s<=sum/2; s++)    // 状态s
            {
                //dp[k1][s] = dp[k1-1][s];
                // 有两个决策包含或不包含元素k1
                if (s>=A[k1] && dp[k2-1][s-A[k1]])
                    dp[k2][s] = true;
            }
    }
    // 之前的dp[k][s]表示从前k个数中取任意k个数,经过下面的步骤后
    // 即表示从前k个数中取任意个数
    for (k1=2; k1<=2*n; k1++)
        for (s=1; s<=sum/2; s++)
            if (dp[k1-1][s])
                dp[k1][s]=true;
    // 确定最接近的给定值sum/2的和
    for (s=sum/2; s>=1 && !dp[2*n][s]; s--);
    printf("the differece between two sub array is %d\n", sum-2*s);
}

但本题还增加了一个限制条件,即选出的物体数必须为n,这个条件限制了内阶段k2的取值范围,并且dp[k][s]的含义也发生变化。这里的dp[k][s]表示从前k个数中取任意不超过n的k个数,且这些数之和为s的取法是否存在。

代码:

#include <iostream>
#include <algorithm>

using namespace std;

#define MAXN 101
#define MAXSUM 100000
int A[MAXN];
bool dp[MAXN][MAXSUM];

// 题目可转换为从2n个数中选出n个数,其和尽量接近于给定值sum/2
int main()
{
    int n, i, k1, k2, s, u;
    cin >> n;
    for (i=1; i<=2*n; i++)
        cin >> A[i];
    int sum = 0;
    for (i=1; i<=2*n; i++)
        sum += A[i];
    memset(dp,0,sizeof(dp));
    dp[0][0]=true;
    // 对于dp[k][s]要进行u次决策,由于阶段k的选择受到决策的限制,
    // 这里决策选择不允许重复,但阶段可以重复,比较特别
    for (k1=1; k1<=2*n; k1++)           // 外阶段k1
        for (k2=min(k1,n); k2>=1; k2--) // 内阶段k2
            for (s=1; s<=sum/2; s++)    // 状态s
                // 有两个决策包含或不包含元素k1
                if (s>=A[k1] && dp[k2-1][s-A[k1]])
                    dp[k2][s] = true;
    // 确定最接近的给定值sum/2的和
    for (s=sum/2; s>=1 && !dp[n][s]; s--);
    printf("the differece between two sub array is %d\n", sum-2*s);
    for(int i = 0; i < n; i++)
        if(dp[i][s])
            printf("the sub array is %d\n", A[i]);
}

时间复杂度为O(sum*N^2)

扩展问题:
1.如果数组中有负数呢?

分析与解法:
1.如果数组中有负数的话,上面的背包策略就不能使用了(因为第三重循环中的s是作为数组的下标的,不能出现负数的),需要将数组中的所有数组都加上最小的那个负数的绝对值,将数组中的元素全部都增加一定的范围,全部转化为正数,然后再使用上面的背包策略就可以解决了。

文章及代码参考以下博文:
http://blog.163.com/yichangjun1989@126/blog/static/13197202820143305145222/
http://my.oschina.net/wizardpisces/blog/114538
http://m.blog.csdn.net/blog/tianshuai11/7828907

    原文作者:zengzhen_csdn
    原文地址: https://blog.csdn.net/zengzhen_CSDN/article/details/50148123
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞