绪言
最近为了能够在机器学习平台支持公司内的各类应用任务,如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 = 1 m x i [ j ] N=\sum_{j=1}^m x_i[j] N=∑j=1mxi[j]。
但在数据量比较的情况下,上面的解决方法的时间复杂度都 O ( N ∗ ∑ j = 1 x i ) O(N * \sum_{j=1} x_i) O(N∗∑j=1xi),那有没有更优化的算法呢?答案是必然的。
实际上不论是完全背包或是多重背包问题,都可以转换为固定个数的某几类物品的组合,针对于某一类物品,设有M个,朴素的方式是对这M个物品物品各做一次遍历,那我们可不可优化遍历这M个相同物品的方式呢,从局部优化进而推广全局?
从数学上来看,一个任意的整数,不外乎奇数或偶数,都可以通过1和任意多个2的组合来表达(二分法)。比如11,可以写成 11 = 1 + 2 + 2 ∗ 2 ∗ 2 = 1 + 2 + 8 11= 1 + 2 + 2 * 2 * 2 = 1 + 2 + 8 11=1+2+2∗2∗2=1+2+8,结合到背包问题上,就是容量N为11的背包,可以装入体积为1的物品、体积为2的物品、体积为8的物品,这样就把一类M个体积为w的物品转换为了 c 1 ∗ 2 0 + c 2 ∗ 2 1 + . . . + c k − 1 ∗ 2 ( k − 1 ) + c k ∗ 2 k c_1 * 2^0 + c_2 * 2^1 + ... + c_{k-1} * 2^{(k-1)} + c_k * 2^k c1∗20+c2∗21+...+ck−1∗2(k−1)+ck∗2k个不同物品的组合,其中 c i = 0 ∣ 1 , 2 k < = M ∗ w c_i = { 0 | 1},2^k <= M * w ci=0∣1,2k<=M∗w。
通过这种方式就可以把原来需要遍历M次某类物品的操作降为了 log 2 M \log_2M log2M次,同理对其它类物品也作同样的分法,最终使得N次的遍历操作降为了 log 2 N 次 \log_2N次 log2N次,最终通过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]);
}
}