带限制的整数划分问题(限制每个部分至少为某个值且最多使用k个部分)
字数 6895 2025-12-18 07:26:21

带限制的整数划分问题(限制每个部分至少为某个值且最多使用k个部分)

题目描述

给定一个正整数 n,以及两个额外的限制条件:

  1. 每个划分出来的正整数(即“部分”)必须至少minVal(一个给定的正整数)。
  2. 最多使用 k 个部分(即划分成的正整数个数不能超过 k)。

请计算,将整数 n 划分成若干个正整数之和,并且满足上述两个限制条件的不同划分方案的数量。注意,划分是有顺序的,即 1+22+1 被视为两种不同的划分(这通常称为“有序划分”或“composition”)。

示例

  • 输入:n = 5, minVal = 1, k = 3
  • 输出:10
  • 解释:满足条件的(有序)划分为:
    [1,1,3], [1,2,2], [1,3,1], [2,1,2], [2,2,1], [3,1,1],
    [1,4], [4,1], [2,3], [3,2]
    (注意 [5] 也是1个部分,但它只用了1个部分,未超过 k=3 的限制,同时 5 >= minVal=1,因此也是有效的。所以总共有 10 种。实际上,我们列举了使用1个、2个、3个部分的所有可能。)

解题思路

这个问题可以被看作是一个线性动态规划问题,因为它涉及到在构建一个序列(划分序列)时,对总和的约束、对序列长度的约束以及对每个元素最小值的约束。我们可以从最基础的“有序划分”计数问题出发,逐步引入限制条件。

基础模型回顾
不考虑 minValk 时,将整数 n 有序划分成正整数的方案数是经典的组合问题,答案是 2^(n-1)。但我们这里需要用动态规划来求解,以便能融入限制。

核心定义
我们定义 dp[i][j] 为:使用恰好 j 个部分(正整数),并且这 j 个数的总和为 i有序划分方案数。

  • i 的范围是从 0n(总和)。
  • j 的范围是从 0k(部分数量)。

初始状态

  • dp[0][0] = 1:总和为0,使用0个部分,有一种方案(空划分)。这是动态规划的起点。
  • 对于其他情况,当 i = 0j > 0 时,dp[0][j] = 0。因为没有正数相加能得到总和0(除了空划分)。

状态转移方程
如何从较小的状态推导出 dp[i][j]
考虑我们构建一个总和为 i、有 j 个部分的划分。最后一个部分(第 j 个数字)可以是多少?
由于每个部分必须至少为 minVal,所以最后一个部分 last 的取值范围是:minVal <= last <= i(因为总和是 i,并且前面已经有 j-1 个部分)。
如果我们固定最后一个部分是 last,那么剩下的问题就是:总和为 i-last,使用 j-1 个部分的方案数,即 dp[i-last][j-1]
因此,我们需要遍历所有可能的 last,并求和:
dp[i][j] = sum(dp[i-last][j-1]),其中 lastminVali

这个转移方程的含义是:要得到总和 ij 个部分划分,我们可以从所有总和为 i - xj-1 个部分划分方案后面,加上一个大小为 xx >= minVal)的部分得到。

最终答案
由于题目要求“最多使用 k 个部分”,所以最终答案不是 dp[n][k],而是所有使用 1 个部分到 k 个部分的方案数之和。即:
ans = dp[n][1] + dp[n][2] + ... + dp[n][k]

详细步骤与示例演算

