Topcoder Jewelry问题是否为子集和变体及DP解法咨询
嘿,我来帮你拆解这个Topcoder Jewelry问题的DP解法,刚好你在从子集和入手,这个思路非常对!
1. 这个问题是不是子集和问题的变体?
绝对是的!而且是带组合计数的扩展版。普通子集和问题只关心“能不能选出一个子集,总和等于目标值”,但Jewelry问题更复杂:它要求把所有珠宝分成两个总价值相等的子集,还要统计有多少种不同的分法(注意:即使珠宝价值相同,只要是不同的个体,就算不同的分法)。本质上,我们需要找所有满足sum(A)=sum(B)的子集对(A,B),其中A∪B是全部珠宝、A∩B为空,这就需要在子集和的基础上,加上组合数的统计,还要处理同价值珠宝的重复选择问题。
2. 两个函数分别统计什么?为什么要用两个DP表?
我们逐个分析:
① cnk() 函数
这个函数是预计算组合数的,nk[n][k] 表示从n个元素中选k个的组合数(也就是数学上的C(n,k))。为什么要预算这个?因为问题里会有多个价值相同的珠宝,比如某类价值的珠宝有c个,我们需要计算从这c个里选u个放到其中一个子集的方法数,这时候就直接用nk[c][u]来快速获取,不用每次临时计算。
代码里的递推式就是组合数的经典递推规则:C(n,k) = C(n-1,k-1) + C(n-1,k),边界条件C(n,0)=1、C(0,k)=0(k>0)也完全符合组合数的定义。
② calc() 函数
这个是标准的子集和计数DP,T[ile][x] 表示前ile个珠宝中,选出若干个珠宝,总价值恰好等于x的不同选法数量。递推逻辑也很直观:
- 对于第
ile个珠宝(价值为a),如果不选它,方法数等于前ile-1个珠宝凑出x的方法数,即T[ile-1][x] - 如果选它,且
x >= a,方法数等于前ile-1个珠宝凑出x-a的方法数,即T[ile-1][x-a]
把这两种情况的方法数相加,就是T[ile][x]的结果。
为什么要用两个DP表?
在Jewelry的解法中,我们会先把珠宝按价值分组(比如先排序,把相同价值的珠宝放在一起)。然后用两个DP表:
B表:统计当前处理的价值组之前的所有珠宝的子集和计数(也就是前done个珠宝的情况)F表:统计当前处理的价值组之后的所有珠宝的子集和计数(也就是从done+1到n个珠宝的情况)
这样拆分的核心原因是:当处理当前价值组(有c个同价值珠宝)时,我们可以独立计算从这个组里选u个到子集A、剩下的c-u个到子集B的情况,再结合前后两组的子集和结果,就能避免重复计算同价值珠宝的分法,保证计数的准确性。
3. 那段代码的逻辑是怎么构建解法的?
这段代码是整个解法的核心,专门处理当前价值组对总结果的贡献,我一步步给你拆解:
u:表示从当前价值组(共c个,价值为v[done])中选出来放到子集A的数量;uu = u * v[done]就是这u个珠宝的总价值。B[done][x-uu]:表示从当前组之前的珠宝中,选出总价值为x-uu的子集的方法数(这些子集属于A)。F[n-done-u][x]:表示从当前组之后的珠宝中,选出总价值为x的子集的方法数(这些子集属于A)。这里n-done-u的含义是:当前组已经选了u个到A,剩下的n-done个珠宝里,当前组未选的c-u个要放到B里,不能再选到A,所以只考虑后面的n-done-u个珠宝。nk[c][u]:从当前组的c个珠宝里选u个到A的组合数,也就是有多少种选法挑出这u个珠宝。
把这三个数相乘,就表示:
选
u个当前组珠宝到A,同时从前面选总价值x-uu的珠宝到A,从后面选总价值x的珠宝到A,这样A的总价值恰好等于total_sum/2(total_sum是所有珠宝的总价值,必须是偶数才有解),这一组选择对应的合法分法数。
最后把所有u的情况累加起来,就得到当前价值组对总结果的贡献。done=p是更新当前处理到的珠宝位置,准备处理下一个价值组。
内容的提问来源于stack exchange,提问作者ng.newbie

