文章目录
题目描述与示例
题目描述
中秋节,公司分月饼,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
输出描述
输出有多少种月饼分法
示例
输入
Shell
2 4
输出
Shell
2
说明
分法有2
种:
4=1+3
4=2+2
注意: 1+3
和3+1
算一种分法
解题思路
用比较严谨的数学语言表达上述问题为:挑选出m
非递减的数,要求相邻两个数的差值不超过3
,这m
个数的和为n
,问一共有多少种挑选方式。
由于在挑选完第i
个数之后,第i+1
个数的取值和第i
个数的取值相关,很容易想到用动态规划来解决上述问题。思考动态规划三部曲:
dp
数组的含义是什么?
我们需要考虑三个因素,选择了第几个数,这个数取了什么数值,当前总和为多少 。因此需要构建一个三维的dp
数组。
考虑dp[i][j][k]
表示,第i
个数取值为j
时,前i
个数的总和为k
的方法数。
分别考虑i
,j
,k
三个数的取值范围来确定dp
数组的大小。
i
和k
的定义比较明确,范围分别是[1, m]
和[1, n]
。
每个数字的取值j
的最小为1
,最大的情况为m-1
个数字都选择最小数1
,剩余一个最大数选择n-(m-1) = n-m+1
。故j的取值范围是[1, n+m+1]
故构建dp
数组是一个大小为(m+1)*(n-m+2)*(n+1)
的三维数组。
- 动态转移方程是什么?
假设第i
个数的取值为j
,那么第i+1
个数只能在j
,j+1
,j+2
,j+3
中进行挑选。
若此时前i
个数的总和为k
,那么当第i+1
个数
- 取了
j
时,前i+1
个数的总和为k+j
。存在dp[i+1][j][k+j] += dp[i][j][k]
- 取了
j+1
时,前i+1
个数的总和为k+j+1
。存在dp[i+1][j+1][k+j+1] += dp[i][j][k]
- 取了
j+2
时,前i+1
个数的总和为k+j+2
。存在dp[i+1][j+2][k+j+2] += dp[i][j][k]
- 取了
j+3
时,前i+1
个数的总和为k+j+3
。存在dp[i+1][j+3][k+j+3] += dp[i][j][k]
上述四个式子可以合并为一个式子,即dp[i+1][j+d][k+j+d] += dp[i][j][k]
,其中d
的取值为[0,3]
先从小到大遍历i
,再从小到大遍历j
,再从小到大遍历k
,则代码如下
python
for i in range(1, m):
for j in range(1, n-m+2):
for k in range(i, n+1):
for d in range(4):
if j+d < n-m+2 and k+j+d < n+1:
dp[i+1][j+d][k+j+d] += dp[i][j][k]
dp
数组如何初始化?
i = 0
没有实际意义,不考虑。
考虑i = 1
的情况,第1
个数字的取值j
最小为1
,最大为n // m
(即所有数字尽可能接近的情况),即此时j
的取值为[1, n // m]
。
同时,由于只选择了一个数字,因此此时前i
个数字的总和k = j
故对于i = 1
,做如下初始化
python
dp = [[[0] * (n+1) for j in range(n-m+2)] for i in range(m+1)]
for j in range(1, n//m+1):
dp[1][j][j] = 1
dp[1][j][j] = 1
表示只对应1
种方法数。
代码
Python
python
# 题目:【DP】2023C-分月饼
# 分值:200
# 作者:许老师-闭着眼睛学数理化
# 算法:DP
# 代码看不懂的地方,请直接在群上提问
# 输入员工人数m,月饼总数n
m, n = map(int, input().split())
# dp数组是一个大小为(m+1)*(n-m+2)*(n+1)的三维数组
# dp[i][j][k]表示,第i个数取值为j时,前i个数的总和为k的方法数
dp = [[[0] * (n+1) for j in range(n-m+2)] for i in range(m+1)]
# 第1个数字选了j,此时总和为j
# j的取值范围是[1, n//m]
# 因为m个数的和需要为n,那么最小那个数的最大值是n//m
for j in range(1, n//m+1):
dp[1][j][j] = 1
# i的最大取值为m-1
for i in range(1, m):
# j的最小取值为1,最大取值为n-m+1
for j in range(1, n-m+2):
# k的最小取值为i(前i个数都选了1,和为i),最大取值为n
for k in range(i, n+1):
# 增量d的取值范围为0,1,2,3
for d in range(4):
# 条件为j+d和k+j+d都没有超过对应的最大范围
if j+d < n-m+2 and k+j+d < n+1:
dp[i+1][j+d][k+j+d] += dp[i][j][k]
ans = 0
# dp[m][j][n]表示第m个数(最后一个数)选了j后,总和为n的方法数
# 将所有的dp[m][j][n]加在一起即为答案
for j in range(n-m+2):
ans += dp[m][j][n]
print(ans)
Java
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt();
int n = scanner.nextInt();
// dp array initialization
int[][][] dp = new int[m + 1][n - m + 2][n + 1];
for (int j = 1; j <= n / m; j++) {
dp[1][j][j] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j <= n - m + 1; j++) {
for (int k = i; k <= n; k++) {
for (int d = 0; d < 4; d++) {
if (j + d < n - m + 2 && k + j + d <= n) {
dp[i + 1][j + d][k + j + d] += dp[i][j][k];
}
}
}
}
}
int ans = 0;
for (int j = 1; j <= n - m + 1; j++) {
ans += dp[m][j][n];
}
System.out.println(ans);
}
}
C++
cpp
#include <iostream>
using namespace std;
int main() {
int m, n;
cin >> m >> n;
// dp array initialization
int dp[m + 1][n - m + 2][n + 1] = {0};
for (int j = 1; j <= n / m; j++) {
dp[1][j][j] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j <= n - m + 1; j++) {
for (int k = i; k <= n; k++) {
for (int d = 0; d < 4; d++) {
if (j + d < n - m + 2 && k + j + d <= n) {
dp[i + 1][j + d][k + j + d] += dp[i][j][k];
}
}
}
}
}
int ans = 0;
for (int j = 1; j <= n - m + 1; j++) {
ans += dp[m][j][n];
}
cout << ans << endl;
return 0;
}
时空复杂度
时间复杂度:O(NM(N-M))
。三重循环所需时间复杂度。
空间复杂度:O(NM(N-M))
。三维dp
数组所需空间。
华为OD算法/大厂面试高频题算法练习冲刺训练
-
华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!
-
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
-
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
-
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
-
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
-
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
-
绿色聊天软件戳
od1336
了解更多