CSDN 博客:一道经典的整数划分题——分弹珠
一、题目描述
这道题目是一道经典的整数划分问题,要求将 (M) 个弹珠分到 (N) 个盘子中,满足以下条件:
- 允许盘子为空。
- 两种分法被认为相同当且仅当分配的弹珠数量相同(不考虑顺序)。
例如,将 (7) 个弹珠分到 (3) 个盘子中的方法数是 (8)。如果不考虑盘子顺序,分法包括:
(
7
,
0
,
0
)
,
(
6
,
1
,
0
)
,
(
5
,
2
,
0
)
,
(
5
,
1
,
1
)
,
(
4
,
3
,
0
)
,
(
4
,
2
,
1
)
,
(
3
,
3
,
1
)
,
(
3
,
2
,
2
)
。
(7,0,0), (6,1,0), (5,2,0), (5,1,1), (4,3,0), (4,2,1), (3,3,1), (3,2,2)。
(7,0,0),(6,1,0),(5,2,0),(5,1,1),(4,3,0),(4,2,1),(3,3,1),(3,2,2)。
二、整数划分题的核心思想
整数划分是组合数学中的经典问题。整数划分问题的核心在于:
- 如果我们用 P ( m , n ) P(m, n) P(m,n)表示将 m m m个物品分到 n n n个容器中的分法数,那么可以利用递推关系解决问题。
递推公式推导
考虑将 M M M 个弹珠分到 N N N个盘子中:
- 如果我们规定 盘子 N N N为空,那么问题就转化为将 M M M个弹珠分到 N − 1 N-1 N−1 个盘子中,即 P ( m , n − 1 ) P(m, n-1) P(m,n−1)。
- 如果盘子 N N N 不为空,我们至少需要放一个弹珠,那么问题就转化为将剩下的 M − N M-N M−N个弹珠分到 N N N个盘子中,即 P ( m − n , n ) P(m-n, n) P(m−n,n)。
因此,递推公式为:
P
(
m
,
n
)
=
P
(
m
,
n
−
1
)
+
P
(
m
−
n
,
n
)
,
P(m, n) = P(m, n-1) + P(m-n, n),
P(m,n)=P(m,n−1)+P(m−n,n),
同时考虑到边界条件:
- 当 (m = 0) 时,只有一种分法,即所有盘子均为空:
P ( 0 , n ) = 1 P(0, n) = 1 P(0,n)=1 - 当 (n = 0) 且 (m > 0) 时,分法数为 (0):
P ( m , 0 ) = 0 P(m, 0) = 0 P(m,0)=0 - 当 (n > m) 时,盘子的数量大于弹珠数量,此时最多只需 (m) 个盘子,因此:
P ( m , n ) = P ( m , m ) P(m, n) = P(m, m) P(m,n)=P(m,m)
三、两种解法详解
方法一:带备忘录的递归
带备忘录的递归利用了记忆化搜索的思想,可以避免重复计算。代码如下:
#include <iostream>
#include <cstring>
using namespace std;
int memo[21][21]; // 备忘录,用于存储中间结果
// 递归计算 P(m, n)
int P(int m, int n) {
if (m == 0) return 1; // 边界条件:0个弹珠只有1种分法
if (m < 0 || n == 0) return 0; // 边界条件:盘子用完或弹珠数量负数
if (n > m) return P(m, m); // 若盘子数多于弹珠数,限制盘子数为m
if (memo[m][n] != -1) return memo[m][n]; // 如果已计算过,直接返回结果
memo[m][n] = P(m, n-1) + P(m-n, n); // 递推公式
return memo[m][n];
}
int main() {
memset(memo, -1, sizeof(memo)); // 初始化备忘录
int t, M, N;
while (cin >> t) {
for (int i = 0; i < t; i++) {
cin >> M >> N;
cout << P(M, N) << endl;
}
}
return 0;
}
解法分析
- 时间复杂度:由于每个状态 P ( m , n ) P(m, n) P(m,n)只计算一次,总的复杂度为 O ( M × N ) O(M \times N) O(M×N)。
- 空间复杂度:需要额外的 O ( M × N ) O(M \times N) O(M×N) 空间存储中间结果。
方法二:分治法(动态规划)
动态规划通过表格存储所有子问题的解,从而避免了递归调用的开销。代码如下:
#include <iostream>
using namespace std;
int dp[21][21]; // 动态规划数组
// 初始化动态规划表
void initialize_dp() {
for (int m = 0; m <= 20; m++) {
for (int n = 0; n <= 20; n++) {
if (m == 0)
dp[m][n] = 1; // 边界条件
else if (n == 0)
dp[m][n] = 0; // 边界条件
else if (n > m)
dp[m][n] = dp[m][m]; // 当盘子数多于弹珠数
else
dp[m][n] = dp[m][n-1] + dp[m-n][n]; // 递推公式
}
}
}
int main() {
initialize_dp(); // 初始化 DP 表
int t, M, N;
while (cin >> t) {
for (int i = 0; i < t; i++) {
cin >> M >> N;
cout << dp[M][N] << endl;
}
}
return 0;
}
解法分析
- 时间复杂度:预处理的复杂度为 O ( M × N ) O(M \times N) O(M×N),查询的复杂度为 O ( 1 ) O(1) O(1)。
- 空间复杂度:需要 O ( M × N ) O(M \times N) O(M×N) 的存储空间。
四、总结
本题作为整数划分问题的典型代表,考察了递归与动态规划的灵活运用:
- 递归方法(带备忘录)更容易直观理解,但需要注意栈的深度。
- 动态规划方法 是实际中更高效且稳定的选择,特别是在多组测试的情况下。
整数划分的核心在于掌握 递推公式 的推导,并灵活运用 边界条件 简化问题的求解。希望本篇博客能够帮助读者对整数划分问题有更深入的理解!