1. 程式人生 > 其它 >牛客華為機試HJ16

牛客華為機試HJ16

原題傳送門

1. 0-1揹包問題

詳細介紹見 演算法筆記 ch11.7.2
01揹包問題是這樣的:

有n件物品,每件物品的重量為w[i],價值為c[i]。現有一個容量為V的揹包,問如何選取物品放入揹包,使得揹包內物品的總價值最大。其中每件物品都只有1件。

樣例:

5 8 		// n = 5, V = 8
3 5 1 2 2 	// w[i]
4 5 2 1 3	// c[i] 
如果採用暴力列舉每一件物品放或者不放進揹包,顯然每件物品都有兩種選擇,因此n件物品就有![](https://cdn.nlark.com/yuque/__latex/d1db0d9c696a8c056e7117dbbb4ef6db.svg#card=math&code=2%5En&id=zucSg)種情況,而![](https://cdn.nlark.com/yuque/__latex/c7cf1b9b9c957554447d062a3d1ab89c.svg#card=math&code=O%282%5En%29&id=q1tVp)的複雜度顯然是很糟糕的。而使用動態規劃方法可以將複雜度降為![](https://cdn.nlark.com/yuque/__latex/ea2ada8735f43e526e4e6ebd0ae996eb.svg#card=math&code=O%28nV%29&id=GF8G7)。

令dp[i][v]表示前i件物品恰好裝入容量為v的揹包中所能獲得的最大價值。怎麼求解dp[i][v]呢?
考慮對第i件物品的選擇策略,有兩種策略:
① 不放第i件物品,那麼問題轉化為前i-1件物品恰好裝入容量為v的 揹包中所能獲得的最大價值,
也即dp[i-1][v]。
② 放第i件物品,那麼問題轉化為前i-1件物品恰好裝入容量為v-w[i]的揹包中所能獲得的最大價值,
也即dp[i-1][v-w[i]] + c[i]。
由於只有這兩種策略,且要求獲得最大價值,因此

上面這個就是狀態轉移方程。注意到dp[i][v] 只與之前的狀態dp[i-1][]有關,所以可以列舉i從1到n,v從0到V,通過邊界

(即前0件物品放入任何容量v的揹包中都只能獲得價值0) 就可以把整個dp陣列遞推出來。而由於dp[i][v]表示的是恰好為v的情況,所以需要列舉,取其最大值才是最後的結果。

for (int i = 1; i <= n; i++) {
    for (int v = w[i]; v <= V; v++) {
        dp[i][v] = max(dp[i - 1][v], dp[i-1][v - w[i]] + c[i]);
    }
}
可以知道,時間複雜度和空間複雜度都是O(nV),其中時間複雜度已經無法再優化,但是空間複雜度還可以再優化。

如下圖所示,注意到狀態轉移方程中計算dp[i][v]時總是隻需要dp[i-1][v]左側部分的資料(即只需要圖中正上方與左下方的資料),且當計算dp[i+1][]的部分時,dp[i-1]的資料又完全用不到了(只需要用到dp[i][]),因此不妨可以直接開一個一維陣列dpv

,列舉方向改變為i從1到n,v從V到0(逆序!),這樣狀態轉移方程該為:


這樣修改對應到圖中可以這樣理解: v的列舉順序變為從右往左,dp[i][v]右邊的部分為剛計算過的需要儲存給下一行使用的資料,而dp[i][v]左上角的陰影部分為當前需要使用的部分。將這兩者結合一下,即把dp[i][v]左下角和右邊部分的部分放在一個數組裡,每計算出一個dp[i][v],就相當於把dp[i-1][v]抹消,因為在後面的運算中dp[i-1][v]再也用不到了。我們把這種技巧稱為滾動陣列。

for (int i = 1; i <= n; i++) {
    for (int v = V; v >= w; v--) { // 逆序列舉v
        dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
    }
}

空間複雜度優化為O(V)。
特別說明: 如果是用二維陣列存放,v的列舉是順序還是逆序都無所謂;如果使用一維陣列存放,則v的列舉必須是逆序!
完整求解01揹包問題的程式碼如下

#include <iostream>
#include <algorithm>

using namespace std;

const int maxn = 100;   // 物品最大件數
const int maxv = 1000;  // V的上限
int w[maxn], c[maxn], dp[maxv];

int main() {
    freopen("./chap11/input/s0701.txt", "r", stdin);
    int n, V;
    scanf("%d%d", &n, &V);
    for (int i = 0; i < n; i++) {
        scanf("%d", &w[i]);
    }
    for (int i = 0; i < n; i++) {
        scanf("%d", &c[i]);
    }

    // 邊界
    for (int v = 0; v <= V; v++) {
        dp[v] = 0;
    }

    for (int i = 1; i <= n; i++) {
        for (int v = V; v >= w[i]; v--) {
            // 狀態轉移方程
            dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
        }
    }

    // 尋找d[0...V]中最大的即為答案
    int max = 0;
    for (int v = 0; v <= V; v++) {
        if (dp[v] > max) {
            max = dp[v];
        }
    }

    printf("%d\n", max);
    return 0;
}

輸入樣例資料:

5 8
3 5 1 2 2
4 5 2 1 3
輸出結果:
10
動態規劃是如何避免重複計算的問題在01揹包問題中非常明顯。在一開始暴力列舉每件物品放或者不放入揹包時,其實忽略了一個特性: 第i件物品放或者不放而產生的最大值是完全可以由前面i-1件物品的最大值來決定的,而暴力做法無視了這一點。

另外,01揹包問題中的每個物品都可以看作一個階段,這個階段中的狀態有 dp[i][0] ~ dp[i][V],它們均由上一個階段的狀態得到。事實上,對能夠劃分階段的問題來說,都可以嘗試把階段作為狀態的一維,這可以使我們更方便地得到滿足無後效性的狀態。從中也可以得到這麼一個技巧,如果當前設計的狀態不是滿足無後效性,那麼不妨把狀態進行升維,即增加一維或若干維來表示相應的資訊,這樣可能就能滿足無後效性了。

2. 問題描述

3. Solution

1、思路
出題者覺得01揹包問題太套路了,因此給我們使了點小絆子,但問題不大。
設主件個數為n,獎金數量為M,每個主件對應的價格為v,每個主件對應的重要程度為w。dp[i][j]表示從前i個主件中選取,獎金數量為j的情況下,所獲得的最大價格*重要程度累加和。另外注意到一個小細節:每個主件只能有0~2個附件,最多才4種搭配方式(00, 01, 10, 11),得到如下狀態公式:

  1. 如果j >= v[i-1],那麼dp[i][j] = max{dp[i][j - v[i]] + v[i-1]w[i-1], dp[i-1][j], ->->->}(->->->表示有附件的情況,為了簡化問題,後面再詳細分析。)
  2. 如果j < v[i-1],那麼dp[i][j] = dp[i-1][j]
  3. 邊界: dp[0][...] = 0dp[...][0] = 0

對於狀態轉移公式的解釋:

  1. 如果總獎金數能涵蓋當前物品,那麼行若無事包含當前物品和不包含當前物品兩種情況下最大的累加和。
    包含: dp[i][j - v[i]] + v[i-1]w[i-1]
    不包含: dp[i-1][j]
  2. 如果總獎金數不能涵蓋當前物品,那麼直接取前i-1個物品的最大累加和
  3. 邊界: 商品數量為0,總獎金為0。

上面的分析,除了->->->之外的其他部分,和01揹包問題完全一致,接下來分析->->->

  1. 如果沒有附件,則跳過
  2. 如果附件數為1,且總獎金容得下附近,那麼取最大值
  3. 如果附件數為2, ...
    2、實現
    Java
package huawei.HJ016;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.*;

public class Main {
    static Scanner in;

    public static void main(String[] args) throws IOException {
        if (!"Linux".equals(System.getProperty("os.name"))) {
            in = new Scanner(Paths.get("/Users/jun/Documents/Learn/JavaLearning/NowCoder/src/huawei/HJ016/input.txt"));
        } else {
            in = new Scanner(System.in);
        }
        int n = in.nextInt();
        n /= 10;
        int m = in.nextInt();
        Map<Integer, int[]> prices = new HashMap<>();
        Map<Integer, int[]> values = new HashMap<>();
        for (int i = 0; i < m; i++) {
            int v = in.nextInt();
            v /= 10;
            int p = in.nextInt();
            int q = in.nextInt();
            if (q == 0) {
                if (prices.get(i + 1) == null) {
                    prices.put(i + 1, new int[]{0, 0, 0});
                    values.put(i + 1, new int[]{0, 0, 0});
                }
                prices.get(i + 1)[0] = v;
                values.get(i + 1)[0] = v * p;
            } else {
                if (prices.get(q) == null) {
                    prices.put(q, new int[]{0, 0, 0});
                    values.put(q, new int[]{0, 0, 0});
                }
                if (prices.get(q)[1] != 0) {
                    prices.get(q)[2] = v;
                    values.get(q)[2] = v * p;
                } else {
                    prices.get(q)[1] = v;
                    values.get(q)[1] = v * p;
                }
            }
        }
        int[] dp = new int[n + 1];
        Set<Map.Entry<Integer, int[]>> entries = prices.entrySet();
        for (Map.Entry<Integer, int[]> entry : entries) {
            int i = entry.getKey();
            int[] v = entry.getValue();
            for (int j = n; j >= v[0]; j--) {
                int p1 = v[0];
                int p2 = v[1];
                int p3 = v[2];
                int v1 = values.get(i)[0];
                int v2 = values.get(i)[1];
                int v3 = values.get(i)[2];

                dp[j] = Math.max(dp[j], dp[j - p1] + v1);
                dp[j] = j >= p1 + p2 ? Math.max(dp[j], dp[j - p1 - p2] + v1 + v2) : dp[j];
                dp[j] = j >= p1 + p3 ? Math.max(dp[j], dp[j - p1 - p3] + v1 + v3) : dp[j];
                dp[j] = j >= p1 + p2 + p3 ? Math.max(dp[j], dp[j - p1 - p2 - p3] + v1 + v2 + v3) : dp[j];
            }
        }
        System.out.println(dp[n] * 10);
    }
}

Python

import sys
from collections import defaultdict

if sys.platform != "linux":
    file_in = open("input/HJ16.txt")
    sys.stdin = file_in

"""
50 5    // 總錢數50, 物品個數 5

i   v  p q
1 | 20 3 5  
2 | 20 3 5
3 | 10 3 0
4 | 10 2 0
5 | 10 1 0

其中 v 表示該物品的價格( v<10000 ), 
p 表示該物品的重要度( 1 ~ 5 ), q 表示該物品是主件還是附件。
如果 q=0 ,表示該物品為主件,如果 q>0 ,表示該物品為附件, q 是所屬主件的編號

由第1行可知總錢數N為50以及希望購買的物品個數m為5;
第2和第3行的q為5,說明它們都是編號為5的物品的附件;
第4~6行的q都為0,說明它們都是主件,它們的編號依次為3~5;
所以物品的價格與重要度乘積的總和的最大值為10*1+20*3+20*3=130 

dp[v] = max(dp[v], dp[v - w[i]] + c[i]) 
"""

# 資料輸入
n, m = map(int, input().split())
n //= 10  # 價格總為10 的倍數,優化空間複雜度
#                             主 從1 從2
prices = defaultdict(lambda: [0, 0, 0])  # 主從物品的價格 v
values = defaultdict(lambda: [0, 0, 0])  # 主從物品的價值 v * p

for i in range(m):  # i 代表第i+1個物品
    v, p, q = map(int, input().split())
    v //= 10  # 價格為10的倍數
    if q == 0:  # 追加主物品
        prices[i + 1][0] = v
        values[i + 1][0] = v * p
    elif prices[q][1]:  # 追加從物品
        prices[q][2] = v
        values[q][2] = v * p
    else:
        prices[q][1] = v
        values[q][1] = v * p

# 處理輸出
dp = [0] * (n + 1)

for i, v in prices.items():
    for j in range(n, v[0] - 1, -1):
        p1, p2, p3 = v
        v1, v2, v3 = values[i]
        # 處理主從組合的4種情況
        dp[j] = max(dp[j], dp[j - p1] + v1)  # 新增主物品,無從物品
        dp[j] = max(dp[j], dp[j - p1 - p2] + v1 + v2) if j >= p1 + p2 else dp[j]  # 主 + 從1
        dp[j] = max(dp[j], dp[j - p1 - p3] + v1 + v3) if j >= p1 + p3 else dp[j]  # 主 + 從2
        dp[j] = max(dp[j], dp[j - p1 - p2 - p3] + v1 + v2 + v3) if j >= p1 + p2 + p3 else dp[j]  # 主 + 從1 + 從2

print(dp[n] * 10)