每日四題打卡-4.22:區間DP-石子合併/線性DP數字三角形/揹包問題

區間DP-石子合併

設有N堆石子排成一排,其編號爲1,2,3,…,N。

每堆石子有一定的質量,可以用一個整數來描述,現在要將這N堆石子合併成爲一堆。

每次只能合併相鄰的兩堆,合併的代價爲這兩堆石子的質量之和,合併後與這兩堆石子相鄰的石子將和新堆相鄰,合併時由於選擇的順序不同,合併的總代價也不相同。

例如有4堆石子分別爲 1 3 5 2, 我們可以先合併1、2堆,代價爲4,得到4 5 2, 又合併 1,2堆,代價爲9,得到9 2 ,再合併得到11,總代價爲4+9+11=24;

如果第二步是先合併2,3堆,則代價爲7,得到4 7,最後一次合併代價爲11,總代價爲4+7+11=22。

問題是:找出一種合理的方法,使總的代價最小,輸出最小代價。

輸入格式

第一行一個數N表示石子的堆數N。

第二行N個數,表示每堆石子的質量(均不超過1000)。

輸出格式

輸出一個整數,表示最小代價。

數據範圍

1≤N≤3001≤N≤300

輸入樣例:

4
1 3 5 2

輸出樣例:

22

思路:假設有一堆石子,1,3,5,2,我們把1,3合併,5,2合併,總共是4 + 7 + 11 = 22.是最小代價。如下圖所示:

所有合併的個數,如果選取堆數有n - 1次選擇,然後第二次從n- 1中選就有n -2次選擇--》(n - 1)*(n - 2)*.....

狀態表示f[i][j],集合:所有將i到j合併成一堆的方案的集合。(j - i)!。屬性:min,集合中付出的最小代價。

狀態計算:化整爲零的過程,把f[i][j]分解成若干個子問題,分而治之。實際上就是從最後一步開始往前遞推。

最小方案:min(f(i, k)) +min(f(k + 1, j))+從i到j的部分和s[i] - s[i - 1];

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n;
int s[N];//前綴和
int f[N][N];
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++) cin >> s[i], s[i] += s[i - 1];//更新前綴和
    for (int len = 2; len <= n; len ++)//len從2開始,如果從1開始沒有意義
        for (int i = 1; i + len - 1 <= n; i ++)//枚舉區間左端點:i+ len - 1是左邊端點
        {
            int j = i + len - 1;//枚舉右端點
            //枚舉之前
            f[i][j] = 1e8;//先將i,J初始化成一個特別大的值
            for (int k = i; k < j; k ++)//枚舉k
                //式子直接抄過來
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
        }
    //把f[1][n]帶入定義就是所有將1-n合併的方案最大值
    cout << f[1][n] << endl;
    return 0;
}

線性DP-數字三角形

給定一個如下圖所示的數字三角形,從頂部出發,在每一結點可以選擇移動至其左下方的結點或移動至其右下方的結點,一直走到底層,要求找出一條路徑,使路徑上的數字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

輸入格式

第一行包含整數n,表示數字三角形的層數。

接下來n行,每行包含若干整數,其中第 i 行表示數字三角形第 i 層包含的整數。

輸出格式

輸出一個整數,表示最大的路徑數字和。

數據範圍

1≤n≤5001≤n≤500,
−10000≤三角形中的整數≤10000−10000≤三角形中的整數≤10000

輸入樣例:

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

輸出樣例:

30

思路:每一次只能走一個格子,有很多條路可以走。找到一條路徑上所有數字之和最大的。

首先先把他分成i 行j列,用f[i][j]表示所有起點到f[i][j]點所有路徑之和的集合。比如第四行第二個點爲7表示爲(4, 2);從最後的終點往前遞推:如圖劃圈的點7,所以從起點到f[i][j]點路徑分成兩類:一種是從左上方一種是來自右上方。

左上方:比如從7那個點到8,所以需要往上走一格即f[i - 1][j - 1] 並且得加上該點的值a[i][j];

右上方:比如畫圈那個點右上方的1,跟左上方的點8在同一行,但是不同列,所以走一格即f[i - 1, j] + a[i][j];

