《算法導論》學習筆記——揹包問題

一、揹包問題(knapsack problem)

(參考維基百科: http://en.wikipedia.org/wiki/Knapsack_problem

1. 0-1 揹包問題(0-1 knapsack problem the most common problem):


2. 有界揹包問題(bounded knapsack problem BKP):


3. 無界揹包問題(unbounded knapsack problem UKP):

二、0-1揹包問題

特點:每件物品或被帶走,或被留下(需要做出0-1選擇)。不能只帶走某個物品的一部分或帶走兩次以上同一個物品。

輸入:數組v,數組w,可取走的物品數n,可取走的最大重量W。

輸出(可選):數組x,x中每個元素爲1或0,1對應該物品被取走,0對應不取;滿足條件下揹包的總重量及總價值。

分析:一個物品選或不選,共有2^n中組合方式,目的就是找到這些組合方式中滿足條件的一種。

  解法一 暴力破解法

分析:枚舉2^n種組合方式下的總重量和總價格,找到滿足條件的最優解。

時間複雜度:O(lg1 + lg2 + lg3 + ... + lg(2^n)) = O(lg((2^n)!)) > O(2^n)。

算法C實現:

枚舉時採用位運算,在每一位上通過判斷0或1指示該位表示的物品是否取走。

void knapsack_problem_violent(ElementType* v, ElementType* w,
        CommonType count,
        ElementType maximum_weight,
        ElementType* best_weight,
        ElementType* best_value,
        ElementType* x)
{
    assert(v != NULL && w != NULL && count >= 1 && maximum_weight > 0 &&
            x != NULL && best_value != NULL && best_weight != NULL);
    /// initialize
    CommonType i, j, k;
    ElementType temp_weight, temp_value;
    CommonType x_temp[MAX_COUNT] = {0};
    *best_weight = 0; *best_value = 0;
    memset(x, 0, count * sizeof(ElementType));
    /// detect all combination
    CommonType type_count = pow(2, count);
    for (i = 0; i < type_count; i++) {
        /// get current total weight and totoal value and combination
        memset(x_temp, 0, count * sizeof(ElementType));
        temp_value = 0;
        temp_weight = 0;
        k = 0;
        j = i;
        do {
            /// 1 bit correspond to 1 item
            if (j & 0x1) {
                temp_value += v[k];
                temp_weight += w[k];
                x_temp[k] = 1;
            }
            k++;
        } while (j >>= 1);
        /// save best state: best weight, best value and item state
        if (temp_weight < maximum_weight && temp_value > *best_value) {
            *best_weight = temp_weight;
            *best_value = temp_value;
            memcpy(x, x_temp, count * sizeof(ElementType));
        }
    }
}

 解法二 遞歸算法 分治策略

遞歸的子問題定義詳見解法三,這裏只給出算法C實現:

ElementType knapsack_problem(ElementType* v, ElementType* w,
        CommonType count,
        ElementType maximum_weight)
{
    assert(v != NULL && w != NULL && count >= -1 && maximum_weight >= 0);
    /// no items or no weight
    if (count == -1 || maximum_weight == 0)
        return 0;
    /// the i-th item's weight exceed the range
    if (w[count - 1] > maximum_weight)
        return knapsack_problem(v, w, count - 1, maximum_weight);
    /// get best solution
    ElementType a = knapsack_problem(v, w, count - 1, maximum_weight);
    ElementType b = knapsack_problem(v, w, count - 1,
            maximum_weight - w[count - 1]) + v[count - 1];
    return a > b ? a : b;
}

 解法三 動態規劃 dynamic programming

動態規劃分析:

  最優子結構 Optimal Structure:即滿足一個問題的最優解包含子問題最優解的情況。選擇一個給定物品i,則需要比較選擇i的形成的子問題的最優解與不選擇i的形成的子問題的最優解。問題分成兩個子問題,進行選擇比較,選擇最優的一種。

  重疊子問題 Overlapping subproblems:子問題之間並非獨立,存在重疊情況。

難點:把問題抽象化並建立數學模型,用科學的數學語言描述問題與子問題的關係,找到狀態轉移點並推出遞歸關係,以便編程求解。

問題:在N個物品中選擇,在揹包最大重量W約束下揹包所能達到的最大價值。

該問題子問題可分爲兩個:

子問題1:不選擇第N個物品,在前N-1個物品中選擇,在揹包最大重量W約束下揹包所能達到的最大價值可能爲問題最優解。

子問題2:選擇第N個物品(重量爲w,價值爲v),並在前N-1個物品中選擇,在揹包最大重量W-w約束下揹包所能達到的最大價值加上第N個物品的價值v可能爲問題最優解。

這樣問題最優解就可以轉化求解相應兩種子問題的最優解。數學表達如下:

定義v(i,w)爲在前i個物品中選擇取捨(並不是說前i個物品都要取),並且揹包最大重量爲w時揹包所能達到的最大價值(最優解)。根據題意可得0<=i<=n,0<=w<=W。

可以得到遞歸關係:

v(i,w) = max(v(i-1,w),v(i-1,w-wi) + vi) (其中wi,vi爲第i個物品重量和價值,v(i-1,w)對應不選擇第i個物品時最優解,v(i-1,w-wi)+vi對應選擇第i個物品時最優解)

v(i,w) = 0, 此時i = 0 或 w = 0。

v(i,w) = v(i-1,w), 此時wi > w,避免揹包最大重量爲負數。

時間複雜度:O(n*W)。

算法C實現:

動態規劃採用自底向上法(bottom-up method)

keep數組用於尋找哪些物品被放入揹包哪些物品被舍,keep(i,w) = 1表示v(i,w)這一最優解情況下保留第i個物品,keep(i,w) = 0時表示不保留。利用keep數組,可進行回溯(trace back),從而判斷v(n,W)情況下哪些物品放入揹包,哪些物品被捨去;

V數組和keep數組第一列和第一行分別表示揹包最大重量w = 0和可選擇物品數爲i = 0時問題的解,爲特殊解,需要初始化爲0(邊界問題)。

/**
 * @file knapsack_problem_dp_bottom_up.c
 * @brief solve 0-1 knapsack problem by dynamic programming
 * with bottom-up method.
 * @author chenxilinsidney
 * @version 1.0
 * @date 2015-03-04
 */

#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <math.h>
// #define NDEBUG
#include <assert.h>

// #define NDBG_PRINT
#include "debug_print.h"

typedef int ElementType;    ///< element data type
typedef int CommonType;     ///< common data type

/// data
#define MAX_COUNT      100  ///< count of the items
#define MAX_WEIGHT     300  ///< max weight of the items for memory
ElementType v[MAX_COUNT + 1] = {0};
ElementType w[MAX_COUNT + 1] = {0};
CommonType x[MAX_COUNT + 1] = {0};
CommonType V[MAX_COUNT + 1][MAX_WEIGHT + 1] = {{0}};
CommonType keep[MAX_COUNT + 1][MAX_WEIGHT + 1] = {{0}};

ElementType knapsack_problem(ElementType* v, ElementType* w,
        ElementType* x,
        CommonType count,
        ElementType maximum_weight,
        ElementType V[MAX_COUNT + 1][MAX_WEIGHT + 1],
        ElementType keep[MAX_COUNT + 1][MAX_WEIGHT + 1])
{
    assert(v != NULL && w != NULL && count >= -1 && maximum_weight >= 0 &&
            V != NULL);
    CommonType i, j;
    /// initialize first row
    for (i = 0; i <= maximum_weight; i++) {
        V[0][i] = 0;
        keep[0][i] = 0;
    }
    for (i = 1; i <= count; i++) {
        /// initialize first column
        V[i][0] = 0;
        keep[i][0] = 0;
        /// get matrix solution
        for (j = 1; j <= maximum_weight; j++) {
            if(w[i - 1] <= j) {
                ElementType a = V[i - 1][j];
                ElementType b = V[i - 1][j - w[i - 1]] + v[i - 1];
                V[i][j] = a > b ? a : b;
                if (a > b)
                    keep[i][j] = 0;
                else
                    keep[i][j] = 1;
            } else {
                V[i][j] = V[i - 1][j];
                keep[i][j] = 0;
            }
        }
    }
    /// display the matrix
    printf("-V--");
    for (j = 0; j <= maximum_weight; j++)
        printf("%3d ", j);
    printf("\n");
    for (i = 0; i <= count; i++) {
        printf("%3d ", i);
        for (j = 0; j <= maximum_weight; j++)
            printf("%3d ", V[i][j]);
        printf("\n");
    }
    printf("keep");
    for (j = 0; j <= maximum_weight; j++)
        printf("%3d ", j);
    printf("\n");
    for (i = 0; i <= count; i++) {
        printf("%3d ", i);
        for (j = 0; j <= maximum_weight; j++)
            printf("%3d ", keep[i][j]);
        printf("\n");
    }
    /// get and display the item list by keep matrix
    printf("k = ");
    ElementType K = maximum_weight;
    for (i = count; i >= 1; i--) {
        if (keep[i][K] == 1) {
            printf("%3d ", i);
            K -= w[i - 1];
            x[i - 1] = 1;
        } else {
            x[i - 1] = 0;
        }
    }
    printf("\n");
    printf("actual weight = %3d\n", maximum_weight - K);
    return V[count][maximum_weight];
}

int main(void) {
    /// read data to array
    /// read maximum weight
    ElementType maximum_weight = 0;
    if (scanf("%d\n", &maximum_weight) != 1 || maximum_weight <= 0) {
        DEBUG_PRINT_STATE;
        DEBUG_PRINT_STRING("can not get right maximum_weight");
    }
    CommonType count = 0;
    while(count < MAX_COUNT && scanf("(%u,%u)\n", v + count, w + count) == 2) {
        ++count;
    }
    /// get result
    ElementType best_value = knapsack_problem(v, w, x, count,
            maximum_weight, V, keep);
    /// output result
    printf("best value = %d\n", best_value);
    CommonType i;
    for (i = 0; i < count; i++) {
        printf("item index = %3d, value = %3d, weight = %3d, get_flag = %3d\n",
                i + 1, v[i], w[i], x[i]);
    }
    return EXIT_SUCCESS;
}
輸入數據:

10
(10,5)
(40,4)
(30,6)
(50,3)

輸出數據:

-V--  0   1   2   3   4   5   6   7   8   9  10 
  0   0   0   0   0   0   0   0   0   0   0   0 
  1   0   0   0   0   0  10  10  10  10  10  10 
  2   0   0   0   0  40  40  40  40  40  50  50 
  3   0   0   0   0  40  40  40  40  40  50  70 
  4   0   0   0  50  50  50  50  90  90  90  90 
keep  0   1   2   3   4   5   6   7   8   9  10 
  0   0   0   0   0   0   0   0   0   0   0   0 
  1   0   0   0   0   0   1   1   1   1   1   1 
  2   0   0   0   0   1   1   1   1   1   1   1 
  3   0   0   0   0   0   0   0   0   0   0   1 
  4   0   0   0   1   1   1   1   1   1   1   1 
k =   4   2 
actual weight =   7
best value = 90
item index =   1, value =  10, weight =   5, get_flag =   0
item index =   2, value =  40, weight =   4, get_flag =   1
item index =   3, value =  30, weight =   6, get_flag =   0
item index =   4, value =  50, weight =   3, get_flag =   1

優化:

1.內存空間優化(減少空間複雜度),把v從二維數組v(i,w)降爲一維數組v(w),理由:子問題只需要一維方向上的信息,考慮到更新一維數組是否對後續問題求解帶來影響,採用遞減方式進行以防止舊有數據被更新而無法使用;

2.可選優化(未實現):可以在遍歷過程中隨着i增加而讀入相應物品的數據vi和wi,避免開闢兩個長度數組存儲這些物品數據。

優化部分代碼:

ElementType knapsack_problem(ElementType* v, ElementType* w,
        ElementType* x,
        CommonType count,
        ElementType maximum_weight,
        ElementType V[MAX_COUNT + 1],
        ElementType keep[MAX_COUNT + 1][MAX_WEIGHT + 1])
{
    assert(v != NULL && w != NULL && count >= -1 && maximum_weight >= 0 &&
            V != NULL);
    CommonType i, j;
    /// initialize first row
    for (i = 0; i <= maximum_weight; i++) {
        keep[0][i] = 0;
    }
    for (i = 1; i <= count; i++) {
        /// initialize first column
        V[0] = 0;
        keep[i][0] = 0;
        /// get matrix solution
        for (j = maximum_weight; j >= 1; j--) {
            if(w[i - 1] <= j) {
                ElementType b = V[j - w[i - 1]] + v[i - 1];
                if (V[j] < b) V[j] = b;
                if (V[j] > b)
                    keep[i][j] = 0;
                else
                    keep[i][j] = 1;
            } else {
                keep[i][j] = 0;
            }
        }
    }
    /// display the matrix
    printf("-V--");
    for (j = 0; j <= maximum_weight; j++)
        printf("%3d ", j);
    printf("\n");
    for (i = 0; i <= count; i++) {
        printf("%3d ", i);
        for (j = 0; j <= maximum_weight; j++)
            printf("%3d ", V[j]);
        printf("\n");
    }
    printf("keep");
    for (j = 0; j <= maximum_weight; j++)
        printf("%3d ", j);
    printf("\n");
    for (i = 0; i <= count; i++) {
        printf("%3d ", i);
        for (j = 0; j <= maximum_weight; j++)
            printf("%3d ", keep[i][j]);
        printf("\n");
    }
    /// get and display the item list by keep matrix
    printf("remain weight = ");
    ElementType remain_weight = maximum_weight;
    for (i = count; i >= 1; i--) {
        if (keep[i][remain_weight] == 1) {
            printf("%3d ", i);
            remain_weight -= w[i - 1];
            x[i - 1] = 1;
        } else {
            x[i - 1] = 0;
        }
    }
    printf("\n");
    printf("actual weight = %3d\n", maximum_weight - remain_weight);
    return V[maximum_weight];
}

其他:採用動態規劃解決0-1揹包問題,貪心算法對0-1揹包問題無效。

三、分數揹包問題(factional knapsack problem):

與0-1揹包問題區別:可以那種物體的一部分,而不是隻能做出二元(0-1)選擇。

使用貪心算法(greedy algorithm),我們計算每個物品單位重量價值,遵循貪心策略,首先儘量多地拿走物品單位重量價值最高的物品,以此類推,直到達到上限W。

時間複雜度:由於需要進行對物品的單位重量價值進行排序,時間複雜度爲O(nlgn)。

貪心算法(greedy algorithm):

最優子結構:對一個問題來說,如果他的最優解包含子問題的最優解,那該問題就就具有最優子結構性質。

貪心選擇性質:一個全局最優解可以通過局部最優(貪心)選擇來得到。

注意:貪心算法和動態規劃對比。(詳見《算法導論》)


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