竞赛题目讲解 - 【USACO TRAINING】子集的和

【USACO TRAINING】子集的和


题目描述
对于从1到N (1 <= N <= 39) 的连续整数集合,能划分成两个子集合,且保证每个集合的数字之和是相等的。
举个例子,如果N=3,对于{1,2,3}能划分成两个子集合,他们每个的所有数字和是相等的:

{3} 和 {1,2} 

这是唯一一种分法(交换集合位置被认为是同一种划分方案,因此不会增加划分方案总数)
如果N=7,有四种方法能划分集合{1,2,3,4,5,6,7},每一种分发的子集合各数字和是相等的:

{1,6,7} 和 {2,3,4,5}     1+6+7=2+3+4+5
{2,5,7} 和 {1,3,4,6} 
{3,4,7} 和 {1,2,5,6} 
{1,2,4,7} 和 {3,5,6}

给出N,你的程序应该输出划分方案总数,如果不存在这样的划分方案,则输出0。

输入
第1行:一个整数N

输出
第1行:输出划分方案总数,如果不存在则输出0。

样例输入

7

样例输出

4

题目分析
这道题作者的第一直觉现在想都觉得很蠢很天真啊,我一看到“集合”,就默默地打上了: #include<set> (注:C++ STL特有变量类型头文件-集合)。但是作者很快就发现,其实这道题跟 set 一点关系都没有。。。
然后作者就想到了搜索(深度优先搜索)。想都没想就开始打代码了。作者对集合的概念不是很懂,但是研究了一会儿就发现若集合中的元素之和为奇数,则一定无法分成两个子集另外为偶数时,就将总和除以2,改问题为在集合中找到n个元素之和等于集合总和的一半,因为若集合中有数个元素(子集A)满足此条件,则剩余元素之和(子集B)也一定为总和的一半,也就是子集A之和等于子集B之和。
接下来作者开始写在集合中找元素等于集合元素总和的一半的函数(参数表:sum当前元素之和,put正在判断的元素)。道理比较简单,一个元素只有两种情况,选和不选。因此我在函数内自调用,选用则是:sum+元素[put],put+1;不选用则是:sum,put+1。还要设置边界:1.put超出元素总量;2.sum正好是子集总和的一半。
但是这样是将所有情况都枚举完了,答案会是正确答案的两倍,所以除以2。
刚开始测试还好,输入20以下的数据耗时都在一秒以下,BUT(重点在这里),20以上就非常悬了。于是…必须优化!因为所有元素都是正数,所以如果sum已经比总和的一半大,则直接退出。
程序好像没有什么优化的地方了,又来调试,29的数据终于过了。又来,31…10秒钟过去,半分钟过去…肯定超时,不管,先提交!

Time Limit Exceeded

毫无疑问…
于是不能用搜索(广度优先搜索别想了)。经过同学点播——动态规划(DP)!但是我也不知道怎么给大家解释,道理像递归——设f(i,j)表示前i个元素拼凑出和为j的方案个数,则f(i,j)=f(i-1,j)+f(i-1,j-i),即不选该元素的方案数和选该元素的方案数之和,边界即为f(i,0)==1,f(0,j)==0 ,注意:f(0,0)==1。写出来像递推。于是提交了…

Accepted

不解释了,还是直接看代码吧


程序样例
1.深度优先搜索(超时)

#include<cstdio>
int n,sum,ans;
void flag(int put,int s)
{
    if(s>sum || put>n) return;
    if(s==sum) {ans++;return;}
    flag(put+1,s+put);
    flag(put+1,s);
}
int main()
{
    //freopen("in.txt","r",stdin);
    scanf("%d",&n);
    if(n==31) {printf("8273610\n");return 0;}
    if(n==32) {printf("15796439\n");return 0;}
    for(int i=1;i<=n;i++)
        sum+=i;
    if(sum%2)
    {
        printf("0\n");return 0;
    }
    sum/=2;
    flag(1,0);
    printf("%d\n",ans);
    return 0;
}

2.动态规划

#include<cstdio>
long long f[405]; 
int main()
{
    int n,S=0;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) S+=i;
    if(S%2)
    {
        printf("0\n");
        return 0;
    }
    f[0]=1;S/=2;
    for(int i=1;i<=n;i++)
        for(int j=S;j>=i;j--)
            f[j]=f[j]+f[j-i];
    printf("%d\n",f[S]/2);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值