C++ 中的笔分配问题

2025年5月13日 | 阅读 15 分钟

引言

笔分配问题 是一个经典的组合学问题,出现在计算机程序设计竞赛、离散数学和计算机科学中。它是如何将现实生活中的场景进行数学建模的一个绝佳范例。它涉及到在满足某些规则和限制的条件下,将固定数量的相同物品(笔)分配给一群人。

组合学:这是计算分配物品所有可能方式的问题。有许多“组合学”的组合技术,如组合、排列和星星与杠方法。

不可区分的物品:笔是不可区分的(相同的物品),而接收它们的人是可区分的(不同的个体)。这种区别很重要,因为解决问题的方式不同。

约束和变体:改变问题解决方案性质的约束条件有所不同。

无限制分配:如果我们“丢失”任意数量的笔,每个人都可以收到该数量的笔(包括零)。

每人至少一支笔:每支笔都必须分发给至少一个人。

最多,我们每人只能给一支笔。我们每人只能给一支笔(或者如果你成对,可以给两支)。

了解这些约束条件将有助于我们确定哪种方法最适合每种数学变体。

方法一:蛮力法

蛮力 方法是解决笔分配问题的最直接方法,无论是第一次学习问题还是处理输入规模较小的情况。这意味着不使用任何特定的数学公式来分配笔给人们,而是系统地创建所有分配笔给人们的方式。在蛮力技术中,对所有情况进行穷举搜索。遵循问题的约束条件,探索、检查和计数每种笔分配的组合。

示例

让我们举一个例子,说明在 C++ 中使用蛮力法来解决笔分配问题

输出

 
Enter the Number of pens (n): 10
Enter the Number of people (k): 3
--- Unrestricted Distribution ---
0 0 10 
0 1 9 
0 2 8 
0 3 7 
0 4 6 
0 5 5 
0 6 4 
0 7 3 
0 8 2 
0 9 1 
0 10 
1 0 9 
1 1 8 
1 2 7 
1 3 6 
1 4 5 
1 5 4 
1 6 3 
1 7 2 
1 8 1 
1 9 
2 0 8 
2 1 7 
2 2 6 
2 3 5 
2 4 4 
2 5 3 
2 6 2 
2 7 1 
2 8 
3 0 7 
3 1 6 
3 2 5 
3 3 4 
3 4 3 
3 5 2 
3 6 1 
3 7 
4 0 6 
4 1 5 
4 2 4 
4 3 3 
4 4 2 
4 5 1 
4 6 
5 0 5 
5 1 4 
5 2 3 
5 3 2 
5 4 1 
5 5 
6 0 4 
6 1 3 
6 2 2 
6 3 1 
6 4 
7 0 3 
7 1 2 
7 2 1 
7 3 
8 0 2 
8 1 1 
8 2 
9 0 1 
9 1 
10 
Total ways (Unrestricted): 66
--- At Least One Pen per Person ---
1 1 8 
1 2 7 
1 3 6 
1 4 5 
1 5 4 
1 6 3 
1 7 2 
1 8 1 
2 1 7 
2 2 6 
2 3 5 
2 4 4 
2 5 3 
2 6 2 
2 7 1 
3 1 6 
3 2 5 
3 3 4 
3 4 3 
3 5 2 
3 6 1 
4 1 5 
4 2 4 
4 3 3 
4 4 2 
4 5 1 
5 1 4 
5 2 3 
5 3 2 
5 4 1 
6 1 3 
6 2 2 
6 3 1 
7 1 2 
7 2 1 
8 1 1 
Total ways (At Least One Pen): 36
--- At Most One Pen per Person ---
Too many pens for each person to get at most one.
--- Equal Distribution ---
Cannot equally distribute pens among people.
--- Distribution with Maximum Limit per Person ---
Enter the maximum Number of pens a person can receive: 1
Total ways (Max Limit 1 pens per person): 0   

说明

