每维只记录已被切掉的最大坐标,剩余部分始终是长方体,体积等于三个剩余长度之积,单次操作 O(1)。
OJ: luogu
题目 ID: P9713
难度:普及-
标签:模拟枚举思维数学
创建: 2026-10-02 15:23
更新: 2026-10-03 11:51
形式化题目
有一个点集
共进行
:删去所有 的点; :删去所有 的点; :删去所有 的点。
注意
求每次操作之后
暴力解法
思路
最直接的写法就是题面的逐字翻译:把蛋糕真的“摆出来”,用一个三维数组(或者压成一维的 alive[])标记每个单位立方体还在不在,初始全部为
对每次操作
这个做法不需要任何观察,正确性是一眼可见的,适合作为理解题意和对拍的基线。它的代价是每次操作都要把整个长方体重新扫一遍,包括那些早就被切掉的格子。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-02 15:23
* update_at: 2026-10-02 15:23
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 写法就是题面的逐字翻译:开一个三维的“还在不在”标记,每次操作把符合条件的方块一个个删掉,
// 每删掉一个就让剩余个数减一,然后输出剩余个数。
// 复杂度是 O(a * b * c * m),只在对拍用的小数据上运行。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a, b, c;
int m;
// 把三维坐标压成一维下标,省内存也省事:idx = ((x-1)*b + (y-1))*c + (z-1)
vector<char> alive;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> a >> b >> c >> m;
ll total = a * b * c;
alive.assign((size_t)total, 1); // 1 表示这个单位立方体还在
for (int t = 1; t <= m; ++t) {
int op;
ll k;
cin >> op >> k;
// 一次操作:把所有满足“第 op 维坐标 <= k”的方块切掉
for (ll x = 1; x <= a; ++x) {
for (ll y = 1; y <= b; ++y) {
for (ll z = 1; z <= c; ++z) {
size_t idx = (size_t)(((x - 1) * b + (y - 1)) * c + (z - 1));
if (alive[idx] == 0) {
continue; // 已经被前面的操作切掉了
}
ll pos;
if (op == 1) {
pos = x;
} else if (op == 2) {
pos = y;
} else {
pos = z;
}
if (pos <= k) {
alive[idx] = 0;
total--;
}
}
}
}
cout << total << "\n";
}
return 0;
}复杂度
设
时间复杂度和空间复杂度分别为
在
瓶颈
瓶颈就在于:我们把每一个单位立方体都当成了独立对象去管理。
而题面其实从头到尾没有要求我们回答“某个具体的点还在不在”,它只问剩余总数。为了回答这个总数,我们却付出了维护
另一个更重要的事实是:一次操作只沿着一个坐标轴、在一个坐标值上切。也就是说,每次操作删掉的都是一整层(或几层)方块,而不是零散的几块。这意味着剩余集合的形状非常特殊,值得重新描述一遍。
正解
思路
先只看一维,把问题简化。
在一维(
- 若
,剩 ,个数 ; - 若
,这些点早就没了,剩余不变,个数还是 。
换句话说,一维时只需要记住历史上出现过的最大 cut = max(所有 k),答案恒为
这个观察能直接推到三维,原因是三次坐标的删除互不干涉:
删掉
的点之后,剩下的点仍然满足“ 和 的分布完全没变”。
所以我们可以逐维独立地做上面那件事。记
(没有 时为 ), , ,
那么某一时刻的剩余集合恰好是
这仍然是一个长方体,它的体积就是三个方向剩余长度的乘积:
于是每次操作只需要:读入 cut[op] = max(cut[op], k),然后输出上面这个乘积。一次操作
用样例
| 操作 | 剩余集合 | 体积 | |||
|---|---|---|---|---|---|
| 初始 | 0 | 0 | 0 | 27 | |
1 2 |
2 | 0 | 0 | ||
2 1 |
2 | 1 | 0 |
和样例输出 max 之后什么都不变,所以输出与第
正确性:对操作次数归纳。初始
而 cut[1]=max(cut[1],k) 后的形式相同;
实现注意:int,必须用 long long;读入输出用 %lld。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-02 15:23
* update_at: 2026-10-02 15:23
*/
// P9713 「QFOI R1」抱抱
// 关键结论:每一刀都只沿着一个坐标轴切,且切的位置是初始的绝对坐标。
// 于是我们不需要真的维护蛋糕,只要记住:
// cut[1] = 所有 op=1 的 k 的最大值(切掉 x <= cut[1] 的全部方块)
// cut[2] = 所有 op=2 的 k 的最大值
// cut[3] = 所有 op=3 的 k 的最大值
// 剩下的方块恰好是 x > cut[1] 且 y > cut[2] 且 z > cut[3] 的那个小长方体,
// 体积 = (a - cut[1]) * (b - cut[2]) * (c - cut[3])。
// 每次操作只需要用 k 更新一次最大值,再输出体积。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
ll a, b, c;
int m;
// cut[i] 表示第 i 个维度上“被切掉的部分”能到达的最大坐标,初始为 0
ll cut[4];
void read_data() {
scanf("%lld %lld %lld %d", &a, &b, &c, &m);
for (int i = 1; i <= m; ++i) {
int op;
ll k;
scanf("%d %lld", &op, &k);
// 切的位置只增不减:k 比历史最大值小就说明这一刀已经没有方块可切
if (k > cut[op]) {
cut[op] = k;
}
// 剩余体积 = 三个方向剩余长度的乘积,最多 1e18,必须用 long long
ll ans = (a - cut[1]) * (b - cut[2]) * (c - cut[3]);
printf("%lld\n", ans);
}
}
signed main () {
ios::sync_with_stdio(false); cin.tie(0);
read_data();
return 0;
}复杂度
每次操作只做一次比较赋值和一次乘法,时间
额外空间只有三个变量,即
总结
这道题的关键在于不要被“切蛋糕”这个说法带着走。
一旦意识到每次操作都是沿坐标轴切走一整层,剩余部分的形状就始终是一个长方体;而“