(UPC寒假15)问题 D: 子矩阵(枚举+dp,中等题)

前言:有点题感后,看到矩阵,条件是求最小,很容易想到要用到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模板,重在思维吧。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值