该代码为根据不同的分配规则,将 n 支笔分配给 (k) 个人提供了蛮力解决方案。每个辅助函数都使用递归和回溯来解决问题的不同变体。

  • countWaysUnrestricted:无限制地分配笔。它递归地探索所有可能性,为每个人分配 0 到 n 之间的笔。
  • countWaysAtLeastOne:确保每个人至少得到一支笔。每个人至少得到一支笔,剩余的笔进行分配,这样就没有人得到零支笔。
  • countWaysAtMostOne:总笔数最多为 n,每个人最多只能获得一支笔,因此确保 (k) 大于或等于 n。
  • countWaysEqualDistribution:每个人获得相等数量的笔(如果可能,则为 n 除以 k)。如果不可能均等分配,则输出无有效配置。否则,将均等分配。
  • countWaysWithMaxLimit:限制每个人可以拥有的笔的数量。此限制将分配限制在最大数量以内,从而减少了配置的数量。

复杂度分析

时间复杂度

无限制分配 (countWaysUnrestricted)

  • 该函数尝试以所有可能的方式将 n 支笔分配给 k 个人。
  • 它对每个人进行检查,看每个人分配了多少支笔(从 0 到 n),以及 k 个人中的每个人有多少种选择(O(n))。
  • 总体而言,它的时间复杂度为 O(n k)。
  • 该函数探索所有可能的组合,其复杂性呈指数级增长。对于较大的 n 和 k 值,它变得不切实际。

每人至少一支笔 (countWaysAtLeastOne)

  • 在这里,约束条件减小了问题规模,因此每个人都应该至少获得一支笔。
  • 我们将问题简化为将 n-k 支笔分配给 k 个人(最初,每个人都得到了一支笔)。
  • 其时间复杂度与 O(n-k) 相同。它比无限制的情况好一点,但对于大的 n 和 k 来说会变得非常糟糕。

每人最多一支笔 (countWaysAtMostOne)

  • 这种变体唯一有效的方式是,每个人只能得到 0 或 1 支笔。
  • 我们通过从 k 个人中选择 n 个人来确定有效配置的数量(即二项式系数 (k n))。最坏情况下的时间复杂度为 O(2k ),因为需要探索每个可能的“可以接收笔的人”的子集。
  • 它的时间复杂度为 O(k),因为我们只需要分配笔并打印结果。

带最大限制 (countWaysWithMaxLimit)

  • 它的工作原理与无限制情况相同,只是每个人最多可以获得 maxPens 支笔。其复杂度为 O(min),用于减少每个人的选择数量。

空间复杂度

所有函数的空间复杂度

  • 为了存储当前配置,所有函数都使用大小为 k 的向量(Distribution)。空间复杂度为 O(k)。
  • 递归调用堆栈中使用的辅助空间也可以达到 k 的深度,因此总辅助空间为 O(k)。

方法二:动态规划

动态规划 (DP) 可以为我们提供更有效的解决方案。DP 方法通过重叠子问题来解决问题,将问题分解成小的子问题,并存储这些子问题的结果以避免重复计算。例如,我们在无限制分配中使用“星星与杠”方法,目标是将 n 个相同的物品分成 k 个箱子。DP 有助于优化此过程,从而显著降低时间复杂度。通过使用此方法,我们可以在合理的时间内解决具有更大输入规模的问题。

示例

让我们举一个例子,说明在 C++ 中使用动态规划来解决笔分配问题

输出

 
Enter the Number of pens (n): 10
Enter the Number of people (k): 3
--- Unrestricted Distribution (DP) ---
Total ways (Unrestricted): 66
--- At Least One Pen per Person (DP) ---
Total ways (At Least One Pen): 36
--- At Most One Pen per Person (Combination) ---
Total ways (At Most One Pen): 0
--- Equal Distribution (DP) ---
Total ways (Equal Distribution): 0
Enter the maximum Number of pens a person can receive: 1
--- Distribution with Maximum Limit per Person (DP) ---
Total ways (Max Limit): 0   

说明

