輕量、高可用的任務調度系統實之揹包問題

緒言
最近爲了能夠在機器學習平臺支持公司內的各類應用任務,如Spark類,Python類,R類等的程序,共享一套集羣資源,並最大化資源利用率,一個輕量、高可用的任務調度系統必不可少,因此本人基於Netty/Raft協議,實現了一個初具功能具的系統,請參考本人的github項目,代碼中可以看到其它優秀開源項目的身影,如有抄襲嫌疑,還請多多指正。

一個功能完善的調度系統,可以參考Alibaba自研的、具有ML(機器學習)性質的調度系統Sigma,需要考慮到很多的場景,這其中最重要的莫過於如何在一個共享的資源池中,最大
限度地並行執行最多的任務。所有的任務都會附帶一些公共的屬性,如CPU核數、內存佔用量、優先級等,那如何把這些有屬性的任務最大化地分配到多臺機器上跑,這個問題就是一個很典型的多揹包問題。

01揹包問題

首先從基本的0/1揹包問題說起,其問題可以表述爲:
現有一個容量大小爲V的揹包和N件物品,每件物品有兩個屬性,體積和價值,且都只能被選一次,請問這個揹包最多能裝價值爲多少的物品?

算法表述:

算法輸入:
第一行兩個整數V和n。接下來n行,每行兩個整數體積和價值。1≤N≤1000,1≤V≤20000。
每件物品的體積和價值範圍在[1,500]。

算法輸出:
輸出揹包最多能裝的物品價值。

示例輸入:
6 3
3 5
2 4
4 2
示例輸出:
9

算法實現:

#include <iostream>
#include <math.h>

using namespace std;

int main() {
    int W = 0, N = 0;
    cin >> W >> N;
    
    int w[N + 1] = {0};
    int v[N + 1] = {0};
    for (int i = 1; i <= N; i++) {
        cin >> w[i] >> v[i];
    }
    
    // 動態規劃方法的思考:
    //   若求最大價值,即需要求在所有體積組合下的最大值,且需要滿足以下條件:
    //   1. 物品總重量小於或等於揹包體積
    //   2. 相同最大值價值的出現,可能發生在不同數量的物品組合,即某一個物品的
    //   價值等於其中未被選擇的某幾個物品的總和;單個物品的最大值不一定就是全局
    //   的最大值,因爲某幾個物品的組合的總價可能大於此物品的價值
    // 因此需要計算加入每個物品的可能性,換言之,需要計算加入每一個物品時,所產
    // 生可能體積總和下的的最大價值總和,然後基於所有可能的體積值下的價值總和,
    // 找出一個最大值。
    
    // 定義:總的物品價值=N*v;總的物品體積=N*w
    // 公式:
    int R[N + 1][W + 1] = {0};
    for (int i = 1; i <= N; i++) {
        // P指當前揹包的空間大小
        for (int p = 1; p <= W; p++) {
            if (p < w[i]) {
                // 裝不下當前物品,就賦值爲添加前一個物品時產生的最大價值
                // 第i-1個物品的已經計算過所有可能的價值
                R[i][p] = R[i-1][p];
            } else {
                R[i][p] = max(R[i-1][p], R[i-1][p - w[i]] + v[i]);
            }
        }
    }
    return R[N][W];
}

算法簡單優化:上面的實現採用了基本的動態規劃的思想,但是會造成空間和時間O(N*W)上的浪費,仔細觀察存儲結果的二維數組,不難發現,我們只需要保存上一次發生替換或追加待選擇的物品時的最大價值即可,這樣就可以通過一維數組來存儲每一次嘗試添加新物品的可能值,核心循環如下:

    int R[W + 1] = {0};
    for (int i = 1; i <= N; i++) {
        // P指當前揹包的空間大小
        for (int p = W; p >= w[i]; p--) {
            // 當前的p位置就是選擇上一個物品時生成的最大價值
            R[p] = max(R[p], R[p - w[i]] + v[i]);
        }
    }
    return R[W];

如果細心的同學翻閱其它揹包問題的類似解,會發現許多達人給的算法最內層循環也都是

for (int p = W; p > w[i]; p--) {
...
}

這種寫法實際上並沒有大多的糾結點,但很多人都對於這種寫法的解讀也真是讓人頭大。
未優化算法的實現,每一行從左至右保存當前可得的最大價值,下一個物品的選擇會依賴上一件物品在位置p - w[i]處記錄的最大值,注意在二維數組的空間下,上一件物品從0到p-w[i]處的結果不會被任何操作覆蓋,但是在一維數組空間下,如果我們依然選擇在內層循環,從左到右(W -> w[i])來嘗試在每個揹包容量大小下更新最大值,那麼在後續計算的過程中,所讀取的位置爲p-w[i]的結果就是被覆蓋後的結果,而不是上一件物品所記錄的最大值,所以這裏必須選擇從右至左的遍歷方向,保證在V的位置處計算的結果依賴的是i-1時有最大值。

完全揹包問題

0/1揹包問題中的物品每個都只能被選擇一次,而在完全揹包問題中,每個物品可能有任意件,具體表述該問題如下:

有N種物品和一個容量爲V的揹包,每種物品都有無限件可用。第i種物品的體積是c,價值是w。將哪些物品裝入揹包可使這些物品的體積總和不超過揹包容量,且價值總和最大?

