【算法總結】動態規划-背包問題


動態規划-背包問題

此博客分別討論0-1背包,完全背包和多重背包,並給出相應的解題模板。

0-1背包

題目:有一個容量為 V 的背包,和一些物品。這些物品分別有兩個屬性,體積 w 和價值 v,每種物品只有一個。要求用這個背包裝下價值盡可能多的物品,求該最大價值,背包可以不被裝滿。 

0-1背包問題:在最優解中,每個物品只有兩種可能的情況,即在背包中或者不在背包中(背包中的該物品數為0或1),因此稱為0-1背包問題。

步驟1-找子問題:子問題必然是和物品有關的,對於每一個物品,有兩種結果:能裝下或者不能裝下。第一,包的容量比物品體積小,裝不下,這時的最大價值和前i-1個物品的最大價值是一樣的。第二,還有足夠的容量裝下該物品,但是裝了不一定大於當前相同體積的最優價值,所以要進行比較。由上述分析,子問題中物品數和背包容量都應當作為變量。因此子問題確定為背包容量為j時,求前i個物品所能達到最大價值

步驟2-確定狀態:由上述分析,“狀態”對應的“值”即為背包容量為j時,求前i個物品所能達到最大價值,設為dp[i][j]。初始時,dp[0][j](0<=j<=V)為0,沒有物品也就沒有價值。

步驟3-確定狀態轉移方程:由上述分析,第i個物品的體積為w,價值為v,則狀態轉移方程為

  • j<w,dp[i][j] = dp[i-1][j] //背包裝不下該物品,最大價值不變
  • j>=w, dp[i][j] = max{ dp[i-1][j-list[i].w] + v, dp[i-1][j] } //和不放入該物品時同樣達到該體積的最大價值比較

示例代碼(具體輸入輸出見王道機試指南第七章):

#include<cstdio>

int max(int a, int b)//取最大值函數
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[101][1001];

int main()
{
    int s, n;//背包容量和物品總數
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//讀入每個物品的體積和價值
        }
        for (int i = 0; i <= s; i++) dp[0][i] = 0;//初始化二維數組
        for (int i = 1; i <= n; i++)//循環每個物品,執行狀態轉移方程
        {
            for (int j = s; j >= list[i].w; j--)
            {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);
            }
            for (int j = list[i].w - 1; j >= 0; j --)
            {
                dp[i][j] = dp[i - 1][j];
            }
        }
        printf("%d\n", dp[n][s]);
    }
    return 0;
}

直接按照狀態轉移方程正序遍歷j也可以,上方代碼只是為了和優化算法統一。

#include<cstdio>

int max(int a, int b)//取最大值函數
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[101][1001];

int main()
{
    int s, n;//背包容量和物品總數
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//讀入每個物品的體積和價值
        }
        for (int i = 0; i <= s; i++) dp[0][i] = 0;//初始化二維數組
        for (int i = 1; i <= n; i++)//循環每個物品,執行狀態轉移方程
        {
            for (int j = 0; j <= s; j++)
            {
                if (j >= list[i].w)dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);
                else dp[i][j] = dp[i - 1][j];
            }
        }
        printf("%d\n", dp[n][s]);
    }
    return 0;
}
正序遍歷j示例代碼

變式:要求恰好裝滿背包時,把dp[0][0]設為0,其余dp[0][i]設為負無窮即可,這樣只有恰好達到dp[n][s]時,dp[n][s]才為正值(用優化算法也可以)。

#include<cstdio>

const int INF = -999999;

int max(int a, int b)//取最大值函數
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[101][1001];

int main()
{
    int s, n;//背包容量和物品總數
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//讀入每個物品的體積和價值
        }
        dp[0][0] = 0;
        for (int i = 1; i <= s; i++) dp[0][i] = INF;//初始化二維數組
        for (int i = 1; i <= n; i++)//循環每個物品,執行狀態轉移方程
        {
            for (int j = s; j >= list[i].w; j--)
            {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);
            }
            for (int j = list[i].w - 1; j >= 0; j--)
            {
                dp[i][j] = dp[i - 1][j];
            }
        }
        printf("%d\n", dp[n][s]);
    }
    return 0;
}
變式示例代碼

優化算法:

觀察狀態轉移方程的特點,我們發現dp[i][j]的轉移只與dp[i-1][j-list[i].w]和dp[i-1][j]有關,即僅與二維數組本行的上一行有關。因此,我們可以將二維數組優化為一維數組。不過這里要注意兩點:1.j<w的狀態轉移方程不再需要了。2.為保證每個物品只能使用一次,我們倒序遍歷所有j的值,這樣在更新dp[j]的時候,dp[j-list[i].w]的值尚未被修改,就不會出現一個物品重復使用的問題。

