洛谷P2858 奶牛零食 題解 區間DP入門題

題目大意:c++

約翰常常給產奶量高的奶牛發特殊津貼,因而很快奶牛們擁有了大筆不知該怎麼花的錢.爲此,約翰購置了 \(N(1 \le N \le 2000)\) 份美味的零食來賣給奶牛們.天天約翰售出一份零食.固然約翰但願這些零食所有售出後能獲得最大的收益.這些零食有如下這些有趣的特性:spa

  • 零食按照 \(1 \cdots N\) 編號,它們被排成一列放在一個很長的盒子裏.盒子的兩端都有開口,約翰天天能夠從盒子的任一端取出最外面的一個.
  • 與美酒與好吃的奶酪類似,這些零食儲存得越久就越好吃.固然,這樣約翰就能夠把它們賣出更高的價錢.
  • 每份零食的初始價值不必定相同.約翰進貨時,第 \(i\) 份零食的初始價值爲 \(V_i(1 \le V_i \le 1000)\) .
  • \(i\) 份零食若是在被買進後的第 \(a\) 天出售,則它的售價是 \(V_i \times a\) .

\(V_i\) 是從盒子頂端往下的第i份零食的初始價值.約翰告訴了你全部零食的初始價值,並但願你能幫他計算一下,在這些零食全被賣出後,他最多能獲得多少錢.code

解題思路:遞歸

咱們定義狀態 \(f[L][R]\) 爲將區間 \([L,R]\) 依次去空能可以得到最多的錢。ci

那麼咱們能夠發現咱們的答案就是 \(f[1][n]\) ,那麼怎麼求解 \(f[1][n]\) 呢?先不急,聽我細細道來~it

假設咱們如今要求解 \(f[L][R]\) ,那麼咱們能夠發現,對於區間 \([L,R]\) ,咱們取走一份零食的方案只有兩種:for循環

  • 方案一:從左邊取走 \(V_L\) ,而後狀態變成了 \(f[L+1][R]\)
  • 方案二:從右邊取走 \(V_R\) ,而後狀態變成了 \(f[L][R-1]\)

對於區間 \([L,R]\) ,首先咱們要肯定咱們取走的零食(不管是方案一仍是方案二)是第幾份取走的零食?class

咱們能夠發現,\([L,R]\) 區間的左邊有 \(L-1\) 份零食在以前被取走了,右邊有 \(n-R\) 份零食在以前被取走了,因此咱們如今取的零食是第 \(L-1 + n-R + 1 = n+L-R\) 份。搜索

因此採用第一種方案可以得到的最多的錢是循環

\[f[L+1][R] + V_l \times (n+L-R)\]

採用第二種方案可以得到的最多的錢是

\[f[L][R-1] + V_R \times (n+L-R)\]

那咱們如今要求解的狀態 \(f[L][R]\) 應該是兩種方案的較大值,因此咱們能夠獲得最終的狀態轉移方程以下:

\[f[L][R] = \max(f[L+1][R] + V_l \times (n+L-R), f[L][R-1] + V_R \times (n+L-R))\]

固然,還須要注意的狀況是咱們的邊界條件,即:區間長度爲 \(1\) 時的狀況,此時,對於全部的區間 \([i,i]\) ,第 \(i\) 份零食都是最後取走的(即第 \(n\) 份被取走的),因此

\[f[i][i] = V_i \times n\]

基於上面的推導,咱們能夠經過記憶化搜索的形式實現咱們的主要代碼:

int dfs(int L, int R) {
    if (f[L][R])    // 記憶化操做
        return f[L][R];
    if (L == R)     // 邊界條件
        return V[L] * n;
    return f[L][R] = max(dfs(L+1, R)+V[L]*(n+L-R), dfs(L, R-1)+V[R]*(n+L-R));
}

記憶化搜索的思想仍是很是對應咱們人腦的思考方式的,咱們能夠發現,這個程序主要分爲三部分:

首先咱們的 dfs(L,R) 就是爲了返回 \(f[L][R]\),因此它:

  • 首先判斷是否是已經計算過了(經過 \(f[L][R]\) 是否爲 \(0\) 來判斷),若是已經計算過了直接發揮結果;
  • 其次判斷是否是邊界條件(經過 \(L\) 是否等於 \(R\) 來判斷),若是是邊界條件直接返回 \(V_L \times n\)
  • 最後計算並記錄值,以便下一次計算的時候直接返回。

使用記憶化搜索的完整代碼以下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2020;
int n, V[maxn], f[maxn][maxn];
int dfs(int L, int R) {
    if (f[L][R])    // 記憶化操做
        return f[L][R];
    if (L == R)     // 邊界條件
        return V[L] * n;
    return f[L][R] = max(dfs(L+1, R)+V[L]*(n+L-R), dfs(L, R-1)+V[R]*(n+L-R));
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i ++) cin >> V[i];
    cout << dfs(1, n) << endl;
    return 0;
}

咱們也能夠採用通常形式來解決這個問題(通常形式和記憶化搜索形式的思路都是同樣的,只不過一個是直接for循環順着來,另外一個是遞歸着來,要注意區分和類比)。

咱們能夠發現,大區間(即區間長度較大的區間)對應的狀態都是經過小區間(即區間長度較小的區間)對應的狀態推導出來的,因此咱們只要從小到大遍歷區間長度,再遍歷區間左座標,計算對應狀態便可。
主要代碼以下:

for (int l = 1; l <= n; l ++) { // 從小到大遍歷區間長度l
    for (int i = 1; i+l-1 <= n; i ++) { // 遍歷區間左邊界i
        int j = i+l-1;  // 經過左邊界i和區間長度l得到區間右邊界j
        if (l == 1) f[i][j] = V[i]*n;   // 邊界條件直接返回結果
        else f[i][j] = max(f[i+1][j] + V[i]*(n+i-j), f[i][j-1] + V[j]*(n+i-j)); // 不然,經過狀態轉移方程推導
    }
}

通常形式的完整實現代碼以下:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2020;
int n, V[maxn], f[maxn][maxn];
int main() {
    cin >> n;
    for (int i = 1; i <= n; i ++) cin >> V[i];
    for (int l = 1; l <= n; l ++) { // 從小到大遍歷區間長度l
        for (int i = 1; i+l-1 <= n; i ++) { // 遍歷區間左邊界i
            int j = i+l-1;  // 經過左邊界i和區間長度l得到區間右邊界j
            if (l == 1) f[i][j] = V[i]*n;   // 邊界條件直接返回結果
            else f[i][j] = max(f[i+1][j] + V[i]*(n+i-j), f[i][j-1] + V[j]*(n+i-j)); // 不然,經過狀態轉移方程推導
        }
    }
    cout << f[1][n] << endl;
    return 0;
}
相關文章
相關標籤/搜索