P2258 子矩阵

Posted xcg123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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~

以上是关于P2258 子矩阵的主要内容,如果未能解决你的问题,请参考以下文章

P2258 子矩阵

luogu P2258 子矩阵 |动态规划

NOIP 普及组 T4 子矩阵(--洛谷P2258)

P2258 子矩阵——搜索+dp

题解 P2258 子矩阵

P2258 子矩阵