我们以 n=5, minVal=1, k=3 为例,手动计算一下动态规划表,验证输出是否为10。

  1. 初始化
    创建 dp 表,大小为 (n+1) x (k+1),即 6 x 4
    dp[0][0] = 1,其余为0。

    i\j 0 1 2 3
    0 1 0 0 0
    1 0
    2 0
    3 0
    4 0
    5 0
  2. i 从小到大,j 从小到大计算

    • 计算 i=1:
      • j=1: dp[1][1] = sum(dp[1-last][0]), lastminVal=11。只有 last=1
        dp[1-1][0] = dp[0][0] = 1。所以 dp[1][1] = 1
      • j=2,3: i=1 无法被分成2个或3个都 >=1 的数,所以为0。
    i\j 0 1 2 3
    0 1 0 0 0
    1 0 1 0 0
    2 0
    3 0
    4 0
    5 0
    • 计算 i=2:
      • j=1: last 从1到2。
        dp[2-1][0] + dp[2-2][0] = dp[1][0] + dp[0][0] = 0 + 1 = 1
        所以 dp[2][1] = 1(划分: [2])。
      • j=2: last 从1到2。
        dp[2-1][1] + dp[2-2][1] = dp[1][1] + dp[0][1] = 1 + 0 = 1
        所以 dp[2][2] = 1(划分: [1,1])。
      • j=3: 不可能,为0。
    i\j 0 1 2 3
    0 1 0 0 0
    1 0 1 0 0
    2 0 1 1 0
    3 0
    4 0
    5 0
    • 计算 i=3:
      • j=1: last 从1到3。dp[2][0] + dp[1][0] + dp[0][0] = 0+0+1 = 1。([3])
      • j=2: last 从1到3。dp[2][1] + dp[1][1] + dp[0][1] = 1 + 1 + 0 = 2。划分: (last=1时,前面是[2] -> [2,1]; last=2时,前面是[1] -> [1,2]last=3时,dp[0][1]=0无效)
      • j=3: last 从1到3。dp[2][2] + dp[1][2] + dp[0][2] = 1 + 0 + 0 = 1。划分: (last=1时,前面是[1,1] -> [1,1,1])
    i\j 0 1 2 3
    0 1 0 0 0
    1 0 1 0 0
    2 0 1 1 0
    3 0 1 2 1
    4 0
    5 0
    • 计算 i=4:
      • j=1: last从1到4。dp[3][0] + dp[2][0] + dp[1][0] + dp[0][0] = 0+0+0+1=1 ([4])
      • j=2: last从1到4。dp[3][1] + dp[2][1] + dp[1][1] + dp[0][1] = 1+1+1+0=3。([3,1], [2,2], [1,3])
      • j=3: last从1到4。dp[3][2] + dp[2][2] + dp[1][2] + dp[0][2] = 2+1+0+0=3。([2,1,1], [1,2,1], [1,1,2])
    i\j 0 1 2 3
    0 1 0 0 0
    1 0 1 0 0
    2 0 1 1 0
    3 0 1 2 1
    4 0 1 3 3
    5 0
    • 计算 i=5:
      • j=1: last从1到5。dp[4][0] + dp[3][0] + dp[2][0] + dp[1][0] + dp[0][0] = 0+0+0+0+1=1 ([5])
      • j=2: last从1到5。dp[4][1] + dp[3][1] + dp[2][1] + dp[1][1] + dp[0][1] = 1+1+1+1+0=4。([4,1], [3,2], [2,3], [1,4])
      • j=3: last从1到5。dp[4][2] + dp[3][2] + dp[2][2] + dp[1][2] + dp[0][2] = 3+2+1+0+0=6。([3,1,1], [2,2,1], [2,1,2], [1,3,1], [1,2,2], [1,1,3])
    i\j 0 1 2 3
    0 1 0 0 0
    1 0 1 0 0
    2 0 1 1 0
    3 0 1 2 1
    4 0 1 3 3
    5 0 1 4 6
  3. 计算最终答案
    对于 n=5,我们累加 j=1k=3 的部分:
    ans = dp[5][1] + dp[5][2] + dp[5][3] = 1 + 4 + 6 = 11

    等等,我们之前说答案是10,怎么算出11?我们检查一下。
    回忆一下我们的初始条件:dp[0][0] = 1。这意味着“空划分”被视为一个方案。在我们 lastminVal 开始遍历时,对于 j=1 的情况,当 last = i 时,我们会用到 dp[0][0]。这确实是对的,因为一个单独的部分 i 本身就是一个有效的划分。
    在我们的例子中,dp[5][1] = 1 对应划分 [5]dp[5][2]dp[5][3] 的计算也依赖于更低 idp 值,而这些值又都依赖于 dp[0][0]
    那么,我们的列举中是否漏掉了某个划分?让我们重新列举 n=5, minVal=1, k=3 的所有有效有序划分:
    1部分: [5]
    2部分: [1,4], [2,3], [3,2], [4,1] (4种)
    3部分: [1,1,3], [1,2,2], [1,3,1], [2,1,2], [2,2,1], [3,1,1] (6种)
    总数为 1 + 4 + 6 = 11。而我们最初例子中给出的答案是10,并且列举了10种。我发现最初的列举漏掉了 [5]!所以正确答案应该是 11。原始示例的描述有误(或者我最初理解有误)。根据我们动态规划的定义和计算,答案是11。

