1. 问题描述:
7 月 17 日是 Mr.W 的生日,ACM-THU 为此要制作一个体积为 Nπ 的 M 层生日蛋糕,每层都是一个圆柱体。设从下往上数第 i 层蛋糕是半径为 Ri,高度为 Hi 的圆柱。当 i<M 时,要求 Ri>Ri+1 且 Hi>Hi+1。由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q 最小。令 Q=Sπ ,请编程对给出的 N 和 M,找出蛋糕的制作方案(适当的 Ri 和 Hi 的值),使 S 最小。除 Q 外,以上所有数据皆为正整数。
输入格式
输入包含两行,第一行为整数 N,表示待制作的蛋糕的体积为 Nπ。第二行为整数 M,表示蛋糕的层数为 M。
输出格式
输出仅一行,是一个正整数 S(若无解则 S=0)。
数据范围
1 ≤ N ≤ 10000,
1 ≤ M ≤ 20
输入样例:
100
2
输出样例:
68
来源:https://www.acwing.com/problem/content/170/
2. 思路分析:
分析题目可以知道我们需要在满足体积为Nπ,高度为M,下一层的蛋糕高度与半径都大于上一层蛋糕的高度与半径的前提下找到对应的蛋糕制作方案,也即需要确定每一层蛋糕的高度与半径,使得最终蛋糕的外表面积是最小的,所以我们需要枚举出所有可能的蛋糕制作方案找到最优解使得S最小,这道题目属于dfs剪枝的经典题目。首先我们需要想一个搜索顺序使得可以枚举出所有的答案,我们可以一层一层自底向上搜索,枚举每一层蛋糕可能的高度与直径,这样搜索一定可以搜索出所有的方案,第二个需要考虑的问题对dfs进行剪枝优化:首先我们需要优化搜索顺序,优先搜索分支数目较少的节点,① 我们可以自底向上进行搜索,在枚举蛋糕高度与半径的时候需要使得体积和表面积越大这样后面可以选择的分支数目就比较少,因为半径是平方级别的,所以可以先枚举半径然后再枚举高度,枚举的时候从大到小进行枚举;② 枚举的每一层蛋糕的高度与半径都是有一定范围的,我们需要推导一下r和h的范围,使用R[u]和H[u]来表示第u层蛋糕的半径与高度,R(u) <= R(u + 1),H(u) <= H(u + 1),因为每一层半径与高度是逐层增加的,所以需要满足u <= R(u) <= min{R(u + 1) - 1, √n - v},u <= H(u) <= min{H(u + 1) - 1, (n - v) / (r * r)},其中需要满足体积的限制n - v >= πR * R * H,H最小取1而π是不用管的==> R(u) <= √n-v,也即上面的式子,类似地 n - v >= πR * R * H,令R = 1可以得到高度的限制为H(u) <= (n - v) / (r * r);③ 可以预处理出前u层体积的最小值与表面积的最小值,对于每一层蛋糕来说每一层的高度与半径在上一层的基础加1,最上面的蛋糕高度和半径都为1;预处理之后之后可以进行可行性剪枝与最优性剪枝,v + min(u) <= n,s + min(s) < res;④ 这个剪枝其实比较难想,需要对公式进行放缩:S1~u = ∑2πRkHk(k = 1,2...u)= 2 / Ru+1 ∑πRkHkRu+1 >= 2 / Ru+1∑Rk * Rk * Hk,n - v = ∑πRk * Rk * Hk ==> S1~u >= 2(n - v) / Ru+1,在递归的时候如果发现s + 2(n - v) / Ru+1 > res,也可以退出了;
3. 代码如下:
import math
from typing import List
class Solution:
res = 0
def dfs(self, n: int, m: int, u: int, s: int, v: int, R: List[int], H: List[int], minv: List[int], mins: List[int]):
# 最优性剪枝
if s + mins[u] >= self.res: return
# 可行性剪枝
if v + minv[u] > n: return
# 下面这个是最难想的剪枝
if s + 2 * (n - v) // R[u + 1] >= self.res: return
if u == 0:
if v == n:
self.res = s
return
# r和h的取值范围
for r in range(min(R[u + 1] - 1, int(math.sqrt(n - v))), u - 1, -1):
for h in range(min(H[u + 1] - 1, (n - v) // (r * r)), u - 1, -1):
t = 0
# 因为从最上面看的表面积相当于是一个最底层的圆的面积这里特判一下就可以了, 也即加上这一部分的长度
if u == m:
t += r * r
# 将当前这一层的r和h置为对应的半径和高度
R[u], H[u] = r, h
self.dfs(n, m, u - 1, s + 2 * r * h + t, v + r * r * h, R, H, minv, mins)
# 这道题目是dfs剪枝很经典的题目
def process(self):
# n表示总体积, m表示层数
n = int(input())
m = int(input())
# R, H用来记录递归过程中的半径和高度
R, H = [0] * 30, [0] * 30
minv, mins = [0] * 30, [0] * 30
INF = 10 ** 10
# 预处理出minv和mins列表, 方便后面计算
for i in range(1, m + 1):
minv[i] = minv[i - 1] + i * i * i
mins[i] = mins[i - 1] + 2 * i * i
# 这两个元素是哨兵
R[m + 1] = H[m + 1] = INF
self.res = INF
self.dfs(n, m, m, 0, 0, R, H, minv, mins)
if self.res == INF: self.res = 0
return self.res
if __name__ == "__main__":
print(Solution().process())