前言:有点题感后,看到矩阵,条件是求最小,很容易想到要用到dp(动态规划),但之前没遇到过枚举+dp的组合,有三个难点:
1、dp数组的含义(知道后,相应的状态转移方程也就出来了)
2、问题分解(一种思维模式,下面详解)
3、别被for循环绕晕
问题 D: 子矩阵
题目描述
给出如下定义:
子矩阵:从一个矩阵当中选取某些行和某些列交叉位置所组成的新矩阵(保持行与列的相对顺序)被称为原矩阵的一个子矩阵。
例如,下面左图中选取第2、4行和第2、4、5列交叉位置的元素得到一个2*3的子矩阵如右图所示。
相邻的元素:矩阵中的某个元素与其上下左右四个元素(如果存在的话)是相邻的。
矩阵的分值:矩阵中每一对相邻元素之差的绝对值之和。
本题任务:给定一个n行m列的正整数矩阵,请你从这个矩阵中选出一个r行c列的子矩阵,使得这个子矩阵的分值最小,并输出这个分值。
输入
第一行包含用空格隔开的四个整数n,m,r,c,意义如问题描述中所述,每两个整数之间用一个空格隔开。
接下来的n行,每行包含m个用空格隔开的整数,用来表示问题描述中那个n行m列的矩阵。
输出
输出共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
样例输出
6
提示
该矩阵中分值最小的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。
对于50%的数据,1 ≤ n ≤ 12,1 ≤ m ≤ 12,矩阵中的每个元素1 ≤ a[i][j] ≤ 20;
对于100%的数据,1 ≤ n ≤ 16,1 ≤ m ≤ 16,矩阵中的每个元素1 ≤ a[i][j] ≤ 1,000,1 ≤ r ≤ n,1 ≤ c ≤ m.
个人思维过程:
(第一印象)
又要选r行,又要选c列,再看这题的数据范围比较小,第一印象是枚举所有行列组合,用dfs1枚举行的组合,再dfs2枚举列的组合,再把每次选出的a[i][j],放到另一个b[][]数组,死求矩阵分值。
下面反例片段
int ss(){
int cnt=0;
for(int i=1;i<=r;i++){
for(int j=2;j<=c;j++)
cnt+=abs(b[i][j]-b[i][j-1]);
}
for(int i=1;i<=c;i++){
for(int j=2;j<=r;j++)
cnt+=abs(b[j][i]-b[j-1][i]);
}
return cnt;
}
无脑,典型的暴力,数据很小后才可以,我试了一下,代码短,但是超时,通过率%9,显然这个数据对dfs里再套个dfs,加死求分值,是不通融的。
(问题分解)
我个人理解的问题分解主要是两个方法:
1、简化条件
于是我开始假设:
如果题目把选的r行已知了,只要考虑选c列,求最小分值。
显然可以用dp解决,建一个二维数组dp[i][j]
表示 前 i 列 选 j 列的最小分值,最后答案只要在dp[c][c]到dp[m][c]之间遍历,选最小的。
甚至状态转移方程也可以出来
dp[i][j]=min(dp[i][j],dp[k][j-1]+链接第k列与第i列的代价);
这里1<k<i;
然后回到原来的问题,根据之前的思维,引出dp法,现在考虑1、r行怎么确定,或者 2、r行是否也可以用dp法?
如果选r行,和选c列都用dp,得建一个四维数组dp[x1][y1][x2][y2]:前x1行选y1行+前x2列选y2列的最小分值。
呵呵。我最多也就见识过三维的(类似背包问题什么的),而且这种矩阵类型的题一般都是二维。
纯枚举不行,纯dp不行。所以试试枚举+dp吧。
2、条件分解
枚举得到暂时确定的 r 行,怎么求dp数组?(因为dp数组中 链接第k列与第i列的代价 暂时不会转换)
其实将问题分为行和列,分开解决,也是一种条件分解。分开行列后,我们可以知道,在选暂时的 r 行时,每列选哪几个数是不变的,而矩阵分值又要竖着相互求,也要横着,所以分值也可以分行列。
这里我把列与列之间的分值叫做列分值(或许有错误,但先这么理解)
原本的全部放入一个数组,死求法,便可以转换dp求。链接第k列与第i列的代价==选第i列自身的行分值+第k列与第i列之间的列分值。
其中“ 选第i列自身的行分值 ”确定r行便可知,可以提前储存进一个数组sum[i]:表示选第i列自身的行分值,也表示dp[i][1].
(多做标注)
枚举很简单,到len>r后考虑dp,我自己写写代码时,卡在for循环比较长,怎么说呢,多刷题,做做标注,断行,让结构更直观,应该可以解决点。
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
int mi=inf;
int n,m,r,c,a[17][17],b[17][17],x[17],dp[17][17],sum[17];
void dfs(int len,int be){
//枚举结束条件
if(len>r){
memset(sum,0,sizeof(sum));
memset(dp,inf,sizeof(dp)); //这里memset()函数初始化值只能-1、0、inf,不能是1
for(int i=1;i<=m;i++){
for(int j=1;j<r;j++){
sum[i]+=abs(a[x[j]][i]-a[x[j+1]][i]); //选第i列自身的行分值 (初始化1)
}
}
for(int i=1;i<=m;i++) dp[i][1]=sum[i]; //初始化2
for(int i=2;i<=m;i++){ //前i列(加入第i列)
for(int j=2;j<=c;j++){ //选j列
for(int k=1;k<i;k++){ //遍历第i列前第k种的情况
int temp=0;
for(int z=1;z<=r;z++) temp+=abs(a[x[z]][i]-a[x[z]][k]); //第k列与第i列之间的列分值
dp[i][j]=min(dp[i][j],dp[k][j-1]+sum[i]+temp); //再加上第i列自身的行分值
}
}
}
for(int i=c;i<=m;i++) mi=min(mi,dp[i][c]); //遍历,找最小
return;
}
//枚举继续
for(int i=be;i<=n;i++){
x[len]=i;
dfs(len+1,i+1);
x[len]=0; //这行代码可以不写
}
}
int main(){
cin>>n>>m>>r>>c;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)
cin>>a[i][j];
}
dfs(1,1);
cout<<mi;
}
/*
案例一
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
6
案例二
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
16
*/
回头看这题,其实全是模板,枚举模板+dp模板,重在思维吧。