算法-动态规划(01背包)
最近闲来无事,学学算法。
什么是动态规划,我们要如何描述它?
动态规划算法通常基于一个递推公式及一个或多个初始状态。当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。
这里所说的子问题并不一定指前一个问题的解
比如说1+2+3+。。。+100,我们这里可以sum[i]+100来求出总和,而sum[i]是1加到i的总和,这个问题就是1加到99的子问题再加上100就得到。
动态规划则并非是如此,它可能跟前面所有的子问题的某个解有联系,举个例子:
如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
(表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候)
首先我们思考一个问题,如何用最少的硬币凑够i元(i<11)?为什么要这么问呢?两个原因:1.当我们遇到一个大问题时,总是习惯把问题的 规模变小,这样便于分析讨论。 2.这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的,本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)。
好了,让我们从最小的i开始吧。当i=0,即我们需要多少个硬币来凑够0元。由于1,3,5都大于0,即没有比0小的币值,因此凑够0元我们最少需 要0个硬币。 (这个分析很傻是不是?别着急,这个思路有利于我们理清动态规划究竟在做些什么。) 这时候我们发现用一个标记来表示这句“凑够0元我们最少需要0个硬币。”会比较方便,如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。那么,我们用 d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0,表示凑够0元最小需要0个硬币。当i=1时,只有面值为1元的硬币可用, 因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的,即d(0)=0。所 以,d(1)=d(1-1)+1=d(0)+1=0+1=1。当i=2时,仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币,接下来我只需要再 凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到这里,你 都可能会觉得,好无聊,感觉像做小学生的题目似的。因为我们一直都只能操作面值为1的硬币!耐心点,让我们看看i=3时的情况。当i=3时,我们能用的硬 币就有两种了:1元的和3元的( 5元的仍然没用,因为你需要凑的数目是3元!5元太多了亲)。既然能用的硬币有两种,我就有两种方案。如果我拿了一个1元的硬币,我的目标就变为了:凑够 3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。这个方案说的是,我拿3个1元的硬币;第二种方案是我拿起一 个3元的硬币,我的目标就变成:凑够3-3=0元需要的最少硬币数量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 这个方案说的是,我拿1个3元的硬币,这里d(3)值跟d(2)是没有关系的,如果你还不理解,下面我们结合01背包来说明。好了,这两种方案哪种更优呢?记得我们可是要用最少的硬币数量来凑够3元的。所以,选择d(3)=1,怎么来的呢? 具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。
状态和状态转移方程
从以上的文字中,我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。
状态:就是上面d(3)d(2)等,抽象d(i);
状态转移方程:d(3)=min{d(3-1)+1, d(3-3)+1},抽象d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值;
01背包问题
问题描述:有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
状态转移方程:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的
伪码:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,
价值为f[i-1][v];
如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,
此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。
如果一开始看01背包这里最容易混淆,我就是在这里迷糊好久,要特别如果不明白要特别注意上面粗体的字,它是先放第i件,然后剩下容量再放前面i-1件,然而剩下容量所装的物品,我们前面都已求出,下面,让我们根据代码来理解;
Input输入的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),用一个空格隔开,T代表背包容量,M代表物品数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整 数,分别表示每个物品的体积和价值。
Output输出包括一行,这一行只包含一个整数,装入背包可使价值总和最大。
Sample Input
70 3
71 100
69 1
1 2
Sample Output
3
#include<iostream>
# include<cstring>
# define max(a,b) a>b?a:b
using namespace std;
int main()
{
int dp[101][1001],m,T,w[101],val[101],i,j;
cin>>T>>m;
for(i=1;i<=m;i++)
cin>>w[i]>>val[i];
memset(dp,0,sizeof(dp));
for(i=1;i<=m;i++)
{
for(j=0;j<=T;j++)//j相当于上面说的V-c[i],这里的循环都是为后面解准备的
{
if(j>=w[i])
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+val[i]);//放还是不放的选择
else dp[i][j]=dp[i-1][j];
cout<<dp[i][j]<<" ";
}
cout<<endl;
}
cout<<dp[m][T]<<endl;
return 0;
}
这里就测试一下,
10 4
5 10
8 16
5 5
10 10
如果要符合上面题目输出要求,把红色代码去掉