洛谷B4222 [常州市赛 2023] 积木 c++题解
题目链接
1.题目内容
题目描述:小X在地上玩积木,每块积木都是1×1×1的正方体。地面可以看成一个n×m的网格,每个格子中堆着若干块积木,第i行第j列有h_{i,j}块积木。要求拿走最少数量的积木,使得剩下的积木组成一个正方体(长、宽、高相同)。
2.难度
普及/提高−
3.题目所需知识点
- 动态规划 DP
- 二分
- 前缀和
- ST 表
4. 思路
动态规划(DP):
- 通过定义状态dp[i][j]表示以(i,j)为右下角的最大正方体边长,状态转移方程为:若当前格子高度≥1,则dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,最终最大边长的立方即为保留积木数。
二分+二维前缀和:
- 预处理二维前缀和数组sum[ ][ ] 二分最大可能边长k(1≤k≤min(n,m)) 对每个k,检查是否存在k×k子区域满足sum[i][j]。
- sum[i-k][j] - sum[i][j-k] + sum[i-k][j-k] ≥ k³ 时间复杂度优化至O(n² log n)。
ST表优化:
- 对每列建立ST表,快速查询区间最小值。 枚举所有可能正方形区域,通过ST表O(1)获取区域最小高度h。 若h≥正方形边长则合法,最终取最大边长计算。
三种方法对比:
- DP适合小规模数据(n,m≤500)
- 二分+前缀和综合性能最优(n,m≤1000)
- ST表实现较复杂但查询效率高(极端大数据场景)
故本题为二分+前缀和讲解。其他提供代码,不做过多讲解
5.复杂度分析
1.动态规划(DP)解法:
- 时间复杂度:O(n²)
需要遍历整个二维网格(n×m),每个格子做一次 min 运算(比较左、上、左上三个方向的值)。 - 空间复杂度:O(n²)
需要维护一个和原网格等大的 dp 数组。
2.二分+二维前缀和解法:
- 时间复杂度:O(n² log K)
二分次数:log K(K 是最大可能边长,最多 log min(n,m) 次)。
每次二分验证:O(n²)(预处理前缀和 + 遍历检查所有可能的 k×k 区域)。 - 空间复杂度:O(n²)
存储二维前缀和数组。
3. ST表解法:
- 时间复杂度:O(n² log n)
预处理每列的ST表:O(nm log n)。
枚举所有正方形区域:O(n²),每次查询区间最小值 O(1)。 - 空间复杂度:O(nm log n)
ST表需要存储多层区间最小值。
| 解法类型 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 动态规划(DP) | O(n²) | O(n²) |
| 二分+二维前缀和 | O(n² log K) | O(n²) |
| ST表 | O(n² log n)预处理 | O(n² log n) |
6.代码片段分析
二分+二维前缀和解法
- 变量、数据结构定义
#include<bits/stdc++.h>
using namespace std;
int n, m, a[1005][1005], sum[1005][1005];
//n=行数,m=列数 a[][]=原始数据矩阵,b[][]=前缀和矩阵
- 前缀和函数
bool check(int mid){ //定义函数,检查是否存在边长为mid的全1正方形
memset(sum, 0, sizeof(sum)); //sum数组全部置为0
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
sum[i][j]=(a[i][j]>=mid);
sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
if(i>=mid && j>=mid){
int total=sum[i][j]-sum[i-mid][j]-sum[i][j-mid]+sum[i-mid][j-mid];
if(total==mid*mid) return true; //7~11 遍历,计算位置(i,j)的前缀和
}
}
}
return false;
}
二维前缀和子矩阵中所有数的和计算
sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
不懂搜这个:二维前缀和
int total=sum[i][j]-sum[i-mid][j]-sum[i][j-mid]+sum[i-mid][j-mid];
sum[i][j]:整个大矩形和(A+B+C+D)
sum[i-mid][j]:上方矩形和(A+B)
sum[i][j-mid]:左侧矩形和(A+C)
sum[i-mid][j-mid]:左上角小矩形(A)
通过 大矩形 - 上方 - 左侧 + 左上角 得到目标区域D的和
- 二分遍历
int cnt=0;
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++){
cin>>a[i][j];
cnt+=a[i][j]; //遍历二维数组 a[][],将每个元素的值累加到cnt中
}
int l=1, r=min(n,m); //确定二分搜索的上界
while(l<=r){ //28~33 二分查找的标准实现
int mid=(l+r)>>1; //等价于(l+r)/2,通过位运算优化效率,避免整数溢出
if(check(mid)) l=mid+1; //check(mid)为真:说明mid满足条件,尝试更大的值(右移左边界)
else r=mid-1; //check(mid) 为假:说明mid不满足条件,尝试更小的值(左移右边界)
}
- 输出
cout<<cnt-(l-1)*(l-1)*(l-1); //未被该正方形覆盖的剩余元素之和
其他解法
动态规划(DP)解法
#include <bits/stdc++.h>
using namespace std;
// 动态规划解法:求二进制矩阵中的最大全1正方形边长
// param matrix 二维矩阵,元素为0或1
// return 最大全1正方形的边长
int maximalSquare(vector<vector<char> >& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int rows=matrix.size();
int cols=matrix[0].size();
int max_side=0; // 记录最大边长
// DP数组:dp[i][j]表示以(i,j)为右下角的最大正方形边长
vector<vector<int> > dp(rows, vector<int>(cols, 0));
for (int i=0; i<rows; ++i) {
for (int j=0; j<cols; ++j) {
if (matrix[i][j]=='1') {
// 边界条件:第一行或第一列时边长只能为1
if (i==0 || j==0) {
dp[i][j] = 1;
} else {
// 状态转移:取左、上、左上三个方向的最小值+1
dp[i][j]=min((dp[i-1][j], dp[i][j-1]), dp[i-1][j-1])+1;
}
max_side=max(max_side, dp[i][j]);
}
// matrix[i][j]为0时,dp[i][j]保持0(默认值)
}
}
return max_side;
}
配上图片

