AcWing 168. 生日蛋糕

AcWing 168. 生日蛋糕

一、题目描述

717日是 Mr.W 的生日,ACMTHU 为此要制作一个体积为 NπM 层生日蛋糕,每层都是一个圆柱体。

从下往上数i 层蛋糕是半径为 Ri,高度为 Hi 的圆柱。

i<M 时,要求 Ri>Ri+1Hi>Hi+1

由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q 最小。

Q=Sπ ,请编程对给出的 NM,找出蛋糕的制作方案(适当的 RiHi 的值),使 S 最小。

Q 外,以上所有数据皆为 正整数

输入格式
输入包含两行,第一行为整数 N,表示待制作的蛋糕的体积为 Nπ

第二行为整数 M,表示蛋糕的层数为 M

输出格式
输出仅一行,是一个正整数 S(若无解则 S=0)。

数据范围
1N10000,1M20

输入样例

100
2

输出样例

68

二、题目分析

1、梳理题意

自上而下 的各层蛋糕编号为1m,题目要求我们设计一个 m 层蛋糕,下层 的蛋糕比 上层 的蛋糕 半径高度要大 ,并且总的体积为n,求 最小的表面积

2、公式推导

体积

V=i=1mπRi2Hi

上表面积S
  • m层的上表面积是SmSm1
  • m1层的上表面积是Sm1Sm2
  • ...
  • 2层的表面积是S2S1
  • 1层的上表面积是S1

S=SmSm1+Sm1Sm2+...+S2S1+S1=Sm=πRm2

解释:被盖上的那部分面积,都会由上面的小蛋糕补上

侧表面积S

侧表面积 就是每个圆柱的侧边面积,也就是一个围成圆柱的矩形面积,圆的边长 乘以 高度

S=i=1m2πRiHi

整体表面积S

上表面积 + 侧表面积:

S=S+S=πRm2+i=1m2πRiHi

3、剪枝与优化技巧

(1) 优化搜索顺序

为了 先搜索的分支数少,应该 自大到小 搜索:

  • 从第m层搜索到第1
  • 体积公式中Ri平方级别变化率要高于Hi,先搜Ri,再搜Hi

解释:类比 运输小猫,大头在前,可以使dfs搜索树搜索的层数减少,搜索效率提升

(2) 各层高度和半径的范围

设第i层蛋糕的半径是Ri,高度是Hi,因为 半径高度 都是 整数,并且Ri<Ri+1,Hi<Hi+1
由于 最上层 的蛋糕半径和高度也必须是 正整数,所以R1H1至少是1R2要大于R1,至少是2,由此可见:第i层蛋糕的 半径高度 至少是i
i层的 高度半径 至少i,这就是 各层高度和半径的最小值, 这是Ri,Hi下界
Ri<Ri+1Hi<Hi+1,这是一个上界

(3) 预处理

为了在走一半时,能迅速 估计 继续走下去是否能成功,我们可以提前预处理出两个数组:

  • mins[i] : 1i层侧表面积的最小值:

mins[i]=mins[i1]+2ii

解释:我们手中资源是剩余的体积,随时需要知道剩余1i这些层中,我们至少需要多少体积,及时发现不合理的分支并及时停止。

  • minv[i] : 1i层体积的最小值:

minv[i]=minv[i1]+iii

解释:从第m层搜到第u层总的体积是v,而从第1层到第u层体积至少是minv[u],所以当v+minv[u]>n时就说明 肯定后续会超过体积n方案不合法 (可行性剪枝)

(4) 找出两个变量之间的制约关系

从第m层蛋糕搜到当前的第u层时,设已经使用的表面积是s,体积是v,则从第u层到第1层留给我们的体积 最多 只有nv,所以第u层的半径πRu2Hu<=nv

题目很贴心,不想让我们计算π=3.1415926...这样的小数,而是让我们计算去掉π后的常数系数,下面的论述中将忽略掉π:

Ru<=((nv)/Hu)

Hu最小值u,取u时,右侧相当于取到了 最大值,不等式也依然成立,所以

