1. 程式人生 > 實用技巧 >P7074 方格取數

P7074 方格取數

題目描述

設有 n×m 的方格圖,每個方格中都有一個整數。現有一隻小熊,想從圖的左上角走到右下角,每一步只能向上、向下或向右走一格,並且不能重複經過已經走過的方格,也不能走出邊界。小熊會取走所有經過的方格中的整數,求它能取到的整數之和的最大值。

輸入格式

第一行有兩個整數 n,m

接下來 n 行每行 m 個整數,依次代表每個方格中的整數。

輸出格式

一個整數,表示小熊能取到的整數之和的最大值。

輸入輸出樣例

輸入 #1
3 4
1 -1 3 2
2 -1 4 -1
-2 2 -3 -1
輸出 #1
9
輸入 #2
2 5
-1 -1 -3 -2 -7
-2 -1 -4 -1 -2
輸出 #2
-10

說明/提示

樣例 1 解釋

樣例 2 解釋

資料規模與約定

  • 對於 20% 的資料,n,m≤5
  • 對於 40% 的資料,n,m≤50
  • 對於 70% 的資料,n,m≤300
  • 對於 100% 的資料,1≤n,m≤10^3
  • 方格中整數的絕對值不超過 10^4

這個題看著很簡單,做起來好難啊qwq

一開始以為是dp,結果就是dp,但是還是不會做(╥╯^╰╥)

好啦,既然不會做,那麼就一起來學習一下正解的思路吧!!

首先,在開始之前還是先來講一講我自己的思路(當然這個是錯的)

我以為這道題就只是一個簡單的dp而已,與其他dp不同的地方只是在於這個題目要求中,上下都可以走,也就是說小熊可以往復迴環走,所以我們腦海中浮現的第一想法不就是:害,加一個bool判斷一下這個格子有沒有走過不就行了嗎?還用得著其他東西嗎?於是我這麼想的,也這麼做的,最後也這麼抱靈的~~

那麼問題來了,上面的方法為什麼是錯的呢?

我們這樣來想,我們如果在小熊走過的路上進行標記,那麼結果有兩證:

1.每經過一個點標記一次小熊的路徑,但是很快我們發現這樣不行,如果這樣的話我們是按照一定的順序從上到下遍歷格子來求每個格子的最優解的,那麼如果這樣標記,我們最後的結果就只能是把所有的格子都標記下來,那麼如果之後我們更新的時候發現還存在更優的解,那麼我們就不能再次更新了,因為這個點在之前已經被標記了,所以這種標記方法不可取

2.我們給這個點設定三個變數,分別表示其是否是從上下左三個方向轉移過來的,那麼如果是這樣的話,其實根本處境還是沒有什麼改變,如果存在一條更優的路徑,那麼因為之前我們已經把這個點進行了標記,這個點就不可以再走了,所以那一條更優路徑就會被我們捨棄。所以最後不一定可以找到最優解。

至此,我發現憑藉我的可憐巴巴的一點點的智商,可能沒有辦法利用標記法來完成這道題,所以我愉快(並不)的放棄了這道題:)

正解來啦||ヽ(* ̄▽ ̄*)ノミ|Ю~~~

讓我來為你們詳細解釋一下正解~~

首先我們來觀察這個題與其他題的不同點在哪裡。其他的dp題只能走上右或者下右,但是這個題卻可以走上下右,所以憑藉單純的dp肯定是無法解決這個問題的。

然後,對於每一列,由於題目規定我們“不能重複經過已經走過的方格”,所以我們在每一列上只能向上走或者向下走,而不能上下來回走,這樣就重複了。而只能走右不能走左,則表示對於每一列來說,當前一列的最大值一定是從左邊那一列推出來的,所以我們可以按照一列來進行劃分,把每一列當成一個整體來求解。

我們再來整理一下上面思路的來源:

1.只能向右走—>可以按照列來劃分,從左到右對每列進行求解

2.不能重複經過已經走過的方格—>每個方格只能向上或者向下走—>設定兩個變數來儲存從上面向下走的最優解和從下面向上走得最優解

這樣這個題的思路就很明白了,我們設定兩個陣列 up[i][j] 和 down[i][j] ,分別表示從下面走上來到 i 行 j 列的最大值和從上面走下來到 i 行 j 列的最大值。

當然最大值也是有可能從左邊來的,那麼我們還需不需要再開設一個變數 left[i][j] 表示從左走到右的最大值呢?

答案是不需要的,因為從上文分析中我們可以得出結論,我們只能從左走到右,方向是唯一確定的,所以我們只需要用我們的 f[i][j] 總變數來表示第 i 行 j 列的點的最大值,而不需要再設定一個變量表示從左走到右的最大值(當然你也可以理解為這兩個變數所表達的意思其實是相同的)。

那麼狀態轉移方程就可以寫出來了(a[i]][j] 表示 i 行 j 列的元素值)

up[i][j]=max(up[i+1][j],f[i][j-1])+a[i][j];

down[i][j]=max(down[i-1][j],f[i][j-1])+a[i][j];

f[i][j]=max(up[i][j],down[i][j]);

