You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.8 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 460 子矩阵

一、题目描述

给出如下定义:

子矩阵:从一个矩阵当中选取某些行和某些列 交叉位置 所组成的新矩阵(保持行与列的相对顺序)被称为原矩阵的一个子矩阵。

例如,下面左图中选取第 2、4 行和第 2、4、5 列交叉位置的元素得到一个 2×3 的子矩阵如右图所示。

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

矩阵的分值:矩阵中每一对相邻元素之差的绝对值之和。

本题任务:给定一个 nm 列的正整数矩阵,请你从这个矩阵中选出一个 rc 列的子矩阵,使得这个子矩阵的分值最小,并输出这个分值。

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

接下来的 n 行,每行包含 m 个用空格隔开的整数(均不超过 1000),用来表示问题描述中那个 nm 列的矩阵。

输出格式 输出共 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 该矩阵中分值最小的 23 列的子矩阵由原矩阵的第 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 该矩阵中分值最小的33列的子矩阵由原矩阵的第 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;
}