『进阶DP专题:二维DP初步』

Posted parsnip

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了『进阶DP专题:二维DP初步』相关的知识,希望对你有一定的参考价值。


<更新提示>

<第一次更新>


<正文>

二维动态规划

初步

二维动态规划并不是指动态规划的状态是二维的,而是指线性动态规划的拓展,由线性变为了平面,即在一个平面上做动态规划。

例题

马拦过河卒

题目描述

 棋盘上A点有一个过河卒,需要走到目标B点。卒行走的规则:可以向下、或者向右。同时在棋盘上C点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。   棋盘用坐标表示,A点(0, 0)、B点(n, m)(n, m为不超过15的整数),同样马的位置坐标是需要给出的。现在要求你计算出卒从A点能够到达B点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。
技术分享图片
输入格式

一行四个数据,分别表示B点坐标和马的坐标。
输出格式

一个数据,表示所有的路径条数。
样例数据

input

6 6 3 3

output

6

数据规模与约定

时间限制:1s

空间限制:256MB

分析

这是一道二维动态规划的入门题,其考点为加法原理。我们直接设置状态f[i][j]代表走到棋盘上坐标为(i,j)的点的路径条数。那么由于卒只能向下或向右走,所以坐标为(i,j)的点只能由(i-1,j),(i,j-1)走来,那么由加法原理可知,走到该点的路径数就是走到以上两点的路径数相加。所以,根据题意,我们得出状态转移方程:[f[i][j]=egin{cases}sum(f[i-1][j],f[i][j-1])((i,j)未被马控制)\0((i,j)被马控制)end{cases}]
我们只需处理出哪些点被马控制即可,这个问题只需在一个新的二维数组根据题意进行特殊标记即可。至于初始值,第一行和第一列的所有位置都只有一种走法,即f[i][0]=f[0][i]=1,当然,第一行和第一列里被马控制的点也是不能走的,那么最终的答案就是f[n][m]。

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int Map[1080][1080]={},Ma[1080][1080]={},x,y,n,m;
int dx[8]={2,2,1,1,-1,-1,-2,-2};
int dy[8]={-1,1,-2,2,-2,2,-1,1};
int main()
{
    cin>>n>>m>>x>>y;
    Ma[x][y]=-1;
    for(int i=0;i<8;i++)
    {
        if(x+dx[i]>=0&&y+dy[i]>=0)Ma[x+dx[i]][y+dy[i]]=-1;
    }
    Map[0][0]=1;
    for(int i=0;i<=n;i++)if(Ma[i][0]==0)Map[i][0]=1;
    else 
    {
        for(int j=i;j<=n;j++)Ma[j][0]=-1;
        break;
    }
    for(int i=0;i<=m;i++)if(Ma[0][i]==0)Map[0][i]=1;
    else 
    {
        for(int j=i;j<=m;j++)Ma[0][j]=-1;
        break;
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            if(Ma[i][j]!=-1)
            {
                Map[i][j]=Map[i-1][j]+Map[i][j-1];
            }
        }
    }
    cout<<Map[n][m]<<endl;
    return 0;
} 
农田个数

题目描述

你的老家在农村。过年时,你回老家去拜年。你家有一片N×M农田,将其看成一个N×M的方格矩阵,有些方格是一片水域。你的农村伯伯听说你是学计算机的,给你出了一道题: 他问你:这片农田总共包含了多少个不存在水域的正方形农田。

两个正方形农田不同必须至少包含下面的两个条件中的一条:

边长不相等

左上角的方格不是同一方格

输入格式

输入数据第一行为两个由空格分开的正整数N、M(1<=m< n <=1000)

第2行到第N+1行每行有M个数字(0或1),描述了这一片农田。0表示这个方格为水域,否则为农田(注意:数字之间没有空格,而且每行不会出现空格)
输出格式

满足条件的正方形农田个数。
样例数据

input

3 3
110
110
000

output

5

样例解释 边长为1的正方形农田有4块 边长为2的正方形农田有1块 合起来就是5块
数据规模与约定

时间限制:1s

空间限制:256MB

分析

