每日一题(19)——数组分割(动态规划)

来源:互联网 发布:淘宝未发货退款流程 编辑:程序博客网 时间:2024/06/01 23:36

一、问题:

      1. 有一个无序、元素个数为2n的正整数数组,要求:如何能把这个数组分割为两个子数组,子数组的元素个数不限,并使两个子数组之和最接近。

      2. 有一个无序、元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组之和最接近。

 

input:

2

2 3 3 6

output:

the count of num: 1  the max sum of the num: 6 //解法3

the differece between two sub array is 2 //解法1、2、3

the index of selected num: 6 //解法3

 

二、分析:

         动态规划解策略,这个问题其实完全就是0-1背包问题(公正陪审团问题与问题2一致)这两个问题的变体都可以总结为数组求和问题(又可分为:数组项数一定与数组项数不定两种)

假设数组A[1..2N]所有元素的和是SUM。令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最接近的那个和,这便是答案。这个算法的时间复杂度是O(2^N).
          因为这个过程中只关注和不大于SUM/2的那个子数组的和。所以集合中重复的和以及大于SUM/2的和都是没有意义的。把这些没有意义的和剔除掉,剩下的有意义的和的个数最多就是SUM/2个。所以,我们不需要记录S(2N,N)中都有哪些和,只需要从SUM/2到1遍历一次,逐个询问这个值是不是在S(2N,N)中出现,第一个出现的值就是答案。我们的程序不需要按照上述递推公式计算每个集合,只需要为每个集合设一个标志数组,标记SUM/2到1这个区间中的哪些值可以被计算出来。

 

三、 数组求和问题分析:

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

       与0-1背包问题&公正陪审团问题相一致,都是涉及到:

        1.个数 2.权重 3.限制(权重和不超过sum/2) 4.最优(最接近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]的值作为下标存储起来,将这个最优问题转化为判定问题,用带动态规划的思想的递推法来解。

 

四、解法

解法1:(小田)

分为几个阶段:

       外阶段:在前k1个数中进行选择,k1=1,2...2*n。
       内阶段:从这k1个数中任意选出k2个数,k2=1,2...k1。

       状态:这k2个数的和为s,s=1,2...sum/2。

       决策:决定这k2个数的和有两种决策,一个是这k2个数中包含第k1个数,另一个是不包含第k1个数。
 

这种做法与0-1背包的方法2相似。很厉害的方法,不需要判断一个节点是否已经使用过

dp[k][s]表示从前k个数中取任意个数,且这些数之和为s的取法是否存在。(这句话跟我的理解不太一样,我得请教一下小田博主~)

 

 

[cpp] view plaincopyprint?
  1. #include <iostream>  
  2. #include <algorithm>  
  3.   
  4. using namespace std;  
  5.   
  6. #define MAXN 101  
  7. #define MAXSUM 100000  
  8. int A[MAXN];  
  9. bool dp[MAXN][MAXSUM];  
  10.   
  11. // dp[k][s]表示从前k个数中去任意个数,且这些数之和为s的取法是否存在  
  12. int main()  
  13. {  
  14.     int n, i, k1, k2, s, u;  
  15.     cin >> n;  
  16.     for (i=1; i<=2*n; i++)  
  17.         cin >> A[i];  
  18.     int sum = 0;  
  19.     for (i=1; i<=2*n; i++)  
  20.         sum += A[i];  
  21.     memset(dp,0,sizeof(dp));  
  22.     dp[0][0]=true;  
  23.         // 外阶段k1表示第k1个数,内阶段k2表示选取数的个数  
  24.          // 这跟前面陪审团和0-1背包的方法不太一样,他们是在外阶段(外循环)迭代选取个数,内阶段迭代具体选取那个数  
  25.          // 这样做需验证选取的数是否出现过,但是可以通过保存Path[个数][状态和]来存储各个状态;  
  26.     for (k1=1; k1<=2*n; k1++)            // 外阶段k1  
  27.     {  
  28.         for (k2=k1; k2>=1; k2--)     // 内阶段k2  
  29.             for (s=1; s<=sum/2; s++) // 状态s  
  30.             {  
  31.                 //dp[k1][s] = dp[k1-1][s];  
  32.                 // 有两个决策包含或不包含元素k1  
  33.                 if (s>=A[k1] && dp[k2-1][s-A[k1]])  
  34.                     dp[k2][s] = true;  
  35.             }  
  36.     }  
  37.     /*根据0-1背包问题改写的方法:事实证明这种方法在不判断选用节点k2是否使用过的情况下,不可取,因为可能会重复调用某一个节点,除非再利用Path[k1][s]保存相应状态的节点。再判断它是否出过。那样的话,就需要用dp[k1][s]保存状态和s,也就是说跟第二个坐标一样。 
  38.     for (k1=0; k1<2*n; k1++)         // 迭代选取数量 
  39.     { 
  40.         for (s=0; s<=sum/2; s++)         // 状态和sum:s 
  41.             if(dp[k1][s]==true) 
  42.             for (k2=1; k2<=2*n; k2++)        // 选取第k2个    
  43.             { 
  44.                 if (s>=A[k1] )   // if(s>=A[k1] && dp[k2-1][s-A[k1]]) 
  45.                     dp[k1+1][s+A[k2]] = true; 
  46.             } 
  47.     } 
  48.     */  
  49.     // 之前的dp[k][s]表示从前k个数中取任意k个数,经过下面的步骤后  
  50.     // 即表示从前k个数中取任意个数  
  51.     for (k1=2; k1<=2*n; k1++)  
  52.         for (s=1; s<=sum/2; s++)  
  53.             if (dp[k1-1][s])  
  54.                 dp[k1][s]=true;  
  55.     // 确定最接近的给定值sum/2的和  
  56.     for (s=sum/2; s>=1 && !dp[2*n][s]; s--)  
  57.                ;  
  58.                  
  59.     printf("the differece between two sub array is %d\n", sum-2*s);  
  60. }  


 

