【问题标题】:Number of ways to divide n objects in k groups, such that no group will have fewer objects than previously formed groups?将 n 个对象分成 k 个组的方法有多少,使得没有一个组的对象比以前形成的组少?
【发布时间】:2020-02-04 04:27:59
【问题描述】:

示例: n=8, k=4 答案:5

[1,1,1,5], [1,1,2,4], [1,1,3,3], [1,2,2,3], [2,2,2, 2]

我想过应用动态规划来计算8个对象可以分成4组的方式的数量,但是不明白如何跟踪前一组对象的数量。

DP 方法:

for(int j=0;j<=n;j++)
{
    for(int i=1;i<=k;i++)
    {
        if(j<i)
            continue;
        if(j==i)
            dp[j]=1;
        for(int k=1;k<i;k++)
        {
            dp[j]+=dp[k]*dp[j-k];
        }
    }
}

请帮助解决方法。我对 DP 比较陌生。

【问题讨论】:

    标签: algorithm recursion dynamic-programming memoization


    【解决方案1】:

    这些被称为partitions with restricted number of parts。递归背后的想法,等于最大部分为 k 的分区数(证明留作简短有趣的阅读)是,如果分区的最小部分是 1,我们将 1 添加到n - 1 的所有分区为k - 1 部分(保证最小部分为1);如果最小部分不是1,我们将n - k的所有分区中的每个k部分加1到k部分中(保证每个部分都大于1)。

    这是一个简单的记忆:

    function f(n, k, memo={}){
      if (k == 0 && n == 0)
        return 1
    
      if (n <= 0 || k <= 0)
        return 0
    
      let key = String([n, k]) // Thanks to comment by user633183
    
      if (memo.hasOwnProperty(key))
        return memo[key]
    
      return memo[key] = f(n - k, k, memo) + f(n - 1, k - 1, memo)
    }
    
    console.time('time taken')
    console.log(f(1000, 10))
    console.timeEnd('time taken')

    这是自下而上的:

    function f(n, k){
      let dp = new Array(n + 1)
      for (let i=0; i<n+1; i++)
        dp[i] = new Array(k + 1).fill(0)
      dp[0][0] = 1
      
      for (let i=1; i<=n; i++)
        for (let j=1; j<=Math.min(i, k); j++)
          dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1]
    
      return dp[n][k]
    }
    
    console.time('time taken')
    console.log(f(1000, 10))
    console.timeEnd('time taken')

    【讨论】:

    • 您可以在memo 示例中显着减少运行时间,只需将[n, k] 字符串化一次——即const key = `${n},${k}`,然后使用memo.hasOwnProperty(key)memo[key]memo[key] = ...。您可以使用const key = String([n, k]),但分配数组确实没有意义。
    • 不错!这是一个比我的更干净的算法。我仍然更喜欢外部记忆,但这对性能影响不大。
    • 我创建了 a version of this 显示原始分区
    • dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1] 我无法理解这背后的逻辑。跨度>
    • @aman_41907 ij 对应于上面的递归公式中的 nk
    【解决方案2】:

    更新

    尽管下面的所有讨论仍然有用,the answer from גלעד ברקן 提供了一个更好的底层算法,允许我们跳过我的min 参数。 (我知道我应该查一下这个!)这种理解可以显着提高下面使用的算法的性能。


    将动态规划 (DP) 视为一种可以加速某些递归过程的简单优化技术。如果你的递归调用是重复的(就像斐波那契数一样),那么存储它们的结果可以大大加快你的程序。但底层逻辑仍然是递归调用。所以让我们先递归求解这个程序,看看我们可以在哪里应用 DP 优化。

    (8, 4) 只有五个解决方案足够小,即使时间是算法上的指数,它仍然可能是可控的。让我们尝试一个简单的递归。首先,让我们实际构建输出而不是计算输出,以便仔细检查我们做的事情是否正确。

    这个版本基于这样的想法,我们可以设置列表的第一个数字,跟踪该值作为剩余元素的最小值,然后重复剩余位置。最后,我们用更高的初始数字再试一次。除了nk 输入之外,我们还需要保留一个min 参数,我们从1 开始。

    这是一个版本:

    const f = (n, k, min = 1) => 
      k < 1 || n < k * min
        ? []
      : k == 1
        ? [[n]]
      : [
          ... f (n - min, k - 1, min) .map (xs => [min, ...xs]), 
          ... f (n, k, min + 1)
        ]
    
    console .log (
      f (8, 4) //~> [[1, 1, 1, 5], [1, 1, 2, 4], [1, 1, 3, 3], [1, 2, 2, 3], [2, 2, 2, 2]]
    )

    (您没有指定语言标签;如果 Javascript ES6 语法不清楚,我们可以改写成另一种风格。)

    既然这看起来是对的,我们可以写一个更简单的版本来计算结果:

    const f = (n, k, min = 1) => 
      k < 1 || n < k * min
        ? 0
      : k == 1
        ? 1
      : f (n - min, k - 1, min) + f (n, k, min + 1)
    
    console .log (
      f (8, 4) //~> 5
    )

    但如果我们要尝试更大的集合,比如 f(1000, 10)(根据检查,应该是 8867456966532531),计算可能需要一些时间。我们的算法可能是指数级的。因此,我们可以通过两种方式进行动态规划。最明显的是自下而上的方法:

    const f = (_n, _k, _min = 1) => {
      const cache = {}
      for (let n = 1; n <= _n; n ++) {
        for (let k = 1; k <= Math.min(_k, n); k++) {
          for (let min = n; min >= 0; min--) {
            cache [n] = cache[n] || {}
            cache [n] [k] = cache [n] [k] || {}
            cache [n] [k] [min] = 
              k < 1 || n < k * min
                ? 0
                : k == 1
                   ? 1
                   : cache [n - min] [k - 1] [min]  + cache [n] [k] [min + 1]
          }
        }
      }
      return cache [_n] [_k] [_min]
    }
    
    console.time('time taken')
    console .log (
      f (1000, 10) //~> 886745696653253
    )
    console.timeEnd('time taken')

    如果没有其他原因,在这里找出正确的边界是很棘手的,因为递归是基于min 的递增值。我们很可能在计算过程中不需要的东西。

    这也是丑陋的代码,失去了原始的优雅和可读性,而只获得了性能。

    我们仍然可以通过记忆我们的函数来保持优雅;这是自上而下的方法。通过使用可重用的memoize 函数,我们可以几乎完整地使用我们的递归解决方案:

    const memoize = (makeKey, fn) => {
      const cache = {}
      return (...args) => {
        const key = makeKey(...args)
        return cache[key] || (cache[key] = fn(...args))
      }
    }
    
    const makeKey = (n, k, min) => `${n}-${k}-${min}`        
            
    const f = memoize(makeKey, (n, k, min = 1) => 
      k < 1 || n < k * min
        ? 0
      : k == 1
        ? 1
      : f (n - min, k - 1, min)  + f (n, k, min + 1)
    )
    
    console.time('time taken')
    console .log (
      f (1000, 10) //~> 886745696653253
    )
    console.timeEnd('time taken')

    memoize 将在每次调用时计算其结果的函数转换为仅在第一次看到特定输入集时才计算结果的函数。此版本要求您提供一个附加函数,将参数转换为唯一键。还有其他的写法,但它们有点难看。这里我们只是将(8, 4, 1) 转换为"8-4-1",然后将结果存储在该键下。没有歧义。下次我们用(8, 4, 1)调用时,已经计算好的结果会立即从缓存中返回。

    注意有尝试的诱惑

    const f = (...args) => {...}
    const g = memoize(createKey, f)
    

    但这不起作用,如果f 中的递归调用指向f。如果他们指向g,我们已经混淆了实现,f 不再是独立的,所以没有什么理由。因此我们将其写为memomize(createKey, (...args) =&gt; {...})offer alternatives 的高级技术不在此处讨论范围。


    在自下而上的 DP 和自上而下的 DP 之间做出决定是一个复杂的问题。您将在上面的案例中看到,对于给定的输入,自下而上的版本运行得更快。附加函数调用有一些递归开销,在某些情况下,您可能会受到递归深度限制。但这有时完全被自上而下的技术所抵消,只计算你需要的东西。自下而上将计算所有较小的输入(对于“较小”的某些定义)以找到您的值。自上而下只会计算解决问题所必需的值。


    1 开个玩笑!我是在使用动态规划后才发现的。

    【讨论】:

    • 全面!我创建的一个小模块DeepMap,非常适合在无法使用${n}-${k}-${min} 等字符串化的情况下记忆复合数据。
    • console.timeconsole.timeEnd 也有助于降低基准演示中的噪音^^
    • @user633183:谢谢。更新为使用console.time/.timeEnd
    • @user633183:DeepMap 非常好。在 Ramda(的前身)的 initial version of memoize 中,同样的想法有一个不太复杂的版本。但是使用 Objects 而不是 Maps,它主要限于 String 和 Number 键。
    • (顺便说一下,我的自上而下和自下而上的versions 似乎都快得多。)
    【解决方案3】:

    从示例中,我假设没有任何组可以为空。还假设 n,k

    dp 状态将为remaining Objectsremaining Groupsf(remObject,remGroup) 将是把remObject 放入remGroup 的方法数,其中没有一个组的对象比以前形成的组少。

    我们将考虑 2 个案例。

    如果我们想把一个对象放在最左边的组中,我们还需要把一个对象放到所有其他组中。所以我们必须确保remaining Objects &gt;= remaining Groups。在这种情况下,我们会将f(remObject - remGroup, remGroup) 添加到我们的答案中。

    如果我们不想再将任何对象放在最左边的组中,我们将在答案中添加f(remObject,remGroup - 1)

    基本情况是没有任何组需要考虑并且所有对象都已放置。

    由于任何组都不能为空,因此在调用我们的 dp 之前,我们会将 1 个对象放入所有 k 个组中。

    查看代码了解更多详情。

    #define mxn 1003
    #define i64 long long int
    #define mod 1000000007
    
    i64 dp[mxn][mxn];
    
    i64 f(int remObject,int remGroup) {
            if(!remGroup) {
                    if(!remObject)
                            return 1;
                    return 0;
            }
    
            if(dp[remObject][remGroup] != -1)
                    return dp[remObject][remGroup];
    
            i64 ans = 0;
            if(remObject >= remGroup)
                    ans += f(remObject - remGroup, remGroup);
            ans += f(remObject,remGroup - 1);
            ans %= mod;
    
             return dp[remObject][remGroup] = ans;
    }
    
    int main()
    {
            int t,n,k;
            memset(dp,-1,sizeof dp);
            cin >> t;
            while(t--) {
                    cin >> n >> k;
                    if(n < k)
                            cout << 0 << endl;
                    else
                            cout << f(n-k,k) << endl;
            }
            return 0;
    }
    

    【讨论】:

      【解决方案4】:

      可以通过添加一些检查来进一步改进记忆的解决方案。如果 n,k 相等,则答案为 1。我们不需要对 1000,1000 进行递归。 k 也是 1,无论 n 是多少,答案都是 1。1000,1 是 1,这样可以节省内存和时间。 更新代码:由于声誉低,无法将此作为评论添加到上述解决方案中,抱歉。你也可以在这里找到简单的解释: N into K groups recursion

      function f(n, k, memo = {}) {
          if (k == 0 && n == 0) return 1;
          if (k == 1 && n != 0) return 1; //when k is 1 no matter what n is
          if (n == k) return 1; // when k and n are equal.
      
          if (n <= 0 || k <= 0) return 0;
      
          let key = String([n, k]); // Thanks to comment by user633183
      
          if (memo.hasOwnProperty(key)) return memo[key];
      
      
          return (memo[key] = f(n - k, k, memo) + f(n - 1, k - 1, memo));
      }
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-27
      • 2018-06-16
      • 1970-01-01
      • 1970-01-01
      • 2012-03-04
      • 1970-01-01
      • 2020-11-04
      • 1970-01-01
      相关资源
      最近更新 更多