这题也是明显的平面二维dp,简单的方法就是直接设置状态f[i][j]代表以格子(i,j)为右下角的正方形个数,那么显然,他也代表了以(i,j)为右下角构成的正方形的最大边长,我们考虑如何求解。
先考虑一种简单情况,如果(i-1,j),(i,j-1),(i-1,j-1)三个点均能作为一个边长为k的正方形的右下角,画图可知,那么点(i,j)一定能作为一个边长为(k+1)的正方形的右下角。
技术分享图片
(此时k=2,(i,j)一定能作为一个边长为3的正方形的右下角)
其实,简单推理可知,点(i,j)作为右下角能构成的正方形的最大边长即为之前提到三点中((i-1,j),(i,j-1),(i-1,j-1))能够成正方形的最大边长的最小值加一,那么以点(i,j)作为右下角的正方形个数也是该值。即状态转移方程为:
[f[i][j]=min(f[i-1][j],f[i][j-1],f[i-1][j-1])+1]
我们可以两重循环暴力求解f数组,对f数组的每一个值求和即为答案。

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[1008][1008]={},ans=0;
string Map[1008];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>Map[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<m;j++)
        {
            if(Map[i][j]==‘1‘)
            {
                f[i][j]=min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1;
                ans+=f[i][j];
            }
        }
    }   
    cout<<ans;
}
矩阵切割

题目描述

给你一个矩阵,其边长均为整数。你想把矩阵切割成总数最少的正方形,其边长也为整数。切割工作由一台切割机器完成,它能沿平行于矩形任一边的方向,从一边开始一直切割到另一边。对得到的矩形再分别进行切割。
输入格式

输入文件中包含两个正整数,代表矩形的边长,每边长均在1—100之间。
输出格式

输出文件包含一行,显示出你的程序得到的最理想的正方形数目。
样例数据

input

5 6

output

5

样例解释
技术分享图片

数据规模与约定

时间限制:1s

空间限制:256MB

分析

这道题也是在平面上动态规划,即二维dp。不过这道题的样例很良心,显然不能每一次直接切割最大的正方形。设置状态f[i][j]代表1到i,1到j构成的矩形的最小切割数。直接能得出的初始条件就是f[i][i]的最优值一定是1,因为这是一个正方形,可以直接切割。那么不是这种情况时,这个矩形一定被分割为若干个更小的矩形或正方形,更小的矩形也是如此。我们只要在i,j之间不断枚举分割点k就能求得最大值,即进行状态转移。所以状态转移方程如下:
[f[i][j]=max(f[i][j],f[k][j]+f[i-k][j])(横向切割,k=1...i-1)\f[i][j]=max(f[i][j],f[i][k]+f[i][j-k])(纵向切割,k=1...j-1)]
三重循环进行转移,f[n][m]即为答案。

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
inline void read(int &k)
{
    int x=0,w=0;char ch;
    while(!isdigit(ch))w|=ch==‘-‘,ch=getchar();
    while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    k=(w?-x:x);return; 
} 
inline void print(int x)
{
    int y=10,len=1;
    while(y<=x)y*=10,len++;
    while(len--){y/=10,putchar(x/y+48),x%=y;}
}
int n,m,f[180][180]={};
int main()
{
    read(n),read(m);
    memset(f,0x3f,sizeof(f));
    for(register int i=1;i<=min(n,m);i++)f[i][i]=1;
    for(register int i=1;i<=n;i++)
    {
        for(register int j=1;j<=m;j++)
        {
            for(register int k=1;k<i;k++)
            {
                f[i][j]=min(f[i][j],f[k][j]+f[i-k][j]);
            }
            for(register int k=1;k<j;k++)
            {
                f[i][j]=min(f[i][j],f[i][k]+f[i][j-k]); 
            } 
        }
    }
    print(f[n][m]);
    return 0;
}
创意吃鱼

题目描述

可爱猫猫家里长方形大池子中有很多鱼,她开始思考:到底要以何种方法吃鱼呢(猫猫就是这么可爱,吃鱼也要想好吃法 ^_*)。她发现,把大池子视为01矩阵(0表示对应位置无鱼,1表示对应位置有鱼)有助于决定吃鱼策略。

在代表池子的01矩阵中,有很多的正方形子矩阵,如果某个正方形子矩阵的某条对角线上都有鱼,且此正方形子矩阵的其他地方无鱼,猫猫就可以从这个正方形子矩阵“对角线的一端”下口,只一吸,就能把对角线上的那一队鲜鱼吸入口中。    猫猫是个贪婪的家伙,所以她想一口吃掉尽量多的鱼。请你帮猫猫计算一下,她一口下去,最多可以吃掉多少条鱼?
输入格式

  第一行有两个整数n和m(n,m≥1),描述池塘规模。接下来的n行,每行有m个数字(非“0”即“1”)。每两个数字之间用空格隔开。
输出格式

只有一个整数——猫猫一口下去可以吃掉的鱼的数量,占一行,行末有回车。
样例数据

input

4 6
0 1 0 1 0 0
0 0 1 0 1 0
1 1 0 0 0 1
0 1 1 0 1 0

