题目描述
中秋节,公司分月饼,m 个员工,买了 n 个月饼,m ≤ n,每个员工至少分 1 个月饼,但可以分多个,单人分到最多月饼的个数是 Max1 ,单人分到第二多月饼个数是 Max2 ,Max1 - Max2 ≤ 3 ,单人分到第 n - 1 多月饼个数是 Max(n-1),单人分到第 n 多月饼个数是 Max(n) ,Max(n-1) – Max(n) ≤ 3,问有多少种分月饼的方法?
输入描述
每一行输入m n,表示m个员工,n个月饼,m ≤ n
输出描述
输出有多少种月饼分法
用例1
输入
2 4
输出
2
说明:
分法有2种:
4 = 1 + 3
4 = 2 + 2
注意:1+3和3+1算一种分法*
用例2
输入
3 5
输出
2
说明
5 = 1 + 1 + 3
5 = 1 + 2 + 2
用例3
输入
3 12
输出
6
说明
满足要求的有6种分法:
12 = 1 + 1 + 10(Max1 = 10, Max2 = 1,不满足Max1 - Max2 ≤ 3要求)
12 = 1 + 2 + 9(Max1 = 9, Max2 = 2,不满足Max1 - Max2 ≤ 3要求)
12 = 1 + 3 + 8(Max1 = 8, Max2 = 3,不满足Max1 - Max2 ≤ 3要求)
12 = 1 + 4 + 7(Max1 = 7, Max2 = 4,Max3 = 1,满足要求)
12 = 1 + 5 + 6(Max1 = 6, Max2 = 5,Max3 = 1,不满足要求)
12 = 2 + 2 + 8(Max1 = 8, Max2 = 2,不满足要求)
12 = 2 + 3 + 7(Max1 = 7, Max2 = 3,不满足要求)
12 = 2 + 4 + 6(Max1 = 6, Max2 = 4,Max3 = 2,满足要求)
12 = 2 + 5 + 5(Max1 = 5, Max2 = 2,满足要求)
12 = 3 + 3 + 6(Max1 = 6, Max2 = 3,满足要求)
12 = 3 + 4 + 5(Max1 = 5, Max2 = 4,Max3 = 3,满足要求)
12 = 4 + 4 + 4(Max1 = 4,满足要求)
思考
暴力DFS+回溯,尝试各种分法。但是要注意1 + 3 和 3 + 1 这种重复的分发,怎么避免呢?我给第一个人分了 1 个,第二个人分 3 个,下次回溯到给第一个人分月饼之前的状态后再分配的时候怎么知道不应该给第一个人分3个?控制月饼分配的数量大小顺序,给第一个人分了 K 个饼,给第二个人分得的饼数量就不应该大于 K 个,通过控制分配的饼数量为非严格递减的顺序就不会出现 1 + 3 和 3 + 1 这种情况。首先从第 1 个人开始分月饼,第一个人可以分的剩余月饼数量remaining
等于所有的 n 个月饼,分多少没有严格限制,他前面没有其他人, dfs 函数第一个参数是员工索引 index
,第二参数是剩余可分的月饼数量 remaining
;第 2 个人分的月饼数量要不多于第一个人的且不多于剩余可分的月饼数量,与第一个相差不能大于3,但至少分一个, dfs 函数需要加第三个参数记录前一个分的月饼数量 prev
,第 2 个人可分的月份数量范围在: [Max(1,pre−3),Min(pre,remaining)] [Max(1, pre-3), Min(pre, remaining)] [Max(1,pre−3),Min(pre,remaining)]
回溯怎么处理?由于把每个人分的月饼数量都通过 dfs 函数形参 remaining
表达出来了,每次函数栈调用结束,对应的变化后的 remaining 变量也自动释放了,也就相当于回溯了,因此整个代码结构比较简洁。
算法过程
-
输入处理:
- 读取输入
m
(员工数)和n
(月饼总数),确保m ≤ n
。
- 读取输入
-
深度优先搜索(DFS):
- 递归函数
dfs(index, remaining, prev)
:index
:当前正在分配的第index
个员工(从 0 开始)。remaining
:剩余的月饼数量。prev
:前一个员工分配的月饼数(初始为Infinity
,表示第一个员工不受限制)。
- 终止条件:
- 当
index === m
且remaining === 0
时,说明分配完成,ans++
。
- 当
- 分配逻辑:
- 当前员工分配的月饼数
k
的范围为[start, end]
:start = max(1, prev - 3)
(确保与前一个员工的分配数差不超过 3)。end = min(prev, remaining)
(确保非递增且不超过剩余月饼数)。
- 递归调用
dfs(index + 1, remaining - k, k)
。
- 当前员工分配的月饼数
- 递归函数
-
初始调用:
dfs(0, n, Infinity)
:从第 0 个员工开始分配,剩余n
个月饼,前一个分配数为Infinity
(无限制)。
-
输出结果:
- 打印
ans
,即合法的分配方法数。
- 打印
时间复杂度分析
-
递归树规模:
- 最坏情况下,每个员工分配的月饼数有
O(n/m)
种可能(均匀分配时)。 - 递归深度为
m
(员工数),因此递归树规模为O((n/m)^m)
。
- 最坏情况下,每个员工分配的月饼数有
-
实际复杂度:
- 由于分配数受
prev
和prev - 3
限制,实际递归树规模远小于O((n/m)^m)
。 - 近似为 O(m · (n/m)^m)(指数级,但约束较强)。
- 由于分配数受
空间复杂度分析
-
递归栈空间:
- 递归深度为
m
,因此栈空间为 O(m)。
- 递归深度为
-
其他空间:
- 仅用常数空间存储
ans
和临时变量,因此额外空间为 O(1)。
- 仅用常数空间存储
参考代码
function solution() {
const [m, n] = readline().split(' ').map(Number);
let ans = 0;
// 从剩余的 remaining 个月饼中给第 index 个员工
const dfs = function(index, remaining, prev) {
if (index === m) {
if (remaining === 0) {
ans++;
}
return;
}
let start = 1;
if (index > 0) {
start = Math.max(1, prev - 3); // 当前分配的数量至少为 max(1, prev - 3)
}
const end = Math.min(prev, remaining); // 当前分配的数量不超过 prev 和 remaining
for (let k = start; k <= end; k++) {
dfs(index + 1, remaining - k, k);
}
};
dfs(0, n, Infinity); // 初始时 prev 设为 Infinity,这样第一个员工的分配不受限制
console.log(ans);
}
const cases = [
`2 4`, // 2
`3 5`, // 2
`3 12` // 6
];
let caseIndex = 0;
let lineIndex = 0;
const readline = (function () {
let lines = [];
return function () {
if (lineIndex === 0) {
lines = cases[caseIndex]
.trim()
.split("\n")
.map((line) => line.trim());
}
return lines[lineIndex++];
};
})();
cases.forEach((_, i) => {
caseIndex = i;
lineIndex = 0;
solution();
});