countWaysUnrestrictedDP

  • 此函数处理 n 支笔在 k 个人之间的无限制分配。
  • 我们使用一个 DP 表 dp[i][j],其中 i 表示人数,j 表示笔的数量。我们的基本情况是 dp[0][0] = 1,这意味着有 1 种方法可以将 0 支笔分配给 0 个人。
  • 递推关系 dp[i][j] = dp[i-1][j] + dp[i][j-1] 考虑了当前这个人获得 0 支笔或至少 1 支笔的情况。其次,将 n 支笔分配给 k 个人的总方法数存储在结果 dp[k][n] 中。

countWaysAtLeastOneDP

  • 此函数解决了每个人都必须至少获得一支笔的情况。我们通过将剩余的 n-k 支笔自由分配给 k 个人来简化问题,首先为每个人分配一支笔。然后,我们使用 countWaysUnrestrictedDP 函数来附加方法,调用该函数使每个人至少获得一支初始笔。

countWaysAtMostOne

  • 如果一个人最多只能得到一支笔,这个函数就处理这种情况。我们通过选择不同的人来解决问题。这可以通过组合来完成,它表示从 k 个人中选择 n 个人的方式,即二项式系数 C(k,n)。
  • 该函数以迭代方式计算。

countWaysEqualDistribution

  • 此函数用于处理均等分配笔的情况。只有当 n / k 是整数时,即 n % k == 0,才可能实现。如果条件成立,则只有一种方法可以均等分配笔:每个人获得 n/k 支笔。

countWaysWithMaxLimit

  • 此函数用于解决笔分配问题,并限制每个人可以获得的笔的数量。
  • 给定这一点,该函数使用 DP 表 dp[i][j],其中 i 代表人数,j 代表使用的笔的数量。递推关系是针对一个人获得 0 到 maxPens 支笔的情况,其中有一个内部循环用于一个人可能获得的笔的不同数量。

复杂度分析

时间复杂度

无限制分配 (countWaysUnrestrictedDP)

这是因为我们要填充一个大小为 (k+1)×(n+1) 的 DP 表,该表根据前一个表中的值进行计算。我们计算每个人(在 1 到 k 之间)使用简单的递推关系分配最多 n 支笔的方式数量。因此,时间复杂度为 O(k⋅n)。

每人至少一支笔 (countWaysAtLeastOneDP)

我们首先给每个人分配 1 支笔,剩下 n-k 支笔。然后,问题被简化为对所有剩余的笔进行无限制分配,并使用 countWaysUnrestrictedDP 函数来计算。此步骤的最坏情况时间复杂度为 O(k⋅(n - k)),因为我们对这些剩余的笔进行了 DP 计算。

每人最多一支笔 (countWaysWithMaxLimit)

为了填充 DP 表,我们考虑到所有人都可能获得 0 到 maxPens 支笔。这导致了三个嵌套循环:两个用于人数,两个用于笔,以及一个内部循环用于可以分配笔的最大人数。因此,时间复杂度为 O(k ⋅n⋅maxPens)。

空间复杂度

无限制分配 (countWaysUnrestrictedDP)

DP 表是一个 (k+1)×(n+1) 的二维 数组,其中每个条目存储了将 j 支笔分配给 i 个人可能的方式数量。它直接导致空间复杂度为 O(k⋅n)。k 是人数,n 是笔的数量。

每人至少一支笔 (countWaysAtLeastOneDP)

因此,空间复杂度为 O(k⋅n)。该函数利用 countWaysUnrestrictedDP 函数,该函数使用 DP 表来计算在给每个人一支笔后,分配剩余 n-k 支笔的方式数量。

每人最多一支笔 (countWaysAtMostOne)

该函数以迭代方式计算二项式系数 C(k,n),在函数中使用了常量的变量。因此,空间复杂度为 O(1)。

每人最多一支笔 (countWaysWithMaxLimit)

在此函数中,DP 表与无限制分布情况下的类似,维度为 (k+1)×(n+1),因此空间复杂度为 O(k⋅n),其中 k 是人数,n 是笔的数量。