output

3

数据规模与约定

对于30%的数据,有n,m≤100

对于60%的数据,有n,m≤1000

对于100%的数据,有n,m≤2500

时间限制:1s

空间限制:256MB

分析

这道题求的是最大全1对角线长度,且要求对角线所在正方形其他地方全是0。本质上这道题和农田个数是相同的。先考虑左上角到右下角的对角线:我们设f1[i][j]代表以点(i,j)为右下角该种对角线的最大长度。分析后可以得知约束该值的和农田个数一题相同,有三个值。分别是f1[i-1][j-1],点(i,j)左边连续0的个数,点(i,j)上门连续0的个数,f1[i][j]即为他们三个数的最小值加一。后两个值首先保证了对角线所在正方形中最下面一行和最右边一列除右下角外全是0,而f1[i-1][j-1]则保证了倒数第二行和右边倒数第二列除他本身外也全是0,而这个值需要1f[i-2][j-2]保证,递归下去,就能够保证这个对角线所在的正方形中除了对角线以外其他值全都是0,符合题意要求。也可以根据定义,直接认为f[i-1][j-1]直接保证了对角线长度为该值时,所在正方形中其余元素全为0。我们可以预处理left[i][j]代表第i行第j个数以前有多少连续的0,up[i][j]代表第i列第j个数以上有多少个连续的0,这样我们就能实现状态转移,状态转移方程如下:
[f1[i][j]=min(f1[i-1][j-1],left[i][j],up[i][j])+1]
那么同理,f2[i][j]代表以以点(i,j)为左下角从右上角到左下角的对角线的最大长度,预处理出right数组就能实现状态转移:
[f2[i][j]=min(f2[i-1][j+1],right[i][j],up[i][j])+1]
在每一个点中寻找f1,f2的最大值,本题完美解决。

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
inline void read(int &k)
{
    int x=0,w=0;char ch;
    while(!isdigit(ch))w|=ch==‘-‘,ch=getchar();
    while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    k=(w?-x:x);return; 
} 
inline void print(int x)
{
    int y=10,len=1;
    while(y<=x)y*=10,len++;
    while(len--){y/=10,putchar(x/y+48),x%=y;}
}
int n,m,Map[3000][3000]={},Right[3000][3000]={},Left[3000][3000]={},Up[3000][3000]={};
int f1[3000][3000]={},f2[3000][3000]={},ans=0;
int main()
{
    freopen("meal.in","r",stdin);
    freopen("meal.out","w",stdout);
    read(n),read(m);
    for(register int i=1;i<=n;++i)
    {
        for(register int j=1;j<=m;++j)
        {
            read(Map[i][j]);
        }
    }
    for(register int i=1;i<=n;++i)
    {
        for(register int j=2;j<=m;++j)
        {
            if(!Map[i][j-1])Left[i][j]=Left[i][j-1]+1;
        }
        for(register int j=m-1;j>=1;--j)
        {
            if(!Map[i][j+1])Right[i][j]=Right[i][j+1]+1;
        }
    }
    for(register int i=1;i<=m;++i)
    {
        for(register int j=2;j<=n;++j)
        {
            if(!Map[j-1][i])Up[j][i]=Up[j-1][i]+1;
        }
    }
    for(register int i=1;i<=n;++i)
    {
        for(register int j=1;j<=m;++j)
        {
            if(Map[i][j])f1[i][j]=min(f1[i-1][j-1],min(Left[i][j],Up[i][j]))+1,ans=max(ans,f1[i][j]);
        }
    }
    for(register int i=1;i<=n;++i)
    {
        for(register int j=m;j>=1;--j)
        {
            if(Map[i][j])f2[i][j]=min(f2[i-1][j+1],min(Right[i][j],Up[i][j]))+1,ans=max(ans,f2[i][j]);
        }
    }
    cout<<ans<<endl;
    return 0;
}

总结

二维dp的标志是将线性扩展到了平面。而解决该类问题,需要我们更恰当的设置状态,仔细思考如何建立状态转移方程,求解问题。其核心思想还是在于将各个阶段联系起来,需要多做题训练。


<后记>


<废话>



































以上是关于『进阶DP专题:二维DP初步』的主要内容,如果未能解决你的问题,请参考以下文章

动态规划专题

算法竞赛专题解析(15):DP应用--状态压缩DP

算法竞赛专题解析(15):DP应用--状态压缩DP

UVA-624 dp专题A

算法面试专题-动态规划

Leetcode之动态规划(DP)专题-53. 最大子序和(Maximum Subarray)