這個問題跟0/1揹包問題很相像,唯一不同的是每個物品可以是任意個,即最大價值可能是選擇某一個物品放入揹包,直到不能再放入,或是放入x1個物品1,x2個物品2,…,xn個物品n,直到不能再放入。

假設已經得出前M件物品的最大價值,那在嘗試添加第M+1件物品時,可能出現的情況就是1.用第M+1件物品替換前M件物品中的一類(同體積同價值)以獲得新的最大值
2.用第M+1件物品替換前M件物品中的某幾個類以獲得新的最大值

基於0/1揹包的動態規劃的方法來實現,代碼如下:

for (int i = 1; i<= N; i++) {
    for (int p = 1; p <= W; p++) {
        // 向下取整,即k * w[i] <= p
        if (w[i] >= p) {
            R[i][p] = R[i-1][p];
        } else {
            // 嘗試用k件物品i來替換前i-1個物品的某一類或某幾類物品,計算最大價值
            for (int k = 1; k <= p / v[i]; k++) {
                R[i][p] = max(R[i-1][p], R[i-1][p - k * w[i]] + k * v[i]);
            }
        }
    }
}
return R[N][W];

優化的算法也是基於0/1揹包問題的優化算法的,使用一維數組來保存前一次的遍歷結果,但內層循環須採用順序遍歷,而非逆序,關於其更詳細的解釋可參考百度百科啦。

for (int p = 1; p < W; p++) {
    for (int k = 1; k <= p/v[i]; k++) {
        R[p] = max(R[p], R[p - k*w[i]] + k*w[i]);
    }
}

https://baike.baidu.com/item/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98

多揹包問題

問題描述如下:

有N種物品和一個容量爲V的揹包。第i種物品最多有x件可用,每件體積是c,價值是w。求解將哪些物品裝入揹包可使這些物品的體積總和不超過揹包容量,且價值總和最大。

此問題跟完全揹包類似,唯一不同的是這裏會限制每個物品的數量,但我們依然可以在完全揹包問題的動態規劃算法基礎之上,來很簡單求解此問題:

for (int i = 1; i <= N; i++) {
    for (int p = 1; p <= W; p++) {
        // x[] 存儲每個物品的數量
        // w[] 存儲每個物品的重量
        // v[] 存儲每個物品的價值
        for (int k = 0; k <= min(p/w[i], x[i])) {
            R[i][p] = max(R[i - 1][p], R[i - 1][p - k*w[i]] + k*v[i]);
        }
    }
}
return R[N][W];

更爲樸素的解法,可以把此問題轉爲0/1揹包問題,即把所有的物品都當成一個獨立的物品,此時的物品數N=j=1mxi[j]N=\sum_{j=1}^m x_i[j]

但在數據量比較的情況下,上面的解決方法的時間複雜度都O(Nj=1xi)O(N * \sum_{j=1} x_i),那有沒有更優化的算法呢?答案是必然的。

實際上不論是完全揹包或是多重揹包問題,都可以轉換爲固定個數的某幾類物品的組合,針對於某一類物品,設有M個,樸素的方式是對這M個物品物品各做一次遍歷,那我們可不可優化遍歷這M個相同物品的方式呢,從局部優化進而推廣全局?

從數學上來看,一個任意的整數,不外乎奇數或偶數,都可以通過1和任意多個2的組合來表達(二分法)。比如11,可以寫成11=1+2+222=1+2+811= 1 + 2 + 2 * 2 * 2 = 1 + 2 + 8,結合到揹包問題上,就是容量N爲11的揹包,可以裝入體積爲1的物品、體積爲2的物品、體積爲8的物品,這樣就把一類M個體積爲w的物品轉換爲了c120+c221+...+ck12(k1)+ck2kc_1 * 2^0 + c_2 * 2^1 + ... + c_{k-1} * 2^{(k-1)} + c_k * 2^k個不同物品的組合,其中ci=012k<=Mwc_i = { 0 | 1},2^k <= M * w

通過這種方式就可以把原來需要遍歷M次某類物品的操作降爲了log2M\log_2M次,同理對其它類物品也作同樣的分法,最終使得N次的遍歷操作降爲了log2N\log_2N次,最終通過0/1揹包問題的解法來最終解決此問題,算法如下:

// V 揹包容量
// M 不同類物品的個數
// m[] 保存了某類物品的個數
// w[] 保存了某類物品的體積
// v[] 保存了某類物品的價值
// N 保存了新組合後的物品個數
// ww[] 保存了新組合後的物品體積
// vv[] 保存了新組合後的物品價值
N = 0;
for (int i = 0; i < M; i ++) {
    int total = m[i];
    int res = total;
    while(res > 0) {
        int cnt = 0;
        // 拆分m個物品
        for (int j = 0; pow(2, j) <= res; j++) {
            cnt = j;
            res = total - pow(2, cnt);
        }
        // 如果能夠分爲1或2個的整數倍,則可以新組合一個物品
        if (res >= 0) {
            ww[N++] = cnt * w[i];
            vv[N] = cnt * v[i];
        } 
    }
}
R[V + 1] = {0};
for (int i = 1; i < N; i ++) {
    for (int j = V; j >= ww[i]; j--) {
        R[j] = max(R[j], R[j - ww[i]] + vv[i]);
    }
}
發佈了9 篇原創文章 · 獲贊 6 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章