解法2.

根据0-1背包问题方法2改写的算法:也好使~这种方式更简便!

只需要改动一点:利用dp[i][m]保存状态和,跟m的值一样。

依然无需测试一个节点是否已经存在,但依然无法保存路径,即无法输出最优解的过程

[cpp] view plaincopyprint?
  1. #include <iostream>  
  2.   
  3. using namespace std;  
  4.   
  5. const int nMax=400;     //待选物品数;  
  6. const int mMax=10000;  //最大和;  
  7.   
  8. struct{  
  9.     int wei;  
  10. }node[nMax];  
  11.   
  12. int main()  
  13. {  
  14.     int m, n, i ,w ,dp[mMax],sumN=0;  
  15.     while(cin>>n)  
  16.     {  
  17.         if(n==0) break;  
  18.         for (i=1; i<=2*n;i++)  
  19.         {  
  20.             cin>>node[i].wei;  
  21.             sumN += node[i].wei;  
  22.         }  
  23.         m=sumN/2;  
  24.   
  25.         memset(dp, 0, (m+1)*sizeof(int));  
  26.   
  27.         for (i=1; i<=2*n; i++)  
  28.             for ( w=m; w>=node[i].wei; w-- )//根据解法1的分析,必须将权重保存成下标才能满足最优化问题。  
  29.                 if ( dp[w] < dp[w - node[i].wei] + node[i].wei )  
  30.                     dp[w] = dp[w - node[i].wei] + node[i].wei;  
  31.   
  32.         cout<<dp[m]<<endl;  
  33.         cout<<"the differece between two sub array is: "<<sumN - 2*dp[m]<<endl;  
  34.     }  
  35. }  

 

解法3,

根据公正陪审团问题改写的算法dp[k][s]:外循环是选取个数k,中间是状态和s,内循环是选取具体哪一个index;同时可以保存路径,输出具体选取哪些数字

 