因此,动态规划的计算结果是正确的

算法总结

  1. 定义状态dp[i][j] 表示总和为 i,使用恰好 j 个部分(每个部分 >= minVal)的有序划分方案数。
  2. 初始化dp[0][0] = 1,其他为0。
  3. 状态转移:对于每个 i1n,对于每个 j1k,计算:
    dp[i][j] = Σ(dp[i - x][j - 1]),其中 xminVali
  4. 计算结果:答案为 Σ(dp[n][j]),其中 j1k

复杂度与优化

  • 时间复杂度:外层循环 i (O(n)),内层循环 j (O(k)),最内层求和需要遍历 x (O(n)),所以总复杂度为 O(n² * k)。对于较大的 nk,这可能会比较慢。
  • 空间复杂度:O(n * k)。

可能的优化

  • 对于内层的求和 Σ(dp[i - x][j - 1]),这实际上是一个区间和(xminVali)。我们可以使用前缀和技巧来优化。具体来说,在计算每一行 j 时,我们可以维护一个关于 i 的前缀和数组 prefixSum[i] = Σ(dp[i‘][j-1]) (i‘ 从0到i)。那么 Σ(dp[i - x][j - 1]) for x in [minVal, i] 就等于 prefixSum[i-minVal] - prefixSum[i - (i+1)]? 需要小心下标。
    更准确地说,对于固定的 j,我们要计算 dp[i][j] = sum_{x=minVal}^{i} dp[i-x][j-1]
    t = i - x,则当 x = minVal 时,t = i-minVal;当 x = i 时,t = 0
    所以求和变为 sum_{t=0}^{i-minVal} dp[t][j-1]。这正是 dp[·][j-1]0i-minVal 的前缀和。
    因此,我们可以预先计算出对于 j-1 这一列的前缀和数组 pre,其中 pre[t] = sum_{u=0}^{t} dp[u][j-1]。那么 dp[i][j] = pre[i - minVal]
  • 通过前缀和优化,可以将最内层循环的复杂度从 O(n) 降到 O(1),从而使总复杂度降至 O(n * k)。

变种与扩展

  • 无序划分:如果划分是无序的(即 1+22+1 视为同一种),这就是经典的“整数划分”问题。通常需要不同的状态定义,例如 dp[i][j] 表示总和为 i,最大部分不超过 j 的划分方案数。状态转移方程会有所不同。
  • 每个部分有最大值限制:除了最小值 minVal,可能还会限制每个部分不能超过 maxVal。这时只需要在求和时修改 x 的上限为 min(i, maxVal) 即可。
  • 完全背包视角:这个问题也可以被视为一个“完全背包”问题:背包容量为 n,有 n 种物品(重量分别为 1,2,...,n,但要求每种物品的重量至少为 minVal),每种物品可以拿无限次,求恰好装满背包物品总个数不超过 k 的方案数。这里的“有序”对应着背包问题中考虑物品顺序的变体(通常称为“排列数”)。标准的完全背包求排列数的状态转移是 dp[i] += dp[i-weight],这里多了一个维度 j 来记录物品个数。
