CSP历年复赛题-P2258 [NOIP2014 普及组] 子矩阵
原题链接:https://www.luogu.com.cn/problem/P2258
题意解读:从矩阵中挑选出子矩阵(行、列都不一定连续),计算相邻元素差的绝对值之和最小值。
解题思路:
1、DFS+DFS
看到题目之后,直觉上应该是一个DP问题,但是考试中不一定能想到DP转移方程,不要放弃,可以使用暴力枚举来得到部分分
主要思路就是通过dfs先从n行中挑选r行,再用一个dfs从m列中挑选c列,计算每一次所选出的r行c列的值,取最小即可
时间复杂度:2^16 * 2^16 * 16^2,高达10^12,
70分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
int n, m, r, c;
int w[N][N];
int ans = 1e9;
int rows[N], cols[N]; //dfs选择的行、列
void dfs_c(int k)
{
if(k > c)
{
//计算rows、cols里存的行、列对应子矩阵的相邻元素差的绝对值之和
int res = 0;
//计算横向相邻的值
for(int i = 1; i <= r; i++)
for(int j = 2; j <= c; j++)
res += abs(w[rows[i]][cols[j-1]] - w[rows[i]][cols[j]]);
//计算纵向相邻的值
for(int i = 1; i <= c; i++)
for(int j = 2; j <= r; j++)
res += abs(w[rows[j-1]][cols[i]] - w[rows[j]][cols[i]]);
ans = min(ans, res);
return;
}
int start = 1;
if(k > 1) start = cols[k-1] + 1; //从上一个列号+1开始选
for(int i = start; i <= m; i++)
{
cols[k] = i;
dfs_c(k + 1);
}
}
void dfs_r(int k)
{
if(k > r)
{
dfs_c(1);
return;
}
int start = 1;
if(k > 1) start = rows[k-1] + 1; //从上一个行号+1开始选
for(int i = start; i <= n; i++)
{
rows[k] = i;
dfs_r(k + 1);
}
}
int main()
{
cin >> n >> m >> r >> c;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> w[i][j];
dfs_r(1);
cout << ans;
return 0;
}
2、DFS+DP
如何用DP来解决本题?
我们可以先简化一下问题:假如只有一行,要在一行中挑选一个子序列,使得相邻元素之差的绝对值之和最小。
该问题就非常类似于最长上升子序列的模型!
可以设f[i][j]表示前i个元素选j个,且以i结尾的子序列的相邻元素之差的绝对值之和最小值
设cw[i][j]为第i个元素与第i个元素之差的绝对值,可以提前预计算出来
则不难想到递推公式:f[i][j] = min(f[i][j], f[k][j-1] + cw[k][i]),k从1~i-1,也就是看前j-1个子序列的结尾k在哪里,然后取最小值
本题是要选择r行、c列,那么可以先通过dfs枚举选择r行,再基于这r行利用dp来计算选择c列的最佳方案
设对于dfs枚举选中的r行中,
cw[i]表示所有r行第i列产生的纵向代价(代价:相邻元素之差的绝对值之和),rw[i][j]表示所有r行的第i列与第j列产生的横向代价
f[i][j]表示所有r行中前i列选择j列且以i列结尾的最小代价,则可以推出递推公式
f[i][j] = min(f[i][j], f[k][j-1] + cw[i] + rw[k][i]),k从1~i-1
初始化时:f[][]要设置为无穷大,对于f[i][1] = cw[i]
通过对行进行dfs,对列进行dp,就可以解决此题。
100分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
int n, m, r, c;
int w[N][N]; //原始矩阵
int rows[N]; //dfs选择的行
int cw[N]; //cw[i]表示dfs枚举出的一组r行第i列产生的纵向代价
int rw[N][N]; //rw[i][j]表示dfs枚举出的一组r行第i列与第j列产生的横向代价
int f[N][N]; //f[i][j]表示所有r行中前i列选择j列且以i列结尾的最小代价
int ans = 2e9;
void dp()
{
//计算cw
for(int i = 1; i <= m; i++) //每一列
{
int res = 0;
for(int j = 2; j <= r; j++) //每一行
{
res += abs(w[rows[j]][i] - w[rows[j-1]][i]);
}
cw[i] = res;
}
//计算rw
for(int i = 1; i < m; i++)
{
for(int j = i + 1; j <= m; j++) // 枚举所有不同列的组合
{
int res = 0;
for(int k = 1; k <= r; k++) //每一行
{
res += abs(w[rows[k]][i] - w[rows[k]][j]);
}
rw[i][j] = res;
}
}
//dp过程
memset(f, 0x3f, sizeof(f));
for(int i = 1; i <= m; i++)
{
f[i][1] = cw[i];
for(int j = 2; j <= c; j++)
{
for(int k = 1; k <= i - 1; k++)
{
f[i][j] = min(f[i][j], f[k][j-1] + cw[i] + rw[k][i]);
}
}
ans = min(ans, f[i][c]);
//cout << f[i][c] << endl;
}
}
void dfs_r(int k)
{
if(k > r)
{
dp();
return;
}
int start = 1;
if(k > 1) start = rows[k-1] + 1; //从上一个行号+1开始选
for(int i = start; i <= n; i++)
{
rows[k] = i;
dfs_r(k + 1);
}
}
int main()
{
cin >> n >> m >> r >> c;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> w[i][j];
dfs_r(1);
cout << ans;
return 0;
}