算法设计与分析——划分问题(动态规划)

Description


给定一个正整数的集合A={a1,a2,….,an},是否可以将其分割成两个子集合,使两个子集合的数加起来的和相等。例A = { 1, 3, 8, 4, 10} 可以分割:{1, 8, 4} 及 {3, 10},

Input

第一行集合元素个数n  n <=300 第二行n个整数
 
Output

如果能划分成两个集合,输出任意一个子集,否则输出“no”

Sample Input

5
1 3 8 4 10

Sample Output

3 10

 

这道题是本校OJ上的一道题,作为算法设计与分析课程的作业,要求使用动态规划,我最开始的想法是将这个问题往背包问题上转换,思路应该是正确的,不过一开始没能实现,最终还是写出来了,但这里要介绍一种老师讲的方法。

用数组a[i]来存储正整数的集合。

构造一个二维数组表t[i][j],表示{a1,a2......ai}存在子集和为j,并将其值赋为真。

这句话该怎么理解呢?

比如此时ai={3,4,8,1,10}

a1=3,a2=4,a3=8,a4=1,a5=10

那么{a1,a2,a3}构成的子集有{∅}、{a1}、{a2}、{a3}、{a1,a2}、{a2,a3}、{a1,a3}、{a1,a2,a3},对应的子集和为0,3,4,8,7,12,11,15。

这样就可以得到t[3][0]=1 t[3][3]=1 t[3][4]=1 t[3][8]=1 t[3][7]=1 t[3][12]=1 t[3][11]=1 t[3][15]=1

看到这里是不是就应该明白这个二维数组的含义了吧,我们只要求得t[n][sum/2]=1,说明{a1,a2......an}存在一组子集正数其和为sum/2,(这里的sum表示整个整数集合之和),也就是说存在两个子集合加起来的和相等。

 

既然是动态规划我们需要探究一下其递归方程应该如何表示

如果t[i-1][j]=1       那么一定有t[i][j]=1  因为前i-1个正整数的子集能够组成的和为j,那么再加上第i个正整数,还是用之前的子集,能够组成和还是j。

如果t[i-1][j-a[i]]=1  那么也会有t[i][j]=1  因为前i-1个正整数的子集能够组成的和为j-a[i],那么再加上第i个正整数,还是用之前的子集再加上a[i],能够组成和j。

由此我们得到了递归迭代方程:

t[1][0] =1

t[1][a[1]] =1

t[i][j] =1 《==( t[i-1][j]=1 || t[i-1][j-a[i]]=1 )

 

还是以上面的ai={3,4,8,1,10}为例子,写一下t[i][j]的二维数组表

我们根据t[n][sum/2]的值就能判断是否存在两个相等的子集和,但怎么样确定子集呢?

确定子集的方法有很多,OJ后台只需要一种答案即可,这里讲一下我使用的方法。

我的想法就是观察上面的表看看sum/2是有那些元素贡献的,其实上面的表是从左上角到右下角打印的,我们从后向前逆推组成元素时就需要从右下角向左上角推理,发现t[4][13]是13列第一个带有真值的,最终结果t[5][13]也为真,说明a[4]肯定是纳入子集中,sum/2-a[4]后,也就是13-1=12,我们再来看t[3][12]是12列第一个带有真值的,说明a[3]肯定被纳入子集中,12-a[3],也就是12-8=4;我们再来看t[2][4]是第4列第一个带有真值的,说明a[2]肯定被纳入子集中,4-a[2],也就是4-4=0,至此子集中所有的元素都找到了。为1、8 、4

 

 

之前有同学不明白第一张表中的数据是怎么来的,这里我再画一一张表,追踪一下动态规划的过程。

 

 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int main()
{
    int n;
    int sum=0;
    int a[310];
    int ans[155];
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i]);
        sum+=a[i];
    }
    if(sum%2!=0)
    {
        printf("no\n");
        return 0;
    }
    int m=sum/2;
    int dp[n+1][m+1];
    memset(dp,0,sizeof(dp));
    dp[1][0]=1;
    dp[1][a[1]]=1;
    for(int i=2; i<=n; i++) //前i个元素
    {
        for(int j=0; j<=m; j++) //可以构成j
        {
            if(dp[i-1][j]||dp[i-1][j-a[i]])
            {
                dp[i][j]=1;
            }
        }
    }
    if(!dp[n][m])
    {

        printf("no\n");
        return 0;
    }
    int cnt=0;
    for(int j=m; j>=0; j--)
    {
        for(int i=n; i>=1; i--)
        {
            if(dp[i][j]&&!dp[i-1][j])//二维数组中每一列最顶部的那个T
            {
                ans[cnt]=a[i];
                cnt++;
                j=j-a[i];
                if(j==0)//找完结束
                {
                    break;
                }
            }
        }
    }
    for(int i=0; i<cnt-1; i++)
    {
        printf("%d ",ans[i]);
    }
    printf("%d\n",ans[cnt-1]);
    return 0;
}

 

 

posted @ 2019-10-11 19:43  王陸  阅读(4205)  评论(1编辑  收藏  举报