然後不斷往上遞歸,直到到達起點爲止。最後將兩種情況取max。如下圖所示:

注意邊界問題:如果涉及到i-1的下標循環得從i = 1開始。

動態規劃時間複雜度如何求:狀態數量 * 轉移的計算量
 

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];//表示每個點的值
int f[N][N];//存從起點到第i,j點的路徑最大長度
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= i; j ++)//這裏有問題是<=i,不是<=n
            scanf("%d", &a[i][j]);
    //初始化,這裏必須注意,從0開始到n,然後每一列得多+1,因爲三角形最右邊有邊界,求f[i][j]的時候會遍歷到每列最右邊的點,然後他的右上角的點實際上不存在的,所以初始化的時候必須把它初始化成INF
    for (int i = 0; i <= n; i ++)
        for (int j = 0; j <= i + 1; j ++)
            f[i][j] = -INF;//這裏得是負無窮
    f[1][1] = a[1][1];//第一個點就是他本身的值
    //i從2開始
    for (int i = 2; i <= n; i ++)
        for (int j = 1; j <= i; j ++)
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);//求左上角右上角那個點的最大值遍歷
    //這裏又一個問題,如果是最後一行,則必須得把最後一行的最大值求出來
    //最終答案是要遍歷最後一行,最後一行可能會走到每一個位置,求終點的最大值,然後枚舉起點到終點的最大值
    int res = -INF;//這裏得是負無窮
    for (int i = 1; i <= n; i ++) res = max(res, f[n][i]);
    printf("%d\n", res);
    return 0;
}

揹包問題

有 NN 件物品和一個容量是 VV 的揹包。每件物品只能使用一次。

第 ii 件物品的體積是 vivi,價值是 wiwi。

求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。
輸出最大價值。

輸入格式

第一行兩個整數,N,VN,V,用空格隔開,分別表示物品數量和揹包容積。

接下來有 NN 行,每行兩個整數 vi,wivi,wi,用空格隔開,分別表示第 ii 件物品的體積和價值。

輸出格式

輸出一個整數,表示最大價值。

數據範圍

0<N,V≤10000<N,V≤1000
0<vi,wi≤10000<vi,wi≤1000

輸入樣例

4 5
1 2
2 4
3 4
4 5

輸出樣例:

8
/*
f[i][j]:
1.不選第一個物品:f[i][j] = f[i - 1][j];
2.選第i個物品:f[i][j] = f[i - 1][j - v[i]]
f[i][j] = max(1, 2)
f[0][0] = 0;
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示物品個數,m表示揹包容量

int v[N], w[N];//體積,價值
//暴力做法
int f[N][N];//存所有狀態
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++)
        {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    cout << f[n][m] << endl;
    return 0;
}

//優化做法:使用滾動數組來做,
//如果f(i)只用到了f(i - 1),縮到一維來做,交替來算。f(0)和f(1)交替來算
// int f[N];//所有狀態
// int main() 
// {
//     cin >> n >> m;
//     for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
//     for (int i = 1; i <= n; i ++)
//         for (int j = m; j >= v[i]; j --)//枚舉體積
//             f[j] = max(f[j], f[j - v[i]] + w[i]);//找最大的
//     cout << f[m] << endl;
//     return 0;
// }

多揹包問題

有 NN 件物品和一個容量是 VV 的揹包。每件物品能使用多次。

狀態計算不一樣了,f(i, j)分爲選和不選兩種問題,但是這一次可以無限用。需要劃分無數個子集。先考慮樸素怎麼做,然後在找優化方法。

推導過程:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示物品個數,m表示揹包容量
int v[N], w[N];//體積,價值
//樸素做法
// int f[N][N];//存所有狀態
// int main()
// {
//     cin >> n >> m;
//     for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
//     for (int i = 1; i <= n; i ++)
//         for (int j = 0; j <= m; j ++)
//         {
//             f[i][j] = f[i - 1][j];
//             if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
//         }
//     cout << f[n][m] << endl;
//     return 0;
// }
 
//優化做法
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i ++)
        for (int j = v[i]; j <= m; j ++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
    return 0;
}

總結

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章