背包问题九讲笔记-01背包问题

问题描述

N 件物品和一个容量为V的背包.放入第 i 件物品.放入第i件物品耗费的容量是 Ci ,所获得的价值是 Wi .每件物品只有一个.求将哪些物品放入背包可使价值总和最大.

思考过程

一般来说求极值的问题可分为贪心,动态规划,以及遍历所有可能.在这三中方法中,动态规划是最常见的,也是很难想出来的.其中最难的是定义子问题,写出动态转移方程.类似于将问题简化,如何由较小的子问题,推到出较复杂的问题.类似的计算机的本质是极其简单的二进制运算,但由无数二进制运算叠加在一起,组成了今天复杂的互联网系统.

在上次面试中,由于没想出思路,很紧张,之后写最简单的01背包也没有写出来.面试后,痛定思痛,之后要逐步加强的自己的算法思维能力,证明问题能力,找错能力,工具应用能力.

思路

一般可从最简单入手:

  • 1如果没有物品,解是什么?显而易见,由于没有任何物体,得到的价值为0.这一般对应于动态规划中的初始化.
  • 2再复杂一点点,如果有一个物体,如何求解?直接比较物品体积和背包容量,如果小于,直接拿下,解就是物体的价值,否则还是0.
  • 3 当有两个物体时呢?如果先考虑第二个物体,那么有两种选择,装或者不装,两种选择之后,会产生新的容量,以及第一个物体,那么问题回归到第二种情况.
  • 如果有 N 个物体,如果放完第N个物体(选择放或者不放)后,那么问题转化为含有 N1 个物体的子问题.这样问题就逐渐分解了.

定义子问题

F[i,v] 表示前 i 件物品放入一个容量为v的背包所获得的最大价值.

状态转移方程

在放与不放两个子问题中选择一个最大的解

F[i,v]=max(F[i1,v],F[i1,vCi]+Wi)

伪代码

F[0, 0...V] = 0
for i = 1 to N 
    for j = 0 to V
        if(C[i] > j)
            F[i, j] = F[i - 1, j]
        else
            F[i, j] = max(F[i - 1, j], F[i - 1, j - C[i]] + W[i])

优化

时间

时间复杂度已无法再优化,.

T=O(NV)

空间

这里使用的是二维数据,但求解第 i 行值时,只利用到i1行的解,并未利用更久之前的解,由于有这种局限解的特性,可以利用一维的滚动数组模拟二维数据,另外由于这里由 i1 的前面的解 jc[i] 推导出 i 后面的解j,也就是利用一维的数据的话,旧解为前面的解,新解为后面的.那么就应该让 j 从大到小进行循环遍历,因为这样第一次接触的到为旧解i1,新出来的新解 j 在此次遍历也不会再用到.也就是当且仅使用了一次.如果是从小到大遍历j,那么新求出的解 j+C[i] 在后面还会遍历到,因此还会被重新用到,再次用到的话,还会选择是否拿下这个物体,不符合题意每件物品只有一个(如果有无穷多个物品,就可以从小到大遍历,之后的完全背包问题会提到)

S=O(V)

优化空间后的伪代码 1

