洛谷 P2258 子矩阵 题解

探讨了一道关于寻找矩阵中具有最小分值的子矩阵的算法问题,通过枚举行和动态规划的方法来求解,适用于NOIP普及组水平。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天看了看枚举与搜索的题,意外中看到了一道“看似简单”的普及组题,然而事实是我想了好久,然后终于搞出来了。
题目:

题目描述

给出如下定义:

子矩阵:从一个矩阵当中选取某些行和某些列交叉位置所组成的新矩阵(保持行与列的相对顺序)被称为原矩阵的一个子矩阵。
例如,下面左图中选取第2、4行和第2、4、5列交叉位置的元素得到一个2*3的子矩阵如右图所示。

9 3 3 3 9

9 4 8 7 4

1 7 4 6 6

6 8 5 6 9

7 4 5 6 1

的其中一个2*3的子矩阵是

4 7 4

8 6 9

相邻的元素:矩阵中的某个元素与其上下左右四个元素(如果存在的话)是相邻的。

矩阵的分值:矩阵中每一对相邻元素之差的绝对值之和。
本题任务:给定一个n行m列的正整数矩阵,请你从这个矩阵中选出一个r行c列的子矩阵,使得这个子矩阵的分值最小,并输出这个分值。

(本题目为2014NOIP普及T4)

输入输出格式

输入格式:
第一行包含用空格隔开的四个整数nmrc意义如问题描述中所述,每两个整数之间用一个空格隔开。

接下来的n行,每行包含m个用空格隔开的整数,用来表示问题描述中那个n行m列的矩阵。

输出格式:
输出共1行,包含1个整数,表示满足题目描述的子矩阵的最小分值。

输入输出样例

输入样例#1:
5 5 2 3
9 3 3 3 9
9 4 8 7 4
1 7 4 6 6
6 8 5 6 9
7 4 5 6 1
输出样例#1:
6
输入样例#2:
7 7 3 3
7 7 7 6 2 10 5
5 8 8 2 1 6 2
2 9 5 5 6 1 7
7 9 3 6 1 7 8
1 9 1 4 7 8 8
10 5 9 1 1 8 10
1 3 1 5 4 8 6
输出样例#2:
16

说明

【输入输出样例1说明】

该矩阵中分值最小的2行3列的子矩阵由原矩阵的第4行、第5行与第1列、第3列、第4列交叉位置的元素组成,为

6 5 6

7 5 6

,其分值为

|65|+|56|+|75|+|56|+|67|+|55|+|66|=6

【输入输出样例2说明】

该矩阵中分值最小的3行3列的子矩阵由原矩阵的第4行、第5行、第6行与第2列、第6列、第7列交叉位置的元素组成,选取的分值最小的子矩阵为

9 7 8 9 8 8 5 8 10

【数据说明】

对于50%的数据,1n121m12,矩阵中的每个元素1a[i][j]20

对于100%的数据,1n161m16,矩阵中的每个元素1a[i][j]1,000

1rn1cm

分析
最暴力的算法是枚举选择哪些行、列。复杂度为O(C(n,r)×C(m,c))。不过显然不能承受。(C为组合数)
注意到虽然O(C(n,r)×C(m,c))不能承受,但O(C(n,r))O(C(m,c))是可以接受的。
不妨考虑枚举其中一个(假设枚举行)。
枚举完行后,由于行已确定,因此可以把所有行捆绑,视为一个整体。
处理处列与列之间的价值,然后可以用动态规划解决这个问题。
dp[i][k]表示前i列选了k列,并且第i列强制被选。那么转移方程为:dp[i][k]=dp[j][k1]+cost[j][i]+val[i],其中j<icost[j][i]表示第i列与第j列相邻的花费,val[i]表示第i列内的花费。
答案即为mindp[i][c]
代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef double DB;

const int inf = 1e9 + 7;

int n, m, r, c;
int ans = inf;
int a[17][17], C[17], f[17][17], cost[17][17], val[17];

inline int read(){
    int r = 0, z = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-') z = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9'){r = r * 10 + ch - '0'; ch = getchar();}
    return r * z;
}

int Max(int a, int b){return a > b ? a : b;}
int Min(int a, int b){return a < b ? a : b;}

