014-背包问题-动态规划-《算法设计技巧与分析》M.H.A学习笔记

01背包:

01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2……Wn,与之相对应的价值为P1,P2……Pn。求能获得的最大总价值。

基本思路:

V[i,j]表示从前i件物品中取出的,能装入体积为j的背包的物品的最大总价值。

初始化条件:

V[i,0]和V[0,j]都为0,我们从其意义上就可以理解。

状态转移方程:

V[i,j]=max{ V[i-1,j],V[i-1,j-Wi]+Pi }
,前后分别为第i件物品不取和取得情况。

总的就是下面的递推式:

算法分析:

表的大小为n*C,所以算法的时间复杂度为Θ(nC),经过一些修改空间复杂度可以控制在Θ(C)内。

伪代码:

C++代码:

1.Θ(nC)的空间。

for(int i=0;i<=V;i++) dp[0][i]=0;  // 初始条件
 for(int i=1;i<=n;i++){
for(int v=0; v<=C[i]-1; v++){
dp[i][v]=dp[i-1][v];
}
for(int v=C[i];v<=V;v++){
dp[i][v]=max(dp[i-1][v],dp[i-1][v-C[i]]+W[i])
}
}
cout<<dp[n][V]<<endl;

2.在空间上做一些优化,Θ(C)的空间。

 for(int i=0;i<=V;i++) dp[i]=0;  // 初始条件
 for(int i=1;i<=n;i++){
for(int v=V;v>=C[i];v--){
dp[v]=max(dp[v],dp[v-C[i]]+W[i])
}
}
cout<<dp[V]<<endl;

空间优化的基本思路:

我们知道原来代码中的二维数组的i是为了表示在前i个物品中做选择,同时也标志第i个物品是否已经选取了。

每次决策的时候是决定第i个物品是否要选取。比如,对dp[i-1][v-ci]我们知道第i个物品并没有选取,而对于dp[i][v-ci]我们可以知道第i个物品已经被选取了,我们每次自从前面一个状态(i-1)来决策第i个是否要选。

而空间优化的代码通过另外一套机制来保证一个物品只选一次。

我们可以看到第二个代码中的v是按逆序循环的,这样做是很有必要的:

这是因为要保证第i次循环中的状态dp[v]是由状态dp[v-c]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个没有已经选入第i件物品的子结果dp[v-ci](如果已经选入了,即dp[v]已经完成状态转移方程,不会再进行)。

完全背包问题:

有N件物品和一个容量为V的背包。放入第i件物品所耗的容量为Ci,得到的价值为Wi,但是同一件物品可以放入任意多件,问您最多可以获得多少价值。

(1)二维数组的做法:时间复杂度O(NVlog2(V/C[i]))

基本思路

这里与01背包不同的是每一件物品可以选任意多件,我们只需要在状态转移方程上进行一些改动:

V[i][j]=max{ V[i-1][j-k*c[i]]+k*w[i] | 0<=k*c[i]<=v }

这里的k表示的是第i件物品选取的数量,在程序中,我们只需为k多进行一个循环,并注意k的取值范围,就可以解决完全背包问题。

伪代码:

     F[0][] ← {0}
     F[][0] ← {0}
     for i←1 to N
         do for j←1 to V
             do for k←0 to j/C[i]
                if(j >= k*C[i])
                     then F[i][k] ← max(F[i][k],F[i-1][j-k*C[i]]+k*W[i])
     return F[N][V]

(2)一维数组的做法:时间复杂度O(NV)

直接放代码:

C++代码:

for(int i=0;i<=V;i++) dp[i]=0;  // 初始条件
 for(int i=1;i<=n;i++){
for(int v=C[i];v<=V;v++){
dp[v]=max(dp[v],dp[v-C[i]]+W[i])
}
}

基本思路:

这里和01背包的空间优化代码差不多,改变的是v的循环顺序。前面v逆序循环的目的是为了保证每件物品只选择一次,改为正序循环后,每件物品选择的次数可以是任意的。

看下面这个例子dp[v-ci]后选择了第i件物品变为dp[v],而dp[(v+ci)-ci]仍然可以选择第i件物品,变为dp[v+ci]。

多重背包:

有N件物品和一个容量为V的背包。放入第i件物品所耗的容量为Ci,得到的价值为Wi,但是第i件物品最多可以放入Mi件,问您最多可以获得多少价值。

基本思路:

(1)二维数组的做法

与前面两种背包的做法类似,只在状态转移方程上做一些更改。

dp[i][v]=max{ dp[i-1][v-k*c[i]]+k*w[i] | 0<=k<=m[i] }

就不贴代码了。

(2)转化为01背包

空间优化的做法是将其转化为01背包,并采用二进制的做法进行拆分,即拆分成1件、2件、4件...。

for(int i=1;i<=n;i++){
 int num=m[i]; // num为第i件物品由多少件
 for(int k=1;num>=0;k*=2){
int mul=min(k,num) //k即为2进制数,之所以要和num取最小就类似与1000的时候512和489的情况,我们要选的时489.
for(int j=V;j>=C[i]*mul;j--){
dp[j]=max(dp[j],dp[j-C[i]*mul]+v[i]*mul)
}
num-=mul; // 分完那堆之后从总数上扣掉
            }
}

最后贴一个代码:

#include <iostream>
using namespace std;

const int N = 3;//物品个数
const int V = 8;//背包容量
int Weight[N + 1] = {0,1,2,2};
int Value[N + 1] = {0,6,10,20};
int Num[N + 1] = {0,10,5,2};

int f[V + 1] = {0};
/*
f[v]:表示把前i件物品放入容量为v的背包中获得的最大收益。
f[v] = max(f[v],f[v - Weight[i]] + Value[i]);
v的为逆序
*/
void ZeroOnePack(int nWeight,int nValue)
{
for (int v = V;v >= nWeight;v--)
{
f[v] = max(f[v],f[v - nWeight] + nValue);
}
}

/*
f[v]:表示把前i件物品放入容量为v的背包中获得的最大收益。
f[v] = max(f[v],f[v - Weight[i]] + Value[i]);
v的为增序
*/
void CompletePack(int nWeight,int nValue)
{
for (int v = nWeight;v <= V;v++)
{
f[v] = max(f[v],f[v - nWeight] + nValue);
}
}

int MultiKnapsack()
{
int k = 1;
int nCount = 0;
for (int i = 1;i <= N;i++)
{
if (Weight[i] * Num[i] >= V)
{
//完全背包:该类物品原则上是无限供应,
//此时满足条件Weight[i] * Num[i] >= V时,
//表示无限量供应,直到背包放不下为止.
CompletePack(Weight[i],Value[i]);
}
else
{
k = 1;
nCount = Num[i];
while(k <= nCount)
{
ZeroOnePack(k * Weight[i],k * Value[i]);
nCount -= k;
k *= 2;
}
ZeroOnePack(nCount * Weight[i],nCount * Value[i]);
}
}
return f[V];
}

int main()
{
cout<<MultiKnapsack()<<endl;
system("pause");
return 1;
}

更进一步的了解,可以看看Tianyi Cui《背包问题九讲》。

时间: 2024-11-12 16:39:43

014-背包问题-动态规划-《算法设计技巧与分析》M.H.A学习笔记的相关文章

013--Floyd算法-动态规划-《算法设计技巧与分析》M.H.A学习笔记

多源最短路:有向图,求从每个顶点到其他所有顶点的最短距离. 基本思路: 假设有向图的所有点编号为1到n,l[i,j]表示从i到j的边的长度,如果不存在边,则置为正无穷. 定义d(k,i,j)表示从点i到点j,并且不经过编号大于k的点的最短距离. 初始化条件: K=0时,d(0,i,j)=l[i,j]. 状态转移方程: d(k,i,j)=min{ d(k-1,i,j),d(k-1,i,k)+d(k-1,k,j) }   1<=k<=n 于是我们有如下的递归式: 算法分析: 显然,Floyd算法的

016-kruskal算法-贪心-《算法设计技巧与分析》M.H.A学习笔记

最小生成树: 在一给定的连通无向图G = (V, E)中,(u, v) 代表连接顶点u与顶点v的边,而 w(u, v)代表此边的权重,若存在T为G的子集且为无循环图,使得w(T) 最小,则此T为G的最小生成树. 基本思路: kruskal算法总共选择n- 1条边,所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中.注意到所选取的边若产生环路则不可能形成一棵生成树.kruskal算法分e 步,其中e 是网络中边的数目.按耗费递增的顺序来考虑这e 条边,每次

019-dfs.bfs-图的遍历-《算法设计技巧与分析》M.H.A学习笔记