[cpp] view plaincopyprint?
  1. #include <iostream>  
  2. #include <algorithm>  
  3. #include <set>  
  4. using namespace std;  
  5.   
  6. #define MAXN 101  
  7. #define MAXSUM 100000  
  8. int A[MAXN];  
  9. int dp[MAXN][MAXSUM];  
  10. int Path[MAXN][MAXSUM];  
  11.   
  12.   
  13. // dp[k][s]表示取k个数,且和为s的情况下,保存的依然是和s;因为要优化判断  
  14. int main()  
  15. {  
  16.     int n, i, k1, k2, s, u,t1,t2;  
  17.     cin >> n;  
  18.     for (i=1; i<=2*n; i++)  
  19.         cin >> A[i];  
  20.     int sum = 0;  
  21.     for (i=1; i<=2*n; i++)  
  22.         sum += A[i];  
  23.     memset(dp,-1,sizeof(dp));  
  24.       
  25.     dp[0][0]=0;//初始状态  
  26.   
  27.     for (k1=0; k1<2*n; k1++)         // 选取的数量k1  
  28.     {  
  29.         for (s=0; s<=sum/2; s++)     // 状态和s  
  30.         if(dp[k1][s]>=0)  
  31.         {  
  32.             for (k2=1; k2<=2*n; k2++)        // 具体选取哪一个k2  
  33.                 if(dp[k1][s]+A[k2]>dp[k1+1][s+A[k2]] && s+A[k2]<=sum/2)  
  34.                 {  
  35.                     t1=k1;t2=s;  
  36.                     while(t1>0&&Path[t1][t2]!=k2)//验证k2是否在前面出现过  
  37.                     {  
  38.                         t2-=A[Path[t1][t2]] ;//减前一个元素的值  
  39.                         t1--;  
  40.                     }  
  41.                     if (t1==0)  
  42.                     {  
  43.                         dp[k1+1][s+A[k2]] = dp[k1][s]+A[k2];  
  44.                         Path[k1+1][s+A[k2]] = k2;       //k2保存在Path中  
  45.                     }  
  46.                 }  
  47.         }  
  48.     }  
  49.   
  50.     int maxS=0,maxN=0;  
  51.     for (k1=1; k1<=2*n; k1++)  
  52.         for (s=1; s<=sum/2; s++)  
  53.             if (dp[k1][s]>maxS)  
  54.             {  
  55.                 maxS=dp[k1][s];  
  56.                 maxN=k1;  
  57.             }  
  58.     cout<<"the count of num: "<<maxN<<"  the max sum of the num: "<<maxS<<endl;         
  59.     cout<<"the differece between two sub array is: "<< sum-2*maxS<<endl;  
  60.   
  61.     set<int> index;  
  62.     index.clear();  
  63.     for (int i=0; i<maxN; i++)  
  64.     {  
  65.         int id = Path[maxN-i][maxS];  
  66.         index.insert(id);  
  67.         maxS -= A[id];  
  68.     }  
  69.     cout<<endl;  
  70.     cout<<"the index of selected num: ";  
  71.     for(set<int>::iterator iter=index.begin(); iter!=index.end(); iter++) cout<<*iter<<" ";  
  72.       
  73. }  


 


 

问题2.

解法1:

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

[cpp] view plaincopyprint?
  1. #include <iostream>  
  2. #include <algorithm>  
  3.   
  4. using namespace std;  
  5.   
  6. #define MAXN 101  
  7. #define MAXSUM 100000  
  8. int A[MAXN];  
  9. bool dp[MAXN][MAXSUM];  
  10.   
  11. // 题目可转换为从2n个数中选出n个数,其和尽量接近于给定值sum/2  
  12. int main()  
  13. {  
  14.     int n, i, k1, k2, s, u;  
  15.     cin >> n;  
  16.     for (i=1; i<=2*n; i++)  
  17.         cin >> A[i];  
  18.     int sum = 0;  
  19.     for (i=1; i<=2*n; i++)  
  20.         sum += A[i];  
  21.     memset(dp,0,sizeof(dp));  
  22.     dp[0][0]=true;  
  23.     // 对于dp[k][s]要进行u次决策,由于阶段k的选择受到决策的限制,  
  24.     // 这里决策选择不允许重复,但阶段可以重复,比较特别  
  25.     for (k1=1; k1<=2*n; k1++)                // 外阶段k1  
  26.         for (k2=min(k1,n); k2>=1; k2--)      // 内阶段k2  
  27.             for (s=1; s<=sum/2; s++) // 状态s  
  28.                 // 有两个决策包含或不包含元素k1  
  29.                 if (s>=A[k1] && dp[k2-1][s-A[k1]])  
  30.                     dp[k2][s] = true;  
  31.     // 确定最接近的给定值sum/2的和  
  32.     for (s=sum/2; s>=1 && !dp[n][s]; s--);  
  33.     printf("the differece between two sub array is %d\n", sum-2*s);  
  34. }  


问题1的解法3肯定适用,只要最后选择最大值时确定dp[k][s]的k即可;

问题公正陪审团问题1的解法2是否使用,还有待验证。

 

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

 

五、总结:                                                                                   

这种带权重的求和的最优化问题,都可以转换为数组求和最优问题,(转化为判别划分问题)。

从具体的解法上来说,解法3,即公正陪审团问题的解法比较完善,可以保存路径,但是需要额外判断选取的某个数是否已经存在。且这种解法比较容易理解:

dp[k][s]:

外循环迭代k,表示取元素的个数;

中间循环迭代s:表示状态和s(限制条件,不能超过多少……)

内循环迭代index:表示具体是否选取A[index];

判别:+A[index]满足最优条件 &A[index]没有出现过(通过Path判别,保存)

最终:输出时若有个数限制K,则在dp[K][]中查找,若没有个数限制,则在dp[][]全部中查找

PS:利用Path输出路径时,可以先保存在set中,这样输出有序。

 

 

再感慨一下:DP你真是让人捉摸不透啊~

0 0
原创粉丝点击