F[0...V] = 0
for i = 1 to N
    for j = V to C[i]
        F[j] = max(F[j], F[j - C[i]] + W[i]

代码更将简单优雅

初始化的细节

如果是求恰好要装满背包时的最优解,那么像上面的都初始化为0就不行了.这时候需要只有F[0, 0] = 0, F[0, 1…V] = INT_MIN(表示负无穷).可以理解只有0个物品,背包为0容量正好有解,其余解为无效解.如果手工画出来子问题转化图示,这样最基础的子问题只能从 F[0,0] 出发,不会从其他无效子问题出发,因为初始化无效解是负无穷,两者去较大的话,肯定不会选取负无穷.

一个常数的优化

可将伪代码1再次优化为

for i = 1 to N
    for j = V to max(V - sum(C[i...N]), C[i])
        F[j] = max(F[j], F[j - C[i]] + W[i])

由于只需要最后F[V]的值,倒推前一个物品,其实只要知道F[V-C[N]]即可。以此类推,对以第j个背包,其实只需要知道到F[v-sum{C[j…n]}]即可

求取最优方案

参照一般动态规划问题输出方案的方法:记录下每个状态的最优质是由状态转移方程的哪一项得到的,也就是记录最优解时,是否选择了这个物品.具体来说是利用path[i, j] 来记录当前的转移过程,path[i, j] = true表示选取物品i, path = False 表示未选取当前物品.(bool数组更加节省内存).空间复杂度 S=O(VN) .

代码库(C++)

不输出最优方案

void zeroOnePack(int c, int w, int V, vector<int> & F){
    for(int i = V; i >= c; i--)
        F[i] = max(F[i], F[i - c] + w);
}
int allZeroOnePack(vector<int>& C, vector<int>& W, int V){
    vector<int> F(V + 1, 0);
    for(int i = 0; i < C.size(); i++)
        zeroOnePack(C[i], W[i], V, F);
    return F[V];
}

输出最优方案

void zeroOnePack(int i, int c, int w, int V, vector<int> & F, vector<vector<bool> > &path){
    for(int j = V; j >= c; j--){
        int choose = F[j - c] + w;
        if(choose > F[j]){
            F[j] = choose;
            path[i][j] = true;
        }
    }
}
int allZeroOnePackDetail(vector<int>& C, vector<int>& W, int V){
    vector<int> F(V + 1, 0);
    vector<vector<bool> > path(C.size(), vector<bool>(V + 1, false));
    for(int i = 0; i < C.size(); i++){
        zeroOnePack(i, C[i], W[i], V, F, path);
    }
    return F[V];
}
void printMethod(vector<int>& C){
    int i = N - 1;
    int v = V;
    while(i >= 0){
        if(path[i][v]){
            cout<<i<<" ";
            v = v - C[i];
        }
        i--;
    }
}

另一个小的常数时间优化

在利用dfs递归求解时,先将物品按照单位价格排好序,单位价格高的靠前,这样如果某个物品超载时,没必要再累积其后面物品的价格, 而是按照该物品的单位价格乘以剩余容量,这样算出的总价格虽然比实际装载的总价格略高些,如果这样略高于实际值的解还低于当前的最优解,则可对后面剪枝,避免多余的计算.

Reference

  1. 背包九讲
  2. 0-1背包问题(回溯结点类排序改进)
  3. 01背包问题:Charm Bracelet (POJ 3624)(外加一个常数的优化)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我明白了。关于分支限界法的01背包问题,我可以给你一些思路和算法实现的笔记。 首先,01背包问题是指在有限的背包容量下,从给定的一组物品中选取一些物品放入背包中,使得背包中物品的总价值最大。而分支限界法则是一种解决优化问题的方法,它通过限制问题的搜索空间,逐步缩小问题的解空间,从而获得最优解。 对于01背包问题,我们可以采用以下步骤: 1. 构造一个搜索树,根节点代表空背包,每个节点代表向背包中添加一个物品或不添加物品的决策。 2. 对于每个节点,计算当前背包中物品的总价值以及还有多少容量可以使用。 3. 对于每个未被扩展的节点,计算它的上界价值(即背包在剩余容量中所能获得的最大价值),并按照上界价值从大到小排序。 4. 选择上界价值最大的节点进行扩展,即添加一个物品或不添加物品的两种决策分别扩展出两个子节点。 5. 不断重复步骤3和4,直到搜索树中的所有节点都被扩展完毕。 以下是一个简单的C语言实现,其中用一个结构体表示每个节点的状态,通过一个优先队列来实现按照上界价值排序的功能: ```c #include <stdio.h> #include <stdlib.h> #define MAX_N 1000 int n, c; int w[MAX_N], v[MAX_N]; int max_v; typedef struct Node { int level; int value; int weight; int bound; } Node; typedef struct PriorityQueue { Node* arr[MAX_N]; int size; } PriorityQueue; PriorityQueue* createPriorityQueue() { PriorityQueue* q = (PriorityQueue*)malloc(sizeof(PriorityQueue)); q->size = 0; return q; } void push(PriorityQueue* q, Node* node) { q->arr[q->size++] = node; int i = q->size - 1; while (i > 0 && q->arr[i]->bound > q->arr[(i - 1) / 2]->bound) { Node* temp = q->arr[i]; q->arr[i] = q->arr[(i - 1) / 2]; q->arr[(i - 1) / 2] = temp; i = (i - 1) / 2; } } Node* pop(PriorityQueue* q) { Node* node = q->arr[0]; q->arr[0] = q->arr[--q->size]; int i = 0; while (i * 2 + 1 < q->size) { int j = i * 2 + 1; if (j + 1 < q->size && q->arr[j + 1]->bound > q->arr[j]->bound) { j++; } if (q->arr[i]->bound < q->arr[j]->bound) { Node* temp = q->arr[i]; q->arr[i] = q->arr[j]; q->arr[j] = temp; i = j; } else { break; } } return node; } void solve() { PriorityQueue* q = createPriorityQueue(); Node* root = (Node*)malloc(sizeof(Node)); root->level = 0; root->value = 0; root->weight = 0; root->bound = max_v; push(q, root); while (q->size > 0) { Node* node = pop(q); if (node->bound <= max_v || node->level == n) { continue; } Node* left = (Node*)malloc(sizeof(Node)); left->level = node->level + 1; left->value = node->value + v[left->level]; left->weight = node->weight + w[left->level]; if (left->weight <= c) { left->bound = left->value + (max_v - left->value) * 1.0 / w[left->level]; if (left->value > max_v) { max_v = left->value; } push(q, left); } Node* right = (Node*)malloc(sizeof(Node)); right->level = node->level + 1; right->value = node->value; right->weight = node->weight; right->bound = right->value + (max_v - right->value) * 1.0 / w[right->level]; push(q, right); } } int main() { scanf("%d%d", &n, &c); for (int i = 1; i <= n; i++) { scanf("%d%d", &w[i], &v[i]); max_v += v[i]; } solve(); printf("%d", max_v); return 0; } ``` 希望这些笔记能够对你有所帮助,如果有任何问题,欢迎随时问我。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值