http://ace.delos.com/usacoprob2?a=HM3rU6DlsPt&S=subset
这题最初的想法是:DFS,因为两个集合理论上是一样的,所以处理时只需处理一个集合,另一个集合自然就出来了。
DFS的程序很好写,这是主要的实现函数:
void dfs(int suml,int h) //sum保存当前的集合元素的和,h保存上一次取的数 { if (suml>=mid) //mid是一个集合所要求的元素的和 { if (suml==mid) sum++; return; } for (int i=1;i<h;i++) //这里我是倒序处理的,听说可以快点。。。 if (f[i]){ f[i]=false; dfs(suml+i,i); f[i]=true; } } //...... dfs(n,n); //这是调用dfs的方法
不过,程序是简单,但是时间上过不去啊。。。。
DFS无论怎么优化,也很难优化到所需的规模。
这样,就要寻求其他方法了。这里我用了DP。
DP,我借用了01背包的方法。
首先一点是确定的,按照DFS那种想法,把问题转化为:在1~n中选出k个数组成一个集合,使它们的和为一个确定的数,求出可选择的方案数。
而01背包问题是,要在n种物品中选出k个,使它们能放入背包中,且使价值最大。
这里只是把其中的“使价值最大”变为“使和为一个确定的数”。
01背包中,方程是这样写的:F[i][j]=max(F[i-1][j],F[i-1][j-w[i]]+v[i])
类似01背包中状态转移方程的定义,定义F[i][j]表示选择前i个数,使它们的和达到j时的方案数。
这样我们不再求价值最大,只是求方案数,这就要求方程中是做加法而不是判断最大。
到此,方程就很好写了。
这样,方程就是:
F[i][j]=F[i-1][j]+f[i-1][j-i] (j>=i)
方程的意思很明了,但是这方程有个条件,就是j>=i,否则j-i<0,这就没意义了。
这样,又可以写出j<i的方程:
F[i][j]=F[i-1][j] (j<i)
到此,已经得出了转移方程:
F[i][j]=F[i-1][j]+F[i-1][j-i] (j>=i)
F[i][j]=F[i-1][j] (j<i)
最后的结果是F[n][tot]/2,其中tot是一个集合所要求的和,至于为什么要除2,也很容易,方程中并不能说明到底当前算的集合到底是不是之前算过的集合的另一半,也就是说,对于n=3,集合为{1,2}我们算了一遍,到集合{3}时我们又算了一遍。。。。
对于初始化条件,明显一个是F[1][1]=1,但是只有这个还不够,还差F[1][0]=1,它表明一个都不取时也是一种方案。
贴上我的代码:
#include <iostream> #include <cstdio> using namespace std; long long f[50][2000]={0}; int main() { freopen("subset.in","r",stdin); freopen("subset.out","w",stdout); int n; cin>>n; if (n*(n+1)%4!=0) { //直接处理了不能划分的数据 cout<<0<<endl; return 0; } int m=n*(n+1)/4; f[1][1]=1; f[1][0]=1; for (int i=2;i<=n;i++) for (int j=0;j<=m;j++) if (i>j) f[i][j]=f[i-1][j]; else f[i][j]=f[i-1][j]+f[i-1][j-i]; cout<<f[n][m]/2<<endl; return 0; }
至此,这题基本上解决了。
当然,这题也可以参照经典题目“数字三角形”来设计方程。