ST表解法
#include<bits/stdc++.h>
using namespace std;
class STable{
private:
vector<vector<int> > st; // st[i][j]表示区间[i, i+2^j-1]的最大值
vector<int> lg; // 预处理log2值
public:
// 预处理log2数组(O(n)优化)
void initLog(int n){
lg.resize(n+1);
lg[1]=0;
for (int i=2; i<=n; ++i)
lg[i]=lg[i/2]+1;
}
// 构建ST表(O(nlogn)预处理)
void build(const vector<int>& nums) {
int n = nums.size();
initLog(n);
int k = lg[n]+1; // 最大层数
st.resize(n, vector<int>(k));
// 初始化:单个元素区间
for (int i=0; i<n; ++i)
st[i][0]=nums[i];
// 动态规划填充ST表
for (int j=1; j<k; ++j)
for (int i=0; i+(1<<j)<=n; ++i)
st[i][j]=max(st[i][j-1], st[i + (1 << (j-1))][j-1]);
}
// 查询区间[l,r]最大值(O(1)查询)
int query(int l, int r){
int len=r-l+1;
int k=lg[len]; // 找到最大的k满足2^k <= len
return max(st[l][k], st[r-(1<<k)+1][k]);
}
};
7.AC代码
#include<bits/stdc++.h>
using namespace std;
int n, m, a[1005][1005], sum[1005][1005]; //n=行数,m=列数 a[][]=原始数据矩阵,b[][]=前缀和矩阵
bool check(int mid){ //定义函数,检查是否存在边长为mid的全1正方形
memset(sum, 0, sizeof(sum)); //sum数组全部置为0
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
sum[i][j]=(a[i][j]>=mid);
sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
if(i>=mid && j>=mid){
int total=sum[i][j]-sum[i-mid][j]-sum[i][j-mid]+sum[i-mid][j-mid];
if(total==mid*mid) return true; //7~11 遍历,计算位置(i,j)的前缀和
}
}
}
return false;
}
int main(){
cin>>n>>m;
int cnt=0; //累计
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++){
cin>>a[i][j];
cnt+=a[i][j]; //遍历二维数组 a[][],将每个元素的值累加到cnt中
}
int l=1, r=min(n,m); //确定二分搜索的上界
while(l<=r){ //28~33 二分查找的标准实现
int mid=(l+r)>>1; //等价于(l+r)/2,通过位运算优化效率,避免整数溢出
if(check(mid)) l=mid+1; //check(mid)为真:说明mid满足条件,尝试更大的值(右移左边界)
else r=mid-1; //check(mid) 为假:说明mid不满足条件,尝试更小的值(左移右边界)
}
cout<<cnt-(l-1)*(l-1)*(l-1); //未被该正方形覆盖的剩余元素之和
return 0;
}
8.推荐题目
P1002 [NOIP 2002 普及组] 过河卒 (DP板子题)
P3865 【模板】ST 表 && RMQ 问题 (ST表板子题)
P1147 连续自然数和 (前缀和和双指针使用)
如果对你理解有帮助,就点个赞再走吧,有问题欢迎随时在评论区指出


668

被折叠的 条评论
为什么被折叠?