優化后的狀態轉移方程:dp[j] = max{ dp[j-list[i].w] + v, dp[j] }

復雜度分析:其狀態數量為n*s, n為物品數量,s為背包總體積,狀態轉移復雜度為O(1),所以綜合時間復雜度為O(n*s),優化后的空間復雜度僅為O(s)。

示例代碼:

#include<cstdio>

int max(int a, int b)//取最大值函數
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[1001];

int main()
{
    int s, n;//背包容量和物品總數
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//讀入每個物品的體積和價值
        }
        for (int i = 0; i <= s; i++) dp[i] = 0;//初始化二維數組
        for (int i = 1; i <= n; i++)//循環每個物品,逆序遍歷j執行狀態轉移方程
        {
            for (int j = s; j >= list[i].w; j--)
            {
                dp[j] = max(dp[j], dp[j - list[i].w] + list[i].v);
            }
        }
        printf("%d\n", dp[s]);
    }
    return 0;
}

完全背包

我們擴展0-1背包問題,使每種物品的數量無限增加,便得到完全背包問題:有一個容積為 V 的背包,同時有 n 個物品,每個物品均有各自的體積 w 和價值 v,每個物品的數量均為無限個,求使用該背包最多能裝的物品價值總和。

正好利用了上述0-1背包的優化算法,這時我們正序遍歷j,正好可以實現每種物品的重復利用,即相當於每種物品有無限個。

#include<cstdio>

int max(int a, int b)//取最大值函數
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[1001];

int main()
{
    int s, n;//背包容量和物品總數
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//讀入每個物品的體積和價值
        }
        for (int i = 0; i <= s; i++) dp[i] = 0;//初始化二維數組
        for (int i = 1; i <= n; i++)//循環每個物品,正序遍歷j執行狀態轉移方程
        {
            for (int j = list[i].w; j <= s; j++)
            {
                dp[j] = max(dp[j], dp[j - list[i].w] + list[i].v);
            }
        }
        printf("%d\n", dp[s]);
    }
    return 0;
}
完全背包示例代碼

完全背包例題:POJ1384 Piggy-Bank

#include<cstdio>

int const INF = 0x7fffffff;//因為此時是求最小值,所以INF應為無窮大
int min(int a, int b){return a < b ? a : b;}//取最小值函數

struct Thing
{
    int w;
    int v;
}list[501];

int dp[10001];

int main()
{
    int T;//測試組數
    scanf("%d", &T);
    int s, s1, s2, n;//硬幣總重、空豬重量、滿豬重量和硬幣總數
    while (T--)
    {
        scanf("%d%d", &s1, &s2);//讀入空豬重量和滿豬重量
        s = s2 - s1;//得到硬幣總重
        scanf("%d", &n);//讀入硬幣總數
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].v, &list[i].w);//讀入每個物品的價值和重量
        }
        dp[0] = 0;
        for (int i = 1; i <= s; i++) dp[i] = INF;//初始化
        for (int i = 1; i <= n; i++)//循環每個物品,正序遍歷j執行狀態轉移方程
        {
            for (int j = list[i].w; j <= s; j++)
            {
                if (dp[j - list[i].w] != INF) dp[j] = min(dp[j], dp[j - list[i].w] + list[i].v);//如果dp[j - list[i].w]不為無窮,就可以由此狀態轉移而來
            }
        }
        if (dp[s] != INF) printf("The minimum amount of money in the piggy-bank is %d.\n", dp[s]);
        else puts("This is impossible.");
    }
    return 0;
}
Piggy-bank AC代碼

多重背包

多重背包問題介於 0-1 背包和完全背包之間:有容積為V的背包,給定一些物品,每種物品包含體積 w、價值 v、和數量 k,求用該背包能裝下的最大價值總量。

與之前討論的問題一樣,我們可以將多重背包問題直接轉化到 0-1 背包上去,即每種物品均被視為k種不同物品,對所有的物品求0-1背包。其時間復雜度為O(s*Σki)。

由此可見,降低每種物品的數量 ki 將會大大的降低其復雜度,於是我們采用一種更為有技巧性的拆分。將原數量為 k 的物品拆分為若干組,每組物品看成一件物品,其價值和重量為該組中所有物品的價值重量總和,每組物品包含的原物品個數分別為:為:1、2、4…k-2^c+1,其中 c 為使 k-2^c+1 大於 0 的最大整數。這種類似於二進制的拆分,不僅將物品數量大大降低,同時通過對這些若干個原物品組合得到新物品的不同組合,可以得到 0 到 k 之間的任意件物品的價值重量和,所以對所有這些新物品做 0-1 背包,即可得到多重背包的解。由於轉化后的 0-1 背包物品數量大大降低,其時間復雜度也得到較大優化,為O(s*Σlog2(ki))。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM