洛谷B4222 [常州市赛 2023] 积木 c++题解

洛谷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 连续自然数和 (前缀和和双指针使用)


如果对你理解有帮助,就点个赞再走吧,有问题欢迎随时在评论区指出

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值