1. 程式人生 > >『進階DP專題:二維DP初步』

『進階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)走來,那麼由加法原理可知,走到該點的路徑數就是走到以上兩點的路徑數相加。所以,根據題意,我們得出狀態轉移方程:

i][j]={sum(f[i&#x2212;1][j],f[i][j&#x2212;1])((i,j)&#x672A;&#x88AB;&#x9A6C;&#x63A7;&#x5236;)0((i,j)&#x88AB;&#x9A6C;&#x63A7;&#x5236;)" role="presentation"> f [
i ] [ j ] = { s u m ( f [ i 1 ] [ j ] , f [ i ] [ j 1 ] ) ( ( i , j ) ) 0 ( ( i , j ) )

我們只需處理出哪些點被馬控制即可,這個問題只需在一個新的二維陣列根據題意進行特殊標記即可。至於初始值,第一行和第一列的所有位置都只有一種走法,即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 ] = m i n ( 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 ] = m a x ( f [ i ] [ j ] , f [ k ] [ j ] + f [ i k ] [ j ] ) ( , k = 1... i 1 ) f [ i ] [ j ] = m a x ( 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,這樣我們就能實現狀態轉移,狀態轉移方程如下:

f 1 [ i ] [ j ] = m i n ( f 1 [ i 1 ] [ j 1 ] , l e