{Ru<=(nv)/uHu<=(nv)/Ru2

解释Ru不是随便想多少就是多少的,受到给定后续大小nv和当前层次u的限制,不能大于(nv)/u。而另一个变量Hu也不随便想多少就是多少的,它的受限和Ru相关,一旦确定了Ru的值,Hu上限 也就确定了。
这就是HuRu另一个上界,两个变量RiHi不是孤立的,而是存在制约关系,我们在上面推导的过程,其实就是试图找出两者之间的关联制约关系,这样就可以在dfs过程中,及时找到冲突点,及时止损,return false

既然RiHi都同时要受两个条件的约束,那么 他们的上界取需要取 两个上界条件 的最小值

(5) 深搜函数设计

参数设计:

  • u:当前是第几层
  • v:已经占用的空间
  • S:已经获得的表面积


ans之前搜到的最小表面积
S =m层的上表面积+已完成的各层侧表面积和

(6) 剪枝策略
  • 搜索到一半时表面积已超过了目前的最优解
    如果s+mins[u]>=ans,也就是当这个方案搜下去的表面积不可能优于小于之前搜到的解时,应该否定这个方案

  • 表面积和体积之间的制约关系
    从第1层到第u层的 侧表面积

S1u=i=1u2πRiHi

1层到第u层的体积(Vu是指 m层出发 到达u 时,已使用了的体积)

V1u=nVu=i=1uπRi2Hi

为了找出两者之间的关系,需要构造成同样的部分:侧表面积 那个RiHi,体积这个Ri2Hi,很明显,表面积乘上一个Ri再除一个Ri就能得到一个和体积相关的东西,但Ri目前是不确定的(正在尝试中),我们需要依赖的是前面u+1Ru+1(本分枝中,前面u+1层的Ru+1已经设定),所以就乘上再除上Ru+1试试:

S1u=i=1u2πRiHi=2πRu+1i=1uRu+1RiHi

同时因为Ru+1>Ri,所以:

S1u>2Ru+1i=1uπRi2Hi

体积等式 代入:

S1u>2(nVu)Ru+1

因此如果S=S+S1u>=Sans
就是说当前阶段,后面不管怎么整,都肯定不会比目前的最优答案Sans更牛X,可以剪枝

即当

S+2(nV)Ru+1>=Sans

时就可以剪枝掉(最优性剪枝)

(7) 递归出口
  • 最后递归的边界是u==0,遍历完了所有层蛋糕
  • v恰好是n时,就可以更新最优解了

四、实现代码

#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 22;
int n;          // 待制作的蛋糕的体积为
int m;          // 蛋糕的层数为 m
int ans = INF;  // 最小表面积,预求最小,先设最大
int mins[N];    // 每一层的最小表面积
int minv[N];    // 每一层的最小体积
int R[N], H[N]; // 为每一层蛋糕分配的半径和高度

// u:正在研究第几层
// v:已经使用了的体积
// s:已经使用了的表面积
void dfs(int u, int v, int s) {
    // 如果来到第u层,发现,已经使用过的体积v,加上第1~u层最小体积minv[u] ,大于整体的体积n,就不用再继续了
    if (v + minv[u] > n) return;

    // 如果来到第u层,发现,已经使用过的表面各s,加上第u层及以上所有层需要的表面积mins[u]
    // 大于当前最优解了,就不用再继续研究了,还不如最优解,再研究也是白扯
    if (s + mins[u] >= ans) return;

    // 第二个最优性剪枝,侧面积和体积不是孤立存在的,是有关联关系的。
    // 不符合这个关联关系的Ri和Hi是没有必要继续的
    if (2 * (n - v) / R[u + 1] + s >= ans) return;

    // 摆完所有层蛋糕
    if (u == 0) {
        // 如果已经使用的体积等于n,如果是,那么更新答案
        if (v == n) ans = s;
        return;
    }
    // 枚举每一个可能的半径,同理,半径由大到小
    for (int r = min(R[u + 1] - 1, (int)sqrt((n - v) / u)); r >= u; r--)
        for (int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h--) {
            // 如果是第m层,需要加上一个底面积,其它层就只计算侧面积
            int t = (u == m ? r * r : 0);
            // 记录下第u层的选择,半径:r,高度:h
            R[u] = r, H[u] = h; // 因为静态数组可以重复按索引写入,所以不需要回溯
            // 递归上一层,体积变为v+r*r*h,表面积由s变大为 s + 2 * r * h + t
            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}
int main() {
    cin >> n >> m;
    // 预处理
    for (int i = 1; i <= m; i++) {
        // 第i层,半径最小是i,高度最小是i,否则更小层无法获得整数解
        // 预处理出每层及上边所有层的最小表面积,看清楚这是一个叠加的关系,不是简单的计算,是递推式
        mins[i] = mins[i - 1] + 2 * i * i;
        // 预处理出每层及上边所有层的最小体积
        minv[i] = minv[i - 1] + i * i * i;
    }
    // 初始化为无穷大,防止在计算下层半径高度时取到0
    R[m + 1] = H[m + 1] = INF;

    // 从下向上进行深搜,按经验来看,这样能减少出发时的分枝数量,效率提升
    // 从m层出发,此时的体积为0,此时的表面积为0
    dfs(m, 0, 0);

    // 输出答案
    if (ans == INF)
        puts("0");
    else
        printf("%d\n", ans);
    return 0;
}
posted @   糖豆爸爸  阅读(149)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2020-03-09 OAuth2与sso的结合思路
Live2D
点击右上角即可分享
微信分享提示