带限制的整数划分问题(限制每个部分至少为某个值且最多使用k个部分) 题目描述 给定一个正整数 n ,以及两个额外的限制条件: 每个划分出来的正整数(即“部分”)必须 至少 为 minVal (一个给定的正整数)。 最多使用 k 个部分(即划分成的正整数个数不能超过 k )。 请计算,将整数 n 划分成若干个正整数之和,并且满足上述两个限制条件的不同划分方案的数量。注意,划分是有顺序的,即 1+2 和 2+1 被视为两种不同的划分(这通常称为“有序划分”或“composition”)。 示例 : 输入: n = 5, minVal = 1, k = 3 输出: 10 解释:满足条件的(有序)划分为: [1,1,3] , [1,2,2] , [1,3,1] , [2,1,2] , [2,2,1] , [3,1,1] , [1,4] , [4,1] , [2,3] , [3,2] (注意 [5] 也是1个部分,但它只用了1个部分,未超过 k=3 的限制,同时 5 >= minVal=1 ,因此也是有效的。所以总共有 10 种。实际上,我们列举了使用1个、2个、3个部分的所有可能。) 解题思路 这个问题可以被看作是一个线性动态规划问题,因为它涉及到在构建一个序列(划分序列)时,对总和的约束、对序列长度的约束以及对每个元素最小值的约束。我们可以从最基础的“有序划分”计数问题出发,逐步引入限制条件。 基础模型回顾 : 不考虑 minVal 和 k 时,将整数 n 有序划分成正整数的方案数是经典的组合问题,答案是 2^(n-1) 。但我们这里需要用动态规划来求解,以便能融入限制。 核心定义 : 我们定义 dp[i][j] 为:使用 恰好 j 个部分(正整数),并且这 j 个数的总和为 i 的 有序 划分方案数。 i 的范围是从 0 到 n (总和)。 j 的范围是从 0 到 k (部分数量)。 初始状态 : dp[0][0] = 1 :总和为0,使用0个部分,有一种方案(空划分)。这是动态规划的起点。 对于其他情况,当 i = 0 且 j > 0 时, dp[0][j] = 0 。因为没有正数相加能得到总和0(除了空划分)。 状态转移方程 : 如何从较小的状态推导出 dp[i][j] ? 考虑我们构建一个总和为 i 、有 j 个部分的划分。最后一个部分(第 j 个数字)可以是多少? 由于每个部分必须至少为 minVal ,所以最后一个部分 last 的取值范围是: minVal <= last <= i (因为总和是 i ,并且前面已经有 j-1 个部分)。 如果我们固定最后一个部分是 last ,那么剩下的问题就是:总和为 i-last ,使用 j-1 个部分的方案数,即 dp[i-last][j-1] 。 因此,我们需要遍历所有可能的 last ,并求和: dp[i][j] = sum(dp[i-last][j-1]) ,其中 last 从 minVal 到 i 。 这个转移方程的含义是:要得到总和 i 的 j 个部分划分,我们可以从所有总和为 i - x 的 j-1 个部分划分方案后面,加上一个大小为 x ( x >= minVal )的部分得到。 最终答案 : 由于题目要求“最多使用 k 个部分”,所以最终答案不是 dp[n][k] ,而是所有使用 1 个部分到 k 个部分的方案数之和。即: ans = dp[n][1] + dp[n][2] + ... + dp[n][k] 。 详细步骤与示例演算 我们以 n=5, minVal=1, k=3 为例,手动计算一下动态规划表,验证输出是否为10。 初始化 : 创建 dp 表,大小为 (n+1) x (k+1) ,即 6 x 4 。 dp[0][0] = 1 ,其余为0。 | i\j | 0 | 1 | 2 | 3 | |:----|:----|:----|:----|:----| | 0 | 1 | 0 | 0 | 0 | | 1 | 0 | | | | | 2 | 0 | | | | | 3 | 0 | | | | | 4 | 0 | | | | | 5 | 0 | | | | 按 i 从小到大, j 从小到大计算 : 计算 i=1 : j=1 : dp[1][1] = sum(dp[1-last][0]) , last 从 minVal=1 到 1 。只有 last=1 。 dp[1-1][0] = dp[0][0] = 1 。所以 dp[1][1] = 1 。 j=2,3 : i=1 无法被分成2个或3个都 >=1 的数,所以为0。 | i\j | 0 | 1 | 2 | 3 | |:----|:----|:----|:----|:----| | 0 | 1 | 0 | 0 | 0 | | 1 | 0 | 1 | 0 | 0 | | 2 | 0 | | | | | 3 | 0 | | | | | 4 | 0 | | | | | 5 | 0 | | | | 计算 i=2 : j=1 : last 从1到2。 dp[2-1][0] + dp[2-2][0] = dp[1][0] + dp[0][0] = 0 + 1 = 1 。 所以 dp[2][1] = 1 (划分: [2] )。 j=2 : last 从1到2。 dp[2-1][1] + dp[2-2][1] = dp[1][1] + dp[0][1] = 1 + 0 = 1 。 所以 dp[2][2] = 1 (划分: [1,1] )。 j=3 : 不可能,为0。 | i\j | 0 | 1 | 2 | 3 | |:----|:----|:----|:----|:----| | 0 | 1 | 0 | 0 | 0 | | 1 | 0 | 1 | 0 | 0 | | 2 | 0 | 1 | 1 | 0 | | 3 | 0 | | | | | 4 | 0 | | | | | 5 | 0 | | | | 计算 i=3 : j=1 : last 从1到3。 dp[2][0] + dp[1][0] + dp[0][0] = 0+0+1 = 1 。( [3] ) j=2 : last 从1到3。 dp[2][1] + dp[1][1] + dp[0][1] = 1 + 1 + 0 = 2 。划分: ( last=1 时,前面是 [2] -> [2,1] ; last=2 时,前面是 [1] -> [1,2] 。 last=3 时, dp[0][1]=0 无效) j=3 : last 从1到3。 dp[2][2] + dp[1][2] + dp[0][2] = 1 + 0 + 0 = 1 。划分: ( last=1 时,前面是 [1,1] -> [1,1,1] ) | i\j | 0 | 1 | 2 | 3 | |:----|:----|:----|:----|:----| | 0 | 1 | 0 | 0 | 0 | | 1 | 0 | 1 | 0 | 0 | | 2 | 0 | 1 | 1 | 0 | | 3 | 0 | 1 | 2 | 1 | | 4 | 0 | | | | | 5 | 0 | | | | 计算 i=4 : j=1 : last 从1到4。 dp[3][0] + dp[2][0] + dp[1][0] + dp[0][0] = 0+0+0+1=1 ( [4] ) j=2 : last 从1到4。 dp[3][1] + dp[2][1] + dp[1][1] + dp[0][1] = 1+1+1+0=3 。( [3,1] , [2,2] , [1,3] ) j=3 : last 从1到4。 dp[3][2] + dp[2][2] + dp[1][2] + dp[0][2] = 2+1+0+0=3 。( [2,1,1] , [1,2,1] , [1,1,2] ) | i\j | 0 | 1 | 2 | 3 | |:----|:----|:----|:----|:----| | 0 | 1 | 0 | 0 | 0 | | 1 | 0 | 1 | 0 | 0 | | 2 | 0 | 1 | 1 | 0 | | 3 | 0 | 1 | 2 | 1 | | 4 | 0 | 1 | 3 | 3 | | 5 | 0 | | | | 计算 i=5 : j=1 : last 从1到5。 dp[4][0] + dp[3][0] + dp[2][0] + dp[1][0] + dp[0][0] = 0+0+0+0+1=1 ( [5] ) j=2 : last 从1到5。 dp[4][1] + dp[3][1] + dp[2][1] + dp[1][1] + dp[0][1] = 1+1+1+1+0=4 。( [4,1] , [3,2] , [2,3] , [1,4] ) j=3 : last 从1到5。 dp[4][2] + dp[3][2] + dp[2][2] + dp[1][2] + dp[0][2] = 3+2+1+0+0=6 。( [3,1,1] , [2,2,1] , [2,1,2] , [1,3,1] , [1,2,2] , [1,1,3] ) | i\j | 0 | 1 | 2 | 3 | |:----|:----|:----|:----|:----| | 0 | 1 | 0 | 0 | 0 | | 1 | 0 | 1 | 0 | 0 | | 2 | 0 | 1 | 1 | 0 | | 3 | 0 | 1 | 2 | 1 | | 4 | 0 | 1 | 3 | 3 | | 5 | 0 | 1 | 4 | 6 | 计算最终答案 : 对于 n=5 ,我们累加 j=1 到 k=3 的部分: ans = dp[5][1] + dp[5][2] + dp[5][3] = 1 + 4 + 6 = 11 。 等等,我们之前说答案是10,怎么算出11?我们检查一下。 回忆一下我们的初始条件: dp[0][0] = 1 。这意味着“空划分”被视为一个方案。在我们 last 从 minVal 开始遍历时,对于 j=1 的情况,当 last = i 时,我们会用到 dp[0][0] 。这确实是对的,因为一个单独的部分 i 本身就是一个有效的划分。 在我们的例子中, dp[5][1] = 1 对应划分 [5] 。 dp[5][2] 和 dp[5][3] 的计算也依赖于更低 i 的 dp 值,而这些值又都依赖于 dp[0][0] 。 那么,我们的列举中是否漏掉了某个划分?让我们重新列举 n=5, minVal=1, k=3 的所有有效有序划分: 1部分: [5] 2部分: [1,4] , [2,3] , [3,2] , [4,1] (4种) 3部分: [1,1,3] , [1,2,2] , [1,3,1] , [2,1,2] , [2,2,1] , [3,1,1] (6种) 总数为 1 + 4 + 6 = 11 。而我们最初例子中给出的答案是10,并且列举了10种。我发现最初的列举 漏掉了 [5] !所以正确答案应该是 11 。原始示例的描述有误(或者我最初理解有误)。根据我们动态规划的定义和计算,答案是11。 因此,动态规划的计算结果是正确的 。 算法总结 定义状态 : dp[i][j] 表示总和为 i ,使用恰好 j 个部分(每个部分 >= minVal )的有序划分方案数。 初始化 : dp[0][0] = 1 ,其他为0。 状态转移 :对于每个 i 从 1 到 n ,对于每个 j 从 1 到 k ,计算: dp[i][j] = Σ(dp[i - x][j - 1]) ,其中 x 从 minVal 到 i 。 计算结果 :答案为 Σ(dp[n][j]) ,其中 j 从 1 到 k 。 复杂度与优化 时间复杂度 :外层循环 i (O(n)),内层循环 j (O(k)),最内层求和需要遍历 x (O(n)),所以总复杂度为 O(n² * k)。对于较大的 n 和 k ,这可能会比较慢。 空间复杂度 :O(n * k)。 可能的优化 : 对于内层的求和 Σ(dp[i - x][j - 1]) ,这实际上是一个区间和( x 从 minVal 到 i )。我们可以使用 前缀和 技巧来优化。具体来说,在计算每一行 j 时,我们可以维护一个关于 i 的前缀和数组 prefixSum[i] = Σ(dp[i‘][j-1]) (i‘ 从0到i)。那么 Σ(dp[i - x][j - 1]) for x in [ minVal, i] 就等于 prefixSum[i-minVal] - prefixSum[i - (i+1)] ? 需要小心下标。 更准确地说,对于固定的 j ,我们要计算 dp[i][j] = sum_{x=minVal}^{i} dp[i-x][j-1] 。 设 t = i - x ,则当 x = minVal 时, t = i-minVal ;当 x = i 时, t = 0 。 所以求和变为 sum_{t=0}^{i-minVal} dp[t][j-1] 。这正是 dp[·][j-1] 从 0 到 i-minVal 的前缀和。 因此,我们可以预先计算出对于 j-1 这一列的前缀和数组 pre ,其中 pre[t] = sum_{u=0}^{t} dp[u][j-1] 。那么 dp[i][j] = pre[i - minVal] 。 通过前缀和优化,可以将最内层循环的复杂度从 O(n) 降到 O(1),从而使总复杂度降至 O(n * k)。 变种与扩展 无序划分 :如果划分是无序的(即 1+2 和 2+1 视为同一种),这就是经典的“整数划分”问题。通常需要不同的状态定义,例如 dp[i][j] 表示总和为 i ,最大部分不超过 j 的划分方案数。状态转移方程会有所不同。 每个部分有最大值限制 :除了最小值 minVal ,可能还会限制每个部分不能超过 maxVal 。这时只需要在求和时修改 x 的上限为 min(i, maxVal) 即可。 完全背包视角 :这个问题也可以被视为一个“完全背包”问题:背包容量为 n ,有 n 种物品(重量分别为 1,2,...,n ,但要求每种物品的重量至少为 minVal ),每种物品可以拿无限次,求 恰好装满背包 且 物品总个数不超过 k 的方案数。这里的“有序”对应着背包问题中考虑物品顺序的变体(通常称为“排列数”)。标准的完全背包求排列数的状态转移是 dp[i] += dp[i-weight] ,这里多了一个维度 j 来记录物品个数。