我对动态编程相当新,并且还不了解它可以解决的大多数类型的问题。因此,我在理解solution的Jewelry topcoder problem方面遇到了问题。
至少有人能给我一些关于代码在做什么的提示吗?
最重要的是这个问题是subset-sum problem的变体吗?因为我正在研究这个问题来理解这个问题。
这两个功能实际上是在计算什么? 为什么我们实际使用两个DP表?
void cnk() {
nk[0][0]=1;
FOR(k,1,MAXN) {
nk[0][k]=0;
}
FOR(n,1,MAXN) {
nk[n][0]=1;
FOR(k,1,MAXN)
nk[n][k] = nk[n-1][k-1]+nk[n-1][k];
}
}
void calc(LL T[MAXN+1][MAX+1]) {
T[0][0] = 1;
FOR(x,1,MAX) T[0][x]=0;
FOR(ile,1,n) {
int a = v[ile-1];
FOR(x,0,MAX) {
T[ile][x] = T[ile-1][x];
if(x>=a) T[ile][x] +=T[ile-1][x-a];
}
}
}
如何使用以下逻辑构建原始解决方案?
FOR(u,1,c) {
int uu = u * v[done];
FOR(x,uu,MAX)
res += B[done][x-uu] * F[n-done-u][x] * nk[c][u];
}
done=p;
}
非常感谢任何帮助。
答案 0 :(得分:5)
让我们先考虑以下任务:
这可以通过使用一些额外内存的动态编程在多项式时间内解决。
动态编程方法如下: 而不是解决 N 和 S 的问题,我们将解决以下形式的所有问题:
这是动态编程解决方案的常见特征:您解决了整个系列的相关问题,而不仅仅是解决原始问题。关键的想法是,更容易解决更难的问题设置(即更高的 n 和 s )可以从更容易的解决方案中建立起来设置。
解决 n = 0的问题是微不足道的(sum s = 0可以用一种方式表示 - 使用空集,而所有其他总和可以&# 39;以任何方式表达)。 现在考虑我们已经解决了所有值到某个 n 的问题,并且我们将这些解决方案放在矩阵 A (即 A [n] [s]是使用第一个 n 元素写入sum s 的方法数。)
然后,我们可以使用以下公式找到 n +1的解决方案:
A [n + 1] [s] = A [n] [s - V [n + 1]] + A [n] [s]。
实际上,当我们使用第一个 n + 1 数字写出总和 s 时,我们可以包含或不包含V [n + 1](n + 1 < sup> th term)。
这是calc
函数计算的内容。 (cnk
函数使用Pascal的规则来计算二项式系数)
注意:一般情况下,如果最终我们只对回答初始问题感兴趣(即 N 和 S ),那么数组A
可以是单维的(长度 S ) - 这是因为无论何时尝试构建 n + 1的解决方案,我们只需要 n 的解决方案,而不是较小的值。
这个问题(在这个答案中最初陈述的问题)确实与subset sum problem有关(找到零和零的元素子集)。
如果我们对所用整数的绝对值有合理限制(我们需要分配一个辅助数组来表示所有可能的可达总和),则可以应用类似类型的动态编程方法。
在零和问题中,我们实际上并不对计数感兴趣,因此 A 数组可以是一个布尔数组(指示一个和是否可达)。
此外,可以使用另一个辅助阵列 B 来重建解决方案(如果存在)。
现在看起来像这样:
if (!A[s] && A[s - V[n+1]]) {
A[s] = true;
// the index of the last value used to reach sum _s_,
// allows going backwards to reproduce the entire solution
B[s] = n + 1;
}
注意:实际实现需要额外注意处理负数,这不能直接表示数组中的索引(索引可以通过考虑最小可达总和来移位,或者,如果在C / C ++中工作,可以应用类似于本答案中描述的技巧:https://stackoverflow.com/a/3473686/6184684)。
我将详细介绍上述提示如何应用于问题中的TopCoder problem及其solution。
B和F矩阵。
首先,请注意解决方案中 B 和 F 矩阵的含义:
B [i] [s]代表仅使用最小 i 项目达到总和 s 的方法数量< / p>
F [i] [s]代表仅使用最大 i 项目达到总和 s 的方法数量< / p>
实际上,两个矩阵都是使用calc
函数计算的,在按照升序排列珠宝值数组( B )和降序排列( F )。
没有重复的案例的解决方案。
首先考虑没有重复珠宝值的情况,使用此示例:[5, 6, 7, 11, 15]
。
对于答案的提醒,我将假设数组按升序排序(因此&#34;第一个 i 项&#34;将引用最小的 i 1)。
给予Bob的每个项目的价值都低于(或等于)给予Frank的每个项目,因此在每个好的解决方案中都会有一个分离点,以便Bob在该分离点之前只接收项目,而Frank只接收到之后的项目那一点。
要计算所有解决方案,我们需要对所有可能的分离点求和。
例如,当分离点位于3 rd 和4 th 项之间时,Bob将仅从[5, 6, 7]
子阵列中选择项目(最小的3项),弗兰克将从剩余的[11, 12]
子阵列(最大的2项)中挑选项目。在这种情况下,可以通过它们获得单个和(s = 11)。每次可以通过两者获得总和时,我们需要乘以每个总和可以达到相应总和的方式的数量(例如如果Bob可以达到总和 s 在4种方式中,弗兰克可以用5种方式得到相同的总和 s ,然后我们可以得到20 = 4 * 5个有效解的总和,因为每种组合都是有效的解决方案。)
因此,我们将通过考虑所有分离点和所有可能的总和得到以下代码:
res = 0;
for (int i = 0; i < n; i++) {
for (int s = 0; s <= maxS; s++) {
res += B[i][s] * F[n-i][s]
}
}
然而,这里有一个微妙的问题。这通常会多次计算相同的组合(对于各种分离点)。在上面提供的示例中,对于分离[5, 6] - [7, 11, 15]
以及分离[5, 6, 7] - [11, 15]
,将计算具有和11的相同解。
为了缓解这个问题,我们可以通过&#34; Bob选择的项目的最大值来划分解决方案。 (或者,等同地,总是强迫Bob在他的选择中包括当前分离下第一个子阵列中最大值的项目。)
当Bob的最大值项是 i th 时,为了计算达到和 s 的方式的数量(按升序排序),我们可以使用B [i] [s - v [i]]。这是因为使用v [i]值项暗示要求使用来自第一个 i 项的子集来表示和s - v [i](索引0,1,... 我 - 1)。
这将实现如下:
res = 0;
for (int i = 0; i < n; i++) {
for (int s = v[i]; s <= maxS; s++) {
res += B[i][s - v[i]] * F[n - 1 - i][s];
}
}
这更接近TopCoder上的解决方案(在该解决方案中,done
对应于上面的i
和uu = v[i]
)。
允许重复的情况下的扩展名。
当重复值出现在数组中时,当Bob最有价值的项目是v [i]时,直接计算解决方案的数量就不再容易了。我们还需要考虑Bob选择的此类项目的数量。
如果 c 项目与v [i]具有相同的值,即 v [i] = v [i + 1] = ... v [i + c - 1],Bob选择你这样的项目,然后他达到某个总和 s 的方式数等于:
comb(c, u) * B[i][s - u * v[i]]
(1)
实际上,这是有效的,因为 u 项可以从 c 的总数中选取,它们具有相同的梳子(c,u)方式。对于 u 项目的每个这样的选择,剩余的总和是s - u * v [i],这应该使用来自第一个 i 项目的子集来表示(索引0,1,... i - 1),因此可以用B [i] [s - u * v [i]]方式完成。
对于Frank,如果Bob使用v [i]项目的 u ,表示总和 s 的方式的数量将等于:
F[n - i - u][s]
(2)
实际上,由于Bob使用最小的i + u值,因此Frank可以使用任何最大的n - i - u值来达到总和 s 。
通过结合上面的关系(1)和(2),我们得到了当Bob的最有价值的项目是v时,Frank和Bob都有和 s 的解的数量[i]他选择你这些项目等于:
comb(c, u) * B[i][s - u * v[i]] * F[n - i - u][s]
。
这正是给定的solution所实现的。
实际上,变量done
对应上面的变量 i ,变量x
对应于总和 s ,索引p
用于确定与v [done]具有相同值的c
项目,并使用u
上的循环来考虑Bob选择的所有可能数量的此类项目。
答案 1 :(得分:1)
以下是一些引用原始解决方案的Java代码。它还结合了qwertyman的奇妙解释(在可行的范围内)。我在此过程中添加了一些评论。
import java.util.*;
public class Jewelry {
int MAX_SUM=30005;
int MAX_N=30;
long[][] C;
// Generate all possible sums
// ret[i][sum] = number of ways to compute sum using the first i numbers from val[]
public long[][] genDP(int[] val) {
int i, sum, n=val.length;
long[][] ret = new long[MAX_N+1][MAX_SUM];
ret[0][0] = 1;
for(i=0; i+1<=n; i++) {
for(sum=0; sum<MAX_SUM; sum++) {
// Carry over the sum from i to i+1 for each sum
// Problem definition allows excluding numbers from calculating sums
// So we are essentially excluding the last number for this calculation
ret[i+1][sum] = ret[i][sum];
// DP: (Number of ways to generate sum using i+1 numbers =
// Number of ways to generate sum-val[i] using i numbers)
if(sum>=val[i])
ret[i+1][sum] += ret[i][sum-val[i]];
}
}
return ret;
}
// C(n, r) - all possible combinations of choosing r numbers from n numbers
// Leverage Pascal's polynomial co-efficients for an n-degree polynomial
// Leverage Dynamic Programming to build this upfront
public void nCr() {
C = new long[MAX_N+1][MAX_N+1];
int n, r;
C[0][0] = 1;
for(n=1; n<=MAX_N; n++) {
C[n][0] = 1;
for(r=1; r<=MAX_N; r++)
C[n][r] = C[n-1][r-1] + C[n-1][r];
}
}
/*
General Concept:
- Sort array
- Incrementally divide array into two partitions
+ Accomplished by using two different arrays - L for left, R for right
- Take all possible sums on the left side and match with all possible sums
on the right side (multiply these numbers to get totals for each sum)
- Adjust for common sums so as to not overcount
- Adjust for duplicate numbers
*/
public long howMany(int[] values) {
int i, j, sum, n=values.length;
// Pre-compute C(n,r) and store in C[][]
nCr();
/*
Incrementally split the array and calculate sums on either side
For eg. if val={2, 3, 4, 5, 9}, we would partition this as
{2 | 3, 4, 5, 9} then {2, 3 | 4, 5, 9}, etc.
First, sort it ascendingly and generate its sum matrix L
Then, sort it descendingly, and generate another sum matrix R
In later calculations, manipulate indexes to simulate the partitions
So at any point L[i] would correspond to R[n-i-1]. eg. L[1] = R[5-1-1]=R[3]
*/
// Sort ascendingly
Arrays.sort(values);
// Generate all sums for the "Left" partition using the sorted array
long[][] L = genDP(values);
// Sort descendingly by reversing the existing array.
// Java 8 doesn't support Arrays.sort for primitive int types
// Use Comparator or sort manually. This uses the manual sort.
for(i=0; i<n/2; i++) {
int tmp = values[i];
values[i] = values[n-i-1];
values[n-i-1] = tmp;
}
// Generate all sums for the "Right" partition using the re-sorted array
long[][] R = genDP(values);
// Re-sort in ascending order as we will be using values[] as reference later
Arrays.sort(values);
long tot = 0;
for(i=0; i<n; i++) {
int dup=0;
// How many duplicates of values[i] do we have?
for(j=0; j<n; j++)
if(values[j] == values[i])
dup++;
/*
Calculate total by iterating through each sum and multiplying counts on
both partitions for that sum
However, there may be count of sums that get duplicated
For instance, if val={2, 3, 4, 5, 9}, you'd get:
{2, 3 | 4, 5, 9} and {2, 3, 4 | 5, 9} (on two different iterations)
In this case, the subset {2, 3 | 5} is counted twice
To account for this, exclude the current largest number, val[i], from L's
sum and exclude it from R's i index
There is another issue of duplicate numbers
Eg. If values={2, 3, 3, 3, 4}, how do you know which 3 went to L?
To solve this, group the same numbers
Applying to {2, 3, 3, 3, 4} :
- Exclude 3, 6 (3+3) and 9 (3+3+3) from L's sum calculation
- Exclude 1, 2 and 3 from R's index count
We're essentially saying that we will exclude the sum contribution of these
elements to L and ignore their count contribution to R
*/
for(j=1; j<=dup; j++) {
int dup_sum = j*values[i];
for(sum=dup_sum; sum<MAX_SUM; sum++) {
// (ways to pick j numbers from dup) * (ways to get sum-dup_sum from i numbers) * (ways to get sum from n-i-j numbers)
if(n-i-j>=0)
tot += C[dup][j] * L[i][sum-dup_sum] * R[n-i-j][sum];
}
}
// Skip past the duplicates of values[i] that we've now accounted for
i += dup-1;
}
return tot;
}
}