深度优先搜索DFS 深搜框架: bool dfs(int loc) { 标记状态loc已访问; if (loc为目标状态) return true; for (每个可能的操作) { 对loc应用操作产生新状态nstat; if (nstat合法且未被访问) { if (dfs(nstat)) return true; } } 撤销loc已访问标记; // 这步要具体问题具体分析了 return false; } 广度优先搜索BFS 实现方法 1. 首先将根节点放入队列中. 2. 从队列中取出第一

017-Prim算法-贪心-《算法设计技巧与分析》M.H.A学习笔记

基本思路: 定义结点集合U, V (U表示已经选择加入MST的结点集合,V表示未选) 1. 任选一个结点加入U 2. 选择一条边权最小的边,他的两个结点分别属于U, V,并把属于V的那个结点加入U 3. 重复执行2直到V空 伪代码: C++代码: int g[mnx][mnx]; int n, m; int d[mnx]; // 朴素 prim, 复杂度O(|V|^2) |V|:点数, |E|:边数 int prim() { memset(d, 0x3f, sizeof d); //初始化 in

算法设计技巧与分析笔记 第一章

1.搜索:设A[1--n]为一个n个元素的数组,判定给定元素x是否在A中 线性搜索:直接扫描A中所有项目,将每个项目与x做比较. 二分搜索: A[low--high]为有序非空数组(假定为升序),A[mid]为中间元素 假定x>A[mid],则丢弃A[low-mid],继续搜索A[mid+1-high]: 假定x<A[mid],则丢弃A[mid-high],继续搜索A[low-mid-1]: 若x=A[mid],返回mid,结束搜索. 算法分析:时间复杂度:O(log n) 2.排序:设A[1

018-Huffman树-贪心-《算法设计技巧与分析》M.H.A学习笔记

Huffman树是完全二叉树,权重较大的节点距离根较近. Huffman编码是一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字. 基本思路: 建立Huffman树的过程: 假设有n个权值,则构造出的哈夫曼树有n个叶子结点. n个权值分别设为 w1.w2.-.wn,则哈夫曼树的构造规则为: (1) 将w1.w2.-,wn看成是有n 棵树的森林(每棵树仅有一个结点): (2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左.右子树,且新树的根结点权值为其左.右子树

五种常用的算法设计技巧之二:分治算法

一,介绍 分治算法主要包含两个步骤:分.治.分,就是递归地将原问题分解成小问题:治则是:在解决了各个小问题之后(各个击破之后)合并小问题的解,从而得到整个问题的解 二,分治递归表达式 分治算法一般都可以写出一个递归表达式:比如经典的归并排序的递归表达式:T(N)=2T(N/2)+O(N) T(N)代表整个原问题,采用了分治解决方案后,它可以表示成: ①分解成了两个规模只有原来一半(N/2)的子问题:T(N/2) ②当解决完这两个子问题T(N/2)之后,再合并这两个子问题需要的代价是 O(N) 递

《数据结构与算法分析:C语言描述》复习——第十章“算法设计技巧”——Alpha-Beta剪枝

2014.07.08 22:43 简介: “搜索”与“剪枝”几乎是如影随形的.此处的“搜索”指的是带有回溯算法的深度优先搜索. 在之前的“Minimax策略”中我们给出了一个三连棋的程序,运行后你就知道计算一步棋要花多少时间. 为了计算最优的一步棋,我们可能需要递归9万多次.如果毫无疑问这种阶乘式的穷举过程必须通过剪枝来加速. 本篇介绍一种用于Minimax策略的剪枝思路——α-β剪枝. 剪枝的英语是pruning,所以不要想当然说成trimming. 图示: 在上一篇讲解Minimax策略的博

数据结构与算法分析(六)——算法设计技巧

从算法的实现向算法的设计转变,提供解决问题的思路 1.贪心算法 一种局部最优算法设计思路,思想是保证每一步选择在当前达到最优.一个很常见的贪心算法案例是零钱找取问题. 调度问题:书上的调度问题比较简单,其目标是所有作业的平均持续时间(调度+运行)最短,无论是但处理器还是多处理器,最优解的方案总是按作业的长短排序进行调度.<计算机算法设计与分析>上的作业调度的目标是最后完成时间最小,这要稍微复杂一些. 霍夫曼编码:最优编码必定保持满树的性质,基本问题在于找到总之最小的满二叉树.最简单的霍夫曼编码