int DP(){
    int res = inf;
    for(int i = 1; i <= m; i ++){
        val[i] = 0;
        for(int j = 1; j < r; j ++)
            val[i] += abs(a[C[j]][i] - a[C[j + 1]][i]);
    }
    memset(cost, 0, sizeof(cost));
    for(int i = 1; i <= m; i ++)
        for(int j = i + 1; j <= m; j ++){
            cost[i][j] = 0;
            for(int k = 1; k <= r; k ++)
                cost[i][j] += abs(a[C[k]][i] - a[C[k]][j]);
        }
    for(int i = 0; i <= m; i ++) f[i][0] = 0;
    for(int i = 1; i <= m; i ++){
        for(int k = 1; k <= Min(i, c); k ++){
            f[i][k] = inf;
            for(int j = k - 1; j < i; j ++)
                f[i][k] = Min(f[i][k], f[j][k - 1] + cost[j][i] + val[i]);
        }
    }
    for(int i = c; i <= m; i ++) res = Min(res, f[i][c]);
    return res;
}

void dfs(int u, int cnt){
    if(u > n){
        //cout<<1<<endl;
        if(cnt == r) ans = Min(ans, DP());
        return ;
    }
    dfs(u + 1, cnt);
    C[cnt + 1] = u;
    dfs(u + 1, cnt + 1);
}

void init(){
    n = read(), m = read(), r = read(), c = read();
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++) a[i][j] = read();
    dfs(1, 0);
    printf("%d\n", ans);
}

int main(){
    init();
    return 0;   
}
### 子矩阵问题的算法解析 #### 问题描述 给定一个 \( N \times N \) 的矩阵以及两个整数 \( S \) 和 \( K \),目标是判断是否存在一个大小为 \( K \times K \) 的子矩阵,使得其所有元素之和等于 \( S \)[^1]。 为了高效解决此问题,可以采用前缀和的思想来优化时间复杂度。以下是具体的解决方案: --- #### 前缀和的概念 前缀和是种常见的预处理技术,用于快速计算数组或矩阵中的区间和。对于二维矩阵而言,可以通过构建前缀和矩阵 \( s[i][j] \) 来表示原矩阵中从左上角到位置 \( (i, j) \) 所有元素的累加和[^2]。 具体定义如下: \[ s[i][j] = \text{matrix}[0..i-1][0..j-1] \] 通过前缀和矩阵,任意子矩阵范围内的元素和可以在常数时间内计算得出。假设要查询以 \( (x_1, y_1) \) 为左上角、\( (x_2, y_2) \) 为右下角的子矩阵,则其和可由以下公式得到[^4]: ```python sum = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] ``` --- #### 算法设计 基于上述理论,我们可以分两步解决问题: 1. **构建前缀和矩阵**:遍历整个输入矩阵并填充前缀和矩阵。 2. **滑动窗口查找**:利用固定大小的 \( K \times K \) 滑窗,在前缀和矩阵中逐验证是否有满足条件的目标子矩阵。 下面是完整的 Java 实现代码: ```java public class SubMatrixSum { public static boolean hasSubMatrixWithSum(int[][] matrix, int S, int K) { if (matrix == null || matrix.length == 0 || K <= 0) return false; int n = matrix.length; // 获取矩阵维度 // 构建前缀和矩阵 int[][] prefixSum = new int[n + 1][n + 1]; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) { prefixSum[i][j] = matrix[i - 1][j - 1] + prefixSum[i - 1][j] + prefixSum[i][j - 1] - prefixSum[i - 1][j - 1]; } } // 使用滑动窗口方法检测是否存在符合条件的子矩阵 for (int i = K; i <= n; ++i) { // 遍历起点 for (int j = K; j <= n; ++j) { // 遍历起点 int sum = prefixSum[i][j] - prefixSum[i - K][j] - prefixSum[i][j - K] + prefixSum[i - K][j - K]; if (sum == S) return true; // 如果找到匹配则返回true } } return false; // 若未发现任何匹配项则返回false } public static void main(String[] args) { int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int S = 45; int K = 3; System.out.println(hasSubMatrixWithSum(matrix, S, K)); // 输出应为true } } ``` --- #### 复杂度分析 - 时间复杂度: 构造前缀和矩阵的时间复杂度为 \( O(N^2) \),而后续滑动窗口扫描同样需耗时 \( O(N^2) \),因此整体复杂度仍保持在 \( O(N^2) \)。 - 空间复杂度: 主要是存储前缀和矩阵的空间开销,即 \( O(N^2) \)。 --- #### 性能改进方向 如果进步提升效率,还可以考虑结合哈希表记录中间状态或者应用动态规划策略减少重复运算次数。不过这些方案通常会增加额外空间需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值