需要帮助了解Jewelry Topcoder解决方案的解决方案

时间:2018-01-02 10:42:00

标签: algorithm dynamic-programming

我对动态编程相当新,并且还不了解它可以解决的大多数类型的问题。因此,我在理解solutionJewelry 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;
}

非常感谢任何帮助。

2 个答案:

答案 0 :(得分:5)

让我们先考虑以下任务:

  • "给定 N V 正整数小于 K ,找到总和等于取值"

这可以通过使用一些额外内存的动态编程在多项式时间内解决。

动态编程方法如下: 而不是解决 N S 的问题,我们将解决以下形式的所有问题:

  • "仅使用第一个找到编写和 s (使用 s S )的方法的数量n N 的数字"。

这是动态编程解决方案常见特征:您解决了整个系列的相关问题,而不仅仅是解决原始问题。关键的想法是,更容易解决更难的问题设置(更高的 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对应于上面的iuu = 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;
    }
}