王者編程大賽之三 — 01揹包

首發於 樊浩柏科學院

服務目前每個月會對搬家師傅進行評級,根據師傅的評級排名結果,咱們將優先保證最優師傅的全天訂單。php

假設師傅天天工做 8 個小時,給定一天 n 個訂單,每一個訂單其佔用時間長爲 $T_i$,掙取價值爲 $V_i$,現請您爲師傅安排訂單,並保證師傅掙取價值最大。html

輸入格式
輸入 n 組數據,每組以逗號分隔,而且每個訂單的編號、時長、掙取價值以空格分隔
輸出格式
輸出爭取價值和訂單編號,訂單編號按照價值由大到小排序,爭取價值相同,則按照每小時平均爭取價值由大到小排序git

示例:
輸入:[MV10001 2 100,MV10008 2 30,MV10003 1 200,MV10009 6 500,MV10010 3 400]
輸出:730 MV10010 MV10003 MV10001 MV10008
輸入:[M10001 2 100,M10002 3 210,M10003 3 300,M10004 2 150,M10005 1 70,M10006 2 220,M10007 1 10,M10008 3 30,M10009 3 200,M10010 2 400]
輸出:990 M10010 M10003 M10006 M10005github

解題思路

因爲本題每一個訂單天天只被安排一次,是典型地採用 動態規劃 求解的 01 揹包問題。算法

動態規劃概念

動態規劃過程:每次決策依賴於當前狀態,又隨即引發狀態的轉移。一個決策序列就是在變化的狀態中產生出來的,因此,這種多階段最優化決策解決問題的過程就稱爲動態規劃。編程

動態規劃原理:動態規劃與分治法相似,都是把原問題拆分紅不一樣規模相同特徵的小問題,經過尋找特定的遞推關係,先解決一個個小問題,最終達到解決原問題的效果。優化

創建動態方程

假設,師傅掙取價值最大時的訂單爲 $x_1$,$x_2$,$x_3$,...,$x_i$(其中 $x_i$ 取 1 或 0,表示第 i 個訂單被安排或者不安排),$v_i$ 表示第 i 個訂單的價值,$w_i$ 表示第 i 個訂單的耗時時長,$wv(i,j)$ 表示安排了第 i 個訂單,師傅總耗時爲 j 時的最大價值。this

可得訂單價值和耗時的關係圖:編碼

i 1 2 3 4 5
w(i) 2 2 1 6 3
v(i) 100 30 200 500 400

所以,可得 動態方程spa

$$wv(i,j) = begin{cases}
wv(i-1,j)(j < w(i)) \
max(wx(i-1,j),wv(i-1,j-w(i))+v(i))(j geq w(i))
end{cases}$$

說明:$j<w(i)$ 表示訂單不被安排,$j \geq w(i)$ 表示訂單被安排。

肯定邊界

能夠肯定邊界條件 $wx(0,j) = wx(i, 0) = 0$,$wx(0,j)$ 表示一個訂單都沒安排,再怎麼耗時價值都爲 0,$wx(i,0)$ 表示沒有耗時,安排多少訂單價值都爲 0。

求解

求解過程,能夠填表來進行模擬:

1) 如 i=1,j=1 時,有 $j<w(i)$,故 $wx(1,1) = wx(1-1,1) = 0$;
2) 又如 i=1,j=2 時,有 $j=w(i)$,故 $wx(1,2) = max(wx(1-1,1), wx(1-1,2-w(1)) + v(1) = 100$;
3) 如此下去,直至填到最後一個,i=5,j=8 時,有 $j<w(i)$,故 $wx(5,8) = max(wx(5-1,8), wx(5-1,8-w(5)) + v(5) = 730$;
4) 在耗時沒有超過 8 小時的前提下,當前 5 個訂單都被安排過期,$wx(5,8) = 730$ 即爲所求的最大價值;

解的組成

儘管 求解 過程已經求出了最大價值,可是並無得出哪些訂單被安排了,也就是沒有得出解的組成部分。

可是在求解的過程當中不難發現,尋解方程知足以下定義:

$$x(i) = begin{cases}
wv(i,j) = wv(i-1,j) \
wv(i,j) neq wv(i-1,j)
end{cases}$$

從表格右下到左上爲尋解方向,尋解過程以下:

