P2258 子矩阵
原题链接 https://www.luogu.org/problemnew/show/P2258
高中学长lwy给我们讲了下这道难题。
其实这道题的思路很简单:暴力枚举每种行和列的排列情况,求出最小的分数;显然这道蓝题是不会这么轻易让你AC的,好像只能得60分,所以我们考虑加上DP做法;
做法的结构大致是这样的:首先枚举其行的排列情况(类似全排列,只是元素个数确定),然后,对于每一种行的排列情况,DP出它列的最优情况,然后取所有行的排列情况的最优情况的最小值。
这样的话时间复杂度会大大降低的。
代码实现
我们先开几个数组:
a [ i ][ j ]:矩阵第 i 行第 j 列的数;
dp [ i ][ j ]:枚举列要用,表示前 i 列我们已经选了 j 列所得到的最小分值,注意要选第 i 列(这 j 列中包含第 i 列);
ver [ i ]:第 i 列上下绝对值差的和;
del [ i ][ j ]:第 i 列和第 j 列左右绝对值差的和;
hang [ i ]:枚举的子矩阵的第 i 行在原大矩阵的行数;
大体代码思路:
我们先一遍dfs,找出行的全排列,当我们找够了 r 行的时候,我们去再去找所有的 c 列,然后我们一遍DP求出当前行情况的最小分值,然后接着回到 dfs 找其他的行排列,直到找出所有的行排列为止;
先看一下 dfs 的代码吧,处理的是找行的全排列的过程:
void dfs(int now, int pos) //我们当前正在选第now行,这一行在原矩阵中是第pos行 { if (now == r + 1) //如果超出了r行,说明我们已经选完了r行,开始进行dp操作 { Dp(); return; } if (pos == n + 1) //判断是否在原矩阵范围内 return; for (int i = pos; i <= n; i++)//从第pos行开始往后选 hang[now] = i, dfs(now + 1, i + 1); //继续往下选 //hang[now]=i :在子矩阵里的第now行就是原矩阵的第i行 }
dfs 的过程应该不难理解,接下来就是最重要的 dp 过程了!
我们在用 dp 求当前行排列的最小分值之前,我们还要有一步预处理操作,就是将ver,del 数组先处理出来;
预处理ver,del 数组
这个其实很好实现,我们只要严格套上面这两个数组的定义就好啦。
先看ver 数组怎么弄,我们回过头来看这个数组的定义:
ver [ i ]:第 i 列上下绝对值差的和;
你看,我们现在把其中一种行排列给确定下来了,接下来要做的就是枚举每一列,然后我们再枚举子矩阵的每一行,套定义用下一行的值减去上一行的值再取绝对值就好啦(这里的行都是指的子矩阵),注意求和:
for (int i = 1; i <= m; i++) //枚举每一列 for (int j = 2; j <= r; j++) //枚举子矩阵的每一行 ver[i] += abs(a[hang[j]][i] - a[hang[j - 1]][i]) ; //利用hang数组回到原矩阵去寻找值
再看一下del数组怎么弄,我们再回过头来看这个数组的定义:
del [ i ][ j ]:第 i 列和第 j 列左右绝对值差的和;
你会发现这个好像跟处理 ver 数组的方法差不多嘛:先枚举每一列 i,然后再来一层枚举列 k,不过这时候 k > i(因为我们要做左右差的绝对值和),然后枚举子矩阵的每一行,第 k 列的值减去第 i 列的值再取绝对值就好啦:
for (int i = 1; i <= m; i++) //枚举每一列 i for (int k = i + 1; k <= m; k++) //再找 i 之后的列 for (int j = 1; j <= r; j++) //枚举子矩阵的每一行 del[i][k] += abs(a[hang[j]][k] - a[hang[j]][i]); //套定义求出del数组
炒鸡重要的dp
我们先看我们 dp 时要用到的 dp 数组的定义(万物先看定义再思考方法嘛):
dp [ i ][ j ]:枚举列要用,表示前 i 列我们已经选了 j 列所得到的最小分值,注意要选第 i 列(这 j 列中包含第 i 列);
考虑到我们已经得到了行的一种排列(行排列已确定),所以我们 dp 过程主要考虑怎么选列;
第一层大循环一定是枚举所有的列,含义是:我们一共找了 i 列;
考虑到我们始终没有涉及到子矩阵要有 c 列,所以在 dp 的时候我们要注意只选 c 列!
那么第二层循环也就出来了:我们枚举1~c,表示在前 i 列(第一层循环的变量)中我们已经选上了 j 列;
第三层循环不是很好想,由于我们选的这个子矩阵的行和列不一定在原矩阵是连续的,所以作为已经被选上的第 i 列作为子矩阵的其中一列,我们并不知道子矩阵的上一列在原矩阵中的位置是否和 i 是相邻的,所以我们有必要来枚举这个距离,由此可以得出第三层循环:我们枚举 k,表示在我们选择第 i 列作为子矩阵的其中一列时,第 i - k 列也同样被选作为子矩阵的其中一列,且这两列在子矩阵中是相邻的,由此我们就可以得出来状态转移方程了(艰辛啊):
dp[i][j] = min(dp[i][j], dp[i - k][j - 1] + ver[i] + del[i - k][i]); //第 i-k 列是第 i 列的上一列,所以应该要选择j-1列 //加上新选的第 i 列的贡献:第i列的上下绝对值差的和,第 i 列与第 i-k 列的左右绝对值差的和
考虑 k 的范围:首先我们是从第 i - k列中选了 j - 1列,那么 i - k 一定得大于等于 j - 1 吧(不然怎么选),其次我们要从第 i 列往前找 k 列来作为邻列,那么显然 i - k > 0
考虑边界条件
注意到我们 dp 数组的第一维是要选上该列的,那么如果我们第二维只选一条列的话,那么肯定是选正在枚举的这一列的,所以就有如下边界设置:
for (int i = 1; i <= m; i++) dp[i][1] = ver[i]; //如果只选择一列肯定是选自己
上 dp 代码:
for (int i = 1; i <= m; i++) //我们已经找了i列 for (int j = 1; j <= c; j++) //我们一共只选择c列 for (int k = 1; i - k > 0 && i - k >= j - 1; k++) //找i的邻列 dp[i][j] = min(dp[i][j], dp[i - k][j - 1] + ver[i] + del[i - k][i]); //状态转移方程 //第 i-k 列是第 i 列的上一列,所以应该要选择j-1列 //加上新选的第 i 列的贡献:第i列的上下绝对值差的和,第 i 列与第 i-k 列的左右绝对值差的和
考虑答案选择
我们最终肯定是要只选择 c 列的,所以我们要枚举每种能够选择 c 列的情况,选择最小得分即可:
for (int i = c; i <= m; i++)//从c行后取最小值 ans = min(ans, dp[i][c]);
完整AC代码(累死QwQ~):
#include <algorithm> #include <iostream> #include <cstring> #include <cstdio> #include <cmath> using namespace std; int n, m, r, c, ans = 2147483647; int a[19][19], hang[19], dp[19][19]; int ver[19], del[19][19]; /* a[i][j]:矩阵第i行第j列的数; dp[i][j]:枚举列要用,表示前i列我们已经选了j列所得到的最小分值,注意要选第i列(这j列中包含第i列); ver[i]:第i列上下绝对值差的和; del[i][j]:第i列和第j列左右绝对值差的和; hang[i]:枚举的子矩阵的第i行在原大矩阵的行数; */ inline void Dp() { memset(dp, 123, sizeof(dp)); //注意不要忘了清空 memset(ver, 0, sizeof(ver)); memset(del, 0, sizeof(del)); for (int i = 1; i <= m; i++) //枚举每一列 for (int j = 2; j <= r; j++) //枚举子矩阵的每一行 ver[i] += abs(a[hang[j]][i] - a[hang[j - 1]][i]) ; //利用hang数组回到原矩阵去寻找值 for (int i = 1; i <= m; i++) //枚举每一列 i for (int k = i + 1; k <= m; k++) //再找 i 之后的列 for (int j = 1; j <= r; j++) //枚举子矩阵的每一行 del[i][k] += abs(a[hang[j]][k] - a[hang[j]][i]); //套定义求出del数组 for (int i = 1; i <= m; i++) dp[i][1] = ver[i]; //如果只选择一列肯定是选自己 for (int i = 1; i <= m; i++) //我们已经找了i列 for (int j = 1; j <= c; j++) //我们一共只选择c列 for (int k = 1; i - k > 0 && i - k >= j - 1; k++) //找i的邻列 dp[i][j] = min(dp[i][j], dp[i - k][j - 1] + ver[i] + del[i - k][i]); //状态转移方程 //第 i-k 列是第 i 列的上一列,所以应该要选择j-1列 //加上新选的第 i 列的贡献:第i列的上下绝对值差的和,第 i 列与第 i-k 列的左右绝对值差的和 for (int i = c; i <= m; i++)//从c行后取最小值 ans = min(ans, dp[i][c]); } void dfs(int now, int pos) //我们当前正在选第now行,这一行在原矩阵中是第pos行 { if (now == r + 1) //如果超出了r行,说明我们已经选完了r行,开始进行dp操作 { Dp(); return; } if (pos == n + 1) //判断是否在原矩阵范围内 return; for (int i = pos; i <= n; i++)//从第pos行开始往后选 hang[now] = i, dfs(now + 1, i + 1); //继续往下选 //hang[now]=i :在子矩阵里的第now行就是原矩阵的第i行 } int main() { scanf("%d%d%d%d", &n, &m, &r, &c); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]); dfs(1, 1); printf("%d", ans); }
蟹蟹你的观看QwQ~