我們發現如果這樣設定狀態,這道題的確可以AC,但是我麼在前面的分析中其實還有一點沒有用到“可以把列當成一個整體來處理”。

考慮優化:
其實對於當前位置的元素,它的 down 和 up 只由上一列的元素的最大值轉移而來所以我們完全可以進行壓維操作。

設定變數 up ,down ,f 分別為 up[i] , down[i] , f[i] ,分別表示這一行從下面走上來的最大值,這一行從上面走下來的最大值,這一行的最大值(注意,因為我們可以把列當做一個整體,所以可以把一整列進行壓縮,但是行是不可以壓縮的,所以最後設定的一維變臉都是用來儲存行最優解的)。

那麼狀態轉移方程就變成了:(不行,我覺得下面的方程需要我來解釋一下了,畢竟當時我就是卡在這裡出不來的qwq)

up[i]=max(f[i],up[i+1])+a[i][j]; //這一行從下到上的最大值要麼是從下一行的up最大值轉移來的,要麼是從這一行的最大值( f[i] )轉移來的.為什麼還會從 f 轉移過來呢?因為我們上面就已經說過了,我們在這裡可以把 f 看做 left 變數,{因為 f 是從左到右更新的},所以當前最大值也可以從 f[i] 轉移過來。

down[i]=max(f[i],down[i-1])+a[i][j]; //同理,這一行從上到下的最大值要麼是從上一行down的最大值轉移過來的,要麼是從這一行的最大值 f[i] 轉移過來的。

f[i]=max(up[i],down[i]); //因為我們每更新完一次down 和 up ,這一行的最大值就有可能發生變化,所以 f 也應當更新,但是問題有來了,為什麼 f 不需要和自己進行比較一下呢?有沒有可能當前的 down 和 up 都沒有 f 自己的值大呢?答案是不可能的,因為down 和 up 就是從 f 轉移過來的,在轉移結束之前他們已經和 f 進行過比較,也就是說他們是和f 進行比較之後,又加上了當前方格中的元素,才轉移過來的,所以up 和down 的值一定比 f 大,所以我們只需要比較up 和down 的大小即可。

好了,囉裡囉嗦半天終於把所有事情都給解釋清楚了,但是程式碼中還存在一個令人迷惑的地方

    /*for(int j=2;j<m;j++)*/{
        memset(down,0,sizeof(down));
        memset(up,0,sizeof(up));
        down[1]=f[1]+a[1][j];up[n]=f[n]+a[n][j];
        for(int i=2;i<=n;i++) down[i]=max(f[i],down[i-1])+a[i][j];
        for(int i=n-1;i>=1;i--) up[i]=max(f[i],up[i+1])+a[i][j];
        for(int i=1;i<=n;i++) f[i]=max(down[i],up[i]);
    }

看見變成綠色的部分了嗎?為什麼對於列的迴圈我們只對2到m-1進行更新,而不對1和m進行更新呢?我們來自習思考一下,其實對於第一列和最後一列來說,他們在我們心中都是特殊列。什麼叫特殊列呢?對於第一列,我們發現它不能從左邊更新到右邊,對於最後一列,我們發現它不能從下面更新到上面,所以我們需要對它們進行單獨處理,因為這樣幹講可能對於理解效果不太好,所以我們來藉助AC程式碼理解一下:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,m;
int a[1001][1001];
long long up[1005],down[1005],f[1005];
inline int read(){
    int f=1,x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return f*x;
}
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        a[i][j]=read();
    f[1]=a[1][1];
    for(int i=2;i<=n;i++) f[i]=f[i-1]+a[i][1];//設定f的初始值,其實也相當於對第一列進行特殊處理
    //但是為了增強程式碼的客觀性,所以我們在這裡先同樣對第一列特殊化處理,只更新第二列的值 
    for(int j=2;j<m;j++){//迴圈更新列 
        memset(down,0,sizeof(down));
        memset(up,0,sizeof(up));
        down[1]=f[1]+a[1][j];up[n]=f[n]+a[n][j];
        for(int i=2;i<=n;i++) down[i]=max(f[i],down[i-1])+a[i][j];
        for(int i=n-1;i>=1;i--) up[i]=max(f[i],up[i+1])+a[i][j];
        for(int i=1;i<=n;i++) f[i]=max(down[i],up[i]);//我們在這裡對於第一列,最後一列的值進行特殊處理,
        //因為上面我們已經把down[1] up[n]更新完了,所以對於第一列來說它的值就是從上到下遍歷過後的值,也就是說對於第一列
        //而言,它只能從上到下走(自己思考一下?)而不能有其他路徑,所以我們可以憑藉down 和up 對f 進行更新 
    }
    f[1]+=a[1][m];//處理一下細節 
    for(int i=2;i<=n;i++) f[i]=max(f[i],f[i-1])+a[i][m];//然後問題又來了,最後一列我們還沒有處理呢,還記得f的定義嗎?
    //走到當前格子的最大值,而最後一列只有可能從上面或者從左面轉移過來,所以取從上面走下來,從左面走過來的最大值,加上
    //當前格中元素的值就可以得到當前格子中的最大值 
    printf("%lld",f[n]);//輸出結果end~~ :) 
    return 0;
}

---------end----------