6.8 KiB
AcWing
460
子矩阵
一、题目描述
给出如下定义:
子矩阵:从一个矩阵当中选取某些行和某些列 交叉位置 所组成的新矩阵(保持行与列的相对顺序)被称为原矩阵的一个子矩阵。
例如,下面左图中选取第 2、4
行和第 2、4、5
列交叉位置的元素得到一个 2×3
的子矩阵如右图所示。
相邻的元素:矩阵中的某个元素与其上下左右四个元素(如果存在的话)是相邻的。
矩阵的分值:矩阵中每一对相邻元素之差的绝对值之和。
本题任务:给定一个 n
行 m
列的正整数矩阵,请你从这个矩阵中选出一个 r
行 c
列的子矩阵,使得这个子矩阵的分值最小,并输出这个分值。
输入格式
第一行包含用空格隔开的四个整数 n,m,r,c
,意义如问题描述中所述,每两个整数之间用一个空格隔开。
接下来的 n
行,每行包含 m
个用空格隔开的整数(均不超过 1000
),用来表示问题描述中那个 n
行 m
列的矩阵。
输出格式
输出共 1
行,包含 1
个整数,表示满足题目描述的子矩阵的最小分值。
数据范围
1≤n,m≤16,1≤r≤n,1≤c≤m
输入样例 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
解析: 样例
1
: 该矩阵中分值最小的2
行3
列的子矩阵由原矩阵的第4
行、第5
行与第1
列、第3
列、第4
列交叉位置的元素组成,为
6 5 6
7 5 6
其分值为:
|6-5| + |5-6| + |7-5| + |5-6| + |6-7| + |5-5| + |6-6| =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
解析 样例
2
: 该矩阵中分值最小的3
行3
列的子矩阵由原矩阵的第4
行、第5
行、第6
行与第2
列、第6
列、第7
列交叉位置的元素组成,选取的分值最小的子矩阵为
9 7 8
9 8 8
5 8 10
二、题目解析
(枚举,DP
,线性DP
)
O(C_n^rn^3)
当选定的行固定时,问题变成:
给定一个长度为
m
的序列,从中选出一个长度为c
的子序列。序列中的每个元素均有一个分值,且任意相邻两个被选出的元素,也会产生一个分值。问:如何选择子序列可使分值之和最小。
这是一个经典的序列DP
模型:
状态表示
f[i][j]
表示所有以第i
个数结尾,且长度为j
的子序列的分值之和的最小值。
状态计算
以倒数第二个数是哪个数为依据,将f[i][j]
所代表的集合分成若干类,则倒数第二个数是第k
个数的所有子序列的最小分值是
f[k][j - 1] + cost()
其中cost()
是在序列末尾加上第i
个数所产生的分值。
f[i][j]
取所有类别的最小分值即可。
由于 n
较小,我们可以直接枚举行的所有选择,然后用上述做法DP
即可。
时间复杂度
一共有 n
行,从中选出 r
行,总共有 C_n^r
种选择,对于每种选择,DP
的状态总共有 O(n^2)
个,计算每个状态需要 O(n)
的计算量,因此DP
的时间复杂度是 O(n^3)
。所以总时间复杂度是 O(C_n^rn^3)
。
三、实现代码
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
const int INF = 0x3f3f3f3f;
int n, m, r, c; // n行m列的矩阵,从中选择r行c列
int a[N][N]; // 原始矩阵
int f[N][N]; // DP数组
int cw[N]; // 每一列内部的代价,column代价,用cw表示(在行确定的情况下)
int rw[N][N];
// 任意两列之间的代价,因为列之间可能不连续,所以需要用二维描述
// 比如rw[3][6],描述第3列与第6列被选中,它们之间相邻,计算它们之间的差值绝对值,
// 也就是横向代价。
int q[N];
// 计算一个二进制数中有多少个数字1
int count(int x) {
int s = 0;
for (int i = 0; i < n; i++) s += x >> i & 1;
return s;
}
int main() {
cin >> n >> m >> r >> c;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> a[i][j];
int res = INF;
for (int st = 0; st < 1 << n; st++) // 二进制枚举
if (count(st) == r) { // 如果二进制的状态表示中,数字1的个数与目标值相同
for (int i = 0, j = 0; i < n; i++)
if (st >> i & 1) q[j++] = i; // 记录选择了哪几行
// 预处理出每一列(i列)(在选择r行后,形成的小矩阵情况下)
// 数字上下之间的abs(差值)
for (int i = 0; i < m; i++) { // 枚举每一列
cw[i] = 0; // 计算每一列数字上下之间的abs(差值)和
for (int j = 1; j < r; j++) // 选择的每一行
cw[i] += abs(a[q[j]][i] - a[q[j - 1]][i]);
}
// 如果i与j列选择后相邻,预处理出sum(abs(差值)),方便DP增量时使用
// j一定要在i后面才有意义
for (int i = 0; i < m; i++) // 枚举每一列,开始列
for (int j = i + 1; j < m; j++) { // 从i列到j列,结束列
rw[i][j] = 0; // 多轮DP,需要手动初始化后才能进行处理
for (int k = 0; k < r; k++) // 选择的每一行
rw[i][j] += abs(a[q[k]][i] - a[q[k]][j]);
// 将多行的相邻(i,j)列之间的所有abs(差值)都汇总到rw[i][j]
}
// DP
for (int i = 0; i < m; i++) { // 枚举每一列
f[i][1] = cw[i]; // 第i个数结尾,长度为1的子矩阵,没有横向的,只有纵向的
for (int j = 2; j <= c; j++) {
f[i][j] = INF;
// 向前寻找前一个有效位置k
for (int k = 0; k < i; k++)
// 两者间的转移关系= + 纵向i列转移代价 + (k,i)之间的横向转移代价
f[i][j] = min(f[i][j], f[k][j - 1] + cw[i] + rw[k][i]);
}
// 每次的结果都有机会参加评比
res = min(res, f[i][c]);
}
}
// 输出最终的最小值
cout << res << endl;
return 0;
}