1) i=5,j=8 時,有 $wv(5,8) != wv(4,8)$,故 $x(5) = 1$,此時 $j -= w(5)$,$j = 5$;
2) i=4 時,不管 j 取何值,都有 $wv(4,j) == wv(3,j)$,故 $x(5) = 0$,此時 $j = 5$;
3) i=3,j=5 時,有 $wv(3,5) != wv(2,5)$,故 $x(3) = 1$,此時 $j -= w(3)$,$j = 4$;
4) i=2,j=4時,有 $wv(2,4) != wv(1,4)$,故 $x(2) = 1$,此時 $j -= w(2)$,$j = 2$;
5) i=1,j=2時,有 $wv(1,2) != wv(1,2)$,故 $x(1) = 1$,此時 $j -= w(1)$,$j = 0$,尋解結束;

編碼實現

實現的類結構以下,特殊的方法已提取出,並將一一詳細說明。

class Knapsack
{
    //物品重量,index從1開始表示第1個物品
    public $w = array();
    //物品價值,index從1開始表示第1個物品
    public $v = array();
    //最大價值,$wv[$i][$w]表示前i個物品重量爲w時的最大價值
    public $wv = array();
    //物品總數
    public $n = 0;
    //物品總重量
    public $W = 0;
    //揹包中的物品
    public $goods = array();

    /**
     * Knapsack constructor.
     * @param array $goods 物品信息,格式以下:
     * [
     *   [index, w, v]   //good1
     *   ...
     * ]
     * @param $c
     */
    public function __construct(array $goods, $c)
    {
        $this->goods = $goods;

        $this->W = $c;
        $this->n = count($goods);
        //初始化物品價值
        $v = array_column($goods, 2);
        array_unshift($v, 0);
        $this->v = $v;
        //初始化物品重量
        $w = array_column($goods, 1);
        array_unshift($w, 0);
        $this->w = $w;
        //初始化最大價值
        $this->wv = array_fill(0, $this->n + 1, array_fill(0, $this->W + 1, 0));

        $this->pd();
        $this->canPut();
    }

    public function getMaxPrice()
    {
        return $this->wv[$this->n][$this->W];
    }
}

動態求解過程:

public function pd()
{
    for ($i = 0; $i <= $this->W; $i++) {
        for ($j = 0; $j <= $this->n; $j++) {
            //未放入物品和重量爲空時,價值爲0
            if ($i == 0 || $j == 0) {
                continue;
            }

            //決策
            if ($i < $this->w[$j]) {
                $this->wv[$j][$i] = $this->wv[$j - 1][$i];
            } else {
                $this->wv[$j][$i] = max($this->wv[$j - 1][$i], $this->wv[$j - 1][$i - $this->w[$j]] + $this->v[$j]);
            }
        }
    }
}

尋解過程:

public function canPut()
{
    $c = $this->W;
    for ($i = $this->n; $i > 0; $i--) {

        //揹包質量爲c時,前i-1個和前i-1個物品價值不變,表示第1個物品未放入
        if ($this->wv[$i][$c] == $this->wv[$i - 1][$c]) {
            $this->goods[$i - 1][3] = 0;
        } else {
            $this->goods[$i - 1][3] = 1;
            $c = $c - $this->w[$i];
        }
    }
}

按照訂單價值降序獲取訂單信息(若訂單價值相同則按單位時間平均價值降序排列):

public function getGoods()
{
    $filter = function($value) {
        return $value[3];
    };
    $goods = array_filter($this->goods, $filter);
    usort($goods, function($a, $b) {
        if ($a[2] == $b[2]) {
            if ($a[2] / $a[1] < $b[2] / $b[1]) {
                return 1;
            }
            return 0;
        }
        return $a[2] < $b[2];
    });

    return $goods;
}

接收標準輸入處理並輸出結果:

$arr = explode(',', $input);
$filter = function ($value) {
    return explode(' ', $value);
};

$knapsack = new Knapsack(array_map($filter, $arr), 8);
$goods = $knapsack->getGoods();

echo $knapsack->getMaxPrice(), ' ', implode(' ', array_column($goods, 0)), PHP_EOL;

總結

該題使用動態規劃求解,算法的時間複雜度爲 $O(nc)$,固然也能夠採用其餘方式求解。例如先將訂單按照價值排序,而後依次嘗試進行安排訂單,直至剩餘耗時不能再被安排訂單。

有關動態規劃的其餘典型應用,請參考 常見的動態規劃問題分析與求解 一文。

相關文章 »

相關文章
相關標籤/搜索