一、动态规划基本概念
1. 概念
动态规划是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。
2. 动态规划的基本思想
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
总结为一句话,将动态规划的问题可以分为多个阶段,对每个阶段用某种决策解决子问题,得到一个无后效性的子状态(即这个状态由历史性的,不再改变),一步步推导到最后就是原问题的解。
3. 三个特性
1. 最优化原理(最优子结构)
一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
2. 无后效性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3. 重叠子问题
动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
思考几个问题
假设现在想开发一款产品,帮助用户挑选选购商品,满足以下需求:
商城有n种商品,每种商品限购1件,用户有V块钱,第i件商品费用为c[i],价值为w[i],问用户能买到的最大的商品价值是多少?
商城有n种虚拟商品,每种商品库存无限,用户有V块钱,第i件商品费用为c[i],价值为w[i],问用户能买到的最大的商品价值是多少?
商城有n种商品,用户有V块钱,第i件商库存为n[i],费用为c[i],价值为w[i],问用户能买到的最大的商品价值是多少?
提示:n件物品,每件物品可以选或者不选。
二、动态规划系列问题:背包问题
1:01背包
1.1 问题:n件物品,背包容量为V,第i件物品费用为c[i],价值为w[i],求哪些物品装入背包可使价值总和最大。
特点:每种物品仅有一件,可以选择放或不放。
状态f[i][j]:表示前i件物品放入一个容量为j的背包的最大价值。
状态方程:f[i][j] = max { f[i][j] , f[ i-1 ][ j-c[i] ] + w[i] } //不放的情况和放的情况的最大值
时间复杂度:O(nv)
空间复杂度:O(v)
代码:
void Pack01(int *bag,int cost,int weight) //01背包放法
{
for(int i=v;i>=cost ;i--) //v为最大花费,没有则改为maxn-1
{
bag[i] = Max ( bag[i] , bag[ i-cost ] + weight ); //一样的放或不放
}
}
i | weight | value | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
1 | 4 | 6 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 5 | 4 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 10 | 10 |
3 | 6 | 5 | 0 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 10 | 11 |
4 | 2 | 3 | 0 | 0 | 3 | 3 | 6 | 6 | 6 | 9 | 9 | 10 | 11 |
5 | 2 | 6 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
1.2 初始化问题:
代码:
void ExactlyInit() //要求 物品重量 恰好等于 背包容量 的初始化{
bag[0] =0 ;
for(int i=1;i<maxn;i++)
{
bag[0]= -0x7fffffff ;
}
}
void NotExactlyInit() //要求 物品重量 不用恰好等于 背包容量 的初始化{
memset(bag,0,sizeof(bag)) ;
}
1.3 优化空间复杂度:
- 滚动数组
f[maxn][maxn] 改为f[2][maxn] ,使用时f[i][j]改为f[i&1][j] - 减少一维
f[maxn][maxn] 改为f[maxn]
使用时for循环方向相反
for(int i=v;i>=cost ;i--) //v为最大花费,没有则改为maxn-1
{
bag[i] = Max ( bag[i] , bag[ i-cost ] + weight ); //一样的放或不放
}
1.4 输出方案
可以参照一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。
输出字典序最小的最优方案
这里“字典序最小”的意思是1..N号物品的选择方案排列出来以后字典序最小。以输出01背包最小字典序的方案为例。
一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。首先,子问题的定义要略改一些。我们注意到,如果存在一个选了物品1的最优方案,那么答案一定包含物品1,原问题转化为一个背包容量为v-c[1],物品为2..N的子问题。反之,如果答案不包含物品1,则转化成背包容量仍为V,物品为2..N的子问题。不管答案怎样,子问题的物品都是以i..N而非前所述的1..i的形式来定义的,所以状态的定义和转移方程都需要改一下。但也许更简易的方法是先把物品逆序排列一下,以下按物品已被逆序排列来叙述。
//字典序最小的最优方案void Pack01mincase(int bag[maxn][maxn],int *cost,int *weight){
for(int i=n;i>0;i--) //从后往前放入背包,最后结果在bag[1][v]
{
for(int j=v;j>cost[i];j--)
{
bag[i][j] = Max (bag[i-1][j] , bag[i-1][j-cost[i]]+weight[i]) ;
}
}
int cases[maxn] ={0}; //保存第i件物品是否有选择
int j =v ;
for(i=1;i<=n;i++)
{
if(bag[i][v] == bag[i+1][v-cost[i]]+weight[i])
{
cases[i] = 1;
v-=cost[i] ;
}
}
}
2:完全背包
问题:n种物品,背包容量为V,第i件物品费用为c[i],价值为w[i],求哪些物品装入背包可使价值总和最大
特点:每种物品无限件,可以选择放任意多件。
状态f[i][j]:表示前i件物品恰好放入一个容量为j的背包的最大价值。
状态方程:f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k*c[i]<=v}
时间复杂度:O(nv)
空间复杂度:O(v)
代码:
void Packall(int *bag,int cost,int weight) //完全背包放法
{
for(int i=cost;i<=v;i++) //v为最大花费,没有则改为maxn-1
{
bag[i] = Max ( bag[i] , bag[ i-cost ] + weight ); //一样的放或不放
}
}
3:多重背包
问题:n种物品,背包容量为V,第i件物品最多有n[i]件,费用为c[i],价值为w[i],求哪些物品装入背包可使价值总和最大。
特点:第i件物品有n[i]件,可取0、1、2….n[i]件。
状态f[i][j]:表示前i件物品恰好放入一个容量为j的背包的最大价值。
状态方程:f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k<=n[i]}
时间复杂度:O(V*Σlog n[i])
空间复杂度:O(v)
解决方法:转化为01背包,把n[i]件物品拆分为n[i]种物品
二进制拆分优化:每1、2、4、8…件物品合并成一种物品
代码:
void Packmany (int *bag, int cost ,int weight ,int num ) //多重背包
{
if(cost * num >= v) //如果物品总价值大于背包容量,则转化为完全背包问题
{
Packall(bag ,cost , weight);
return ;
}
for(int k=1 ; k < num ; k*=2) //拆分为1、2、4、8……件的组合,转化为01背包问题
{
Pack01 (bag,k*cost , k*weight) ;
num = num -k ;
}
Pack01 (bag ,num*cost , num*weight ) ;
}
4:混合背包
问题:n种物品,背包容量为V,第i件物品最多有n[i]件,费用为c[i],价值为w[i],求哪些物品装入背包可使价值总和最大
特点:前3种背包混合
解决方法:根据n[i]判断是哪种背包,就调用上面哪种背包的函数就ok了。
//伪代码for i=1..N
if 第i件物品是01背包
ZeroOnePack(c[i],w[i])
else if 第i件物品是完全背包
CompletePack(c[i],w[i])
else if 第i件物品是多重背包
MultiplePack(c[i],w[i],n[i])
接受得了就继续:
商城有一些商品,既可以用人民币购买,也可以用积分兑换,人民币+积分购买也可以哦~~
商城有n种商品,用户有V块钱,U的积分,第i件商品可花费c1[i]块钱或c2[i]积分,价值为w[i],问用户能买到的最大的商品价值是多少?
提示:n件物品,每件物品可以可使用钱,也可使用积分,也可同时使用,也可不选。
5:二维费用背包
问题:n种物品,第i件物品有两种代价,代价1为cost1[i],代价2为cost2[i],价值为w[i],现有代价1的总量为V,代价2的总量为U,求哪些物品装入背包可使价值总和最大
特点:每件物品有两种代价。
状态f[i][v][u]:表示前i件物品放入一个代价1的总量为v,代价2的总量为u的背包的最大价值。
状态方程:f[i][v][u]=max{f[i-1][v][u],f[i-1][v-c1[i]][u-c2[i]]+w[i]}
还有时间,就再接着讲:
用户觉得同类商品买一件就好了,不想买太多。。。比如空调有很多型号,最多买一个。
商城有n种商品商,同类商品限购1件,用户有V块钱,第i件商品费用为c[i],价值为w[i],问用户能买到的最大的商品价值是多少?
提示:n件物品,分为多个组,每组可选也可不选。
6. 分组背包
问题:有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
状态:f[k][v]表示前k组物品花费费用v能取得的最大价值 状态方程:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
//一维数组的伪代码for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
还有时间,就再接着讲:
满赠换购。。。
商城有一些商品,买了之后你就可以加钱买另一件商品了。。。商品都限购1件,用户有V块钱,第i件商品费用为c[i],价值为w[i],问用户能买到的最大的商品价值是多少?
提示:物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。另外,没有某件物品同时依赖多件物品。
7. 依赖背包
问题:有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中必须先选主键,才能选附件,所有物品最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
特点:商品之间依赖关系
描述: 可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有n个附件,则策略有2^n+1个,为指数级。)
考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。
这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f'[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f'[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为V-c[i]+1个物品的物品组,就可以直接应用P06的算法解决问题了。
解法:
- 将不同的主键和对应的附件分为一个组
- 对每一个组进行一次01背包,得到容量为0..V-c[i]对应的最大价值f'[0..V-c[i]]。
- 将每组的解合并到所有组的解。
void Union(int *a,int *b) //泛化物品的和,结果放在a里{
for(int i=maxn-1;i>=0;i--)
{
for(int k=0;k<=i;k++)
{
a[i] = Max( a[i] , a[i-k]+b[k] );
}
}
}
for 所有的组k
Union(a,b)
事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这也是“泛化物品”的思想。这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。
8. 泛化物品
更严格的定义之。在背包容量为V的背包问题中,泛化物品是一个定义域为0..V中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。
这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0..V],给它费用v,可得到价值h[V]。
一个费用为c价值为w的物品,如果它是01背包中的物品,那么把它看成泛化物品,它就是除了h(c)=w其它函数值都为0的一个函数。如果它是完全背包中的物品,那么它可以看成这样一个函数,仅当v被c整除时有h(v)=v/c w,其它函数值均为0。如果它是多重背包中重复次数最多为n的物品,那么它对应的泛化物品的函数有h(v)=v/cw仅当v被c整除且v/c<=n,其它情况函数值均为0。
一个物品组可以看作一个泛化物品h。对于一个0..V中的v,若物品组中不存在费用为v的的物品,则h(v)=0,否则h(v)为所有费用为v的物品的最大价值。P07中每个主件及其附件集合等价于一个物品组,自然也可看作一个泛化物品。 泛化物品的和