## [$AcWing$ $460$ 子矩阵](https://www.acwing.com/problem/content/462/) ### 一、题目描述 给出如下定义: 子矩阵:从一个矩阵当中选取某些行和某些列 **交叉位置** 所组成的新矩阵(保持行与列的相对顺序)被称为原矩阵的一个子矩阵。 例如,下面左图中选取第 $2、4$ 行和第 $2、4、5$ 列交叉位置的元素得到一个 $2×3$ 的子矩阵如右图所示。 ![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/10/212a1b14bd6e771e4ac1fe3b1211bfb2.png) **相邻的元素**:矩阵中的某个元素与其上下左右四个元素(如果存在的话)是相邻的。 **矩阵的分值**:矩阵中每一对相邻元素之差的绝对值之和。 本题任务:给定一个 $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**: ```cpp {.line-numbers} 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**: ```cpp {.line-numbers} 6 ``` > **解析**: > 样例$1$: 该矩阵中分值最小的 $2$ 行 $3$ 列的子矩阵由原矩阵的第 $4$ 行、第 $5$ 行与第 $1$ 列、第 $3$ 列、第 $4$ 列交叉位置的元素组成,为 ```cpp {.line-numbers} 6 5 6 7 5 6 ``` > 其分值为: $|6-5| + |5-6| + |7-5| + |5-6| + |6-7| + |5-5| + |6-6| =6$。 **输入样例 2**: ```cpp {.line-numbers} 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**: ```cpp {.line-numbers} 16 ``` > **解析** > 样例$2$: 该矩阵中分值最小的$3$行$3$列的子矩阵由原矩阵的第 $4$ 行、第 $5$ 行、第 $6$ 行与第 $2$ 列、第 $6$ 列、第 $7$ 列交叉位置的元素组成,选取的分值最小的子矩阵为 ```cpp {.line-numbers} 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$ ```cpp {.line-numbers} #include 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; } ```