zoukankan      html  css  js  c++  java
  • 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~

  • 相关阅读:
    如何简单实现一个react组件
    css实现弹框
    flex实现流式布局
    classnames的简单使用
    css处理内容溢出
    webpack学习
    github+hexo搭建,运行hexo g报错
    (附代码和截图)spring基于注解的java定时任务功能实现
    关于Cookie、session和localStorage、以及sessionStorage之间的区别和联系,超详细
    今天向大家推荐一个很强的编辑器——notepad++,没有插件plugin Manager的解决办法
  • 原文地址:https://www.cnblogs.com/xcg123/p/11128486.html
Copyright © 2011-2022 走看看