思想
名词解释:通过一个例子来解释
A : "1+1+1+1+1+1+1+1 =?"
A : "上面等式的值是多少"
B : 计算 "8!"
A : 在上面等式的左边写上 "1+"
A : "此时等式的值为多少"
B : "9!" (quickly)
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"
例如斐波拉契数列Fibonacci
Fibonacci (n) = 1; n = 0
Fibonacci (n) = 1; n = 1
Fibonacci (n) = Fibonacci(n-1) + Fibonacci(n-2)
public int fib(int n)
{
if(n<=0)
return 0;
if(n==1)
return 1;
return fib(n-1)+fib(n-2);
}
这是常用的递归法,但是我们发现很多fib()会重复执行,而我们可以算出来一个fib()后把它存到一个数组中再次用到的时候直接根据数组取值,这就是动态规划
经典例题
🚩1. 01背包问题(物品个数为1)
有N个物品,其重量分别为weight[i],其价值分别为value[i],现有一个容量为V的背包,问怎样装商品才能使得背包中的价值最大?
int N=5,V=10;
int weight[]={0,3,6,3,8,6}; //方便统计从1开始
int value[]={0,4,6,6,12,10};
可先看B站这个视频讲解:https://www.bilibili.com/video/BV1K4411X766/
其实就是相当于我们创建一个表格即dp[][],左边那一列表示商品编号,上面那一行表示背包容量,表格内容是此时状态的最大价值
先看结果,如下
解释:
一行一行的填写,从上至下从左至右。如编号1重量为3,那么容量为0、1、2时都不能放下,最大价值都为0
当背包重量为3时可以放下商品1所以最大价值为4
再考虑第二行,j等于02时都放不下最大价值为0,由于商品2重量为6,那么背包容量为35时都放不下商品2只能放商品1最大价值为商品1的价值4。
而当背包容量为6时,此时能够放下商品2,那么就要考虑要不要放商品2
1️⃣放商品2,那么最大价值就是此时背包的容量减去商品2的重量为0,再看第一行重量为0时的价值也是0,那么最大价值为6
2️⃣不放商品2,那么最大价值就是第一行背包容量为6时的最大价值为4
放商品2最大价值为6,不放最大价值为4,我们选最大值为6
总结如下:
若背包容量小于商品n的重量放不下商品
- 那么前n个商品的最大价值和前n-1个商品的最大价值是一样的(既然不放相当于就不考虑这个商品,那么考虑前n个商品其实就是考虑前n-1个商品)
若此时背包装得下商品n
- 若选择放商品n,那么背包可用容量KV就等于背包总容量SV减去商品n的重量nV,那么问题就变成了只考虑前n-1个商品且背包容量为KV时的最大价值,而此前我们已经求得了这个值填写在了表中所以直接用这个值加上商品n的价值就是最大价值
- 若不放商品n,那么前n个商品的最大价值和前n-1个商品的最大价值是一样的
- 从以上两个选择选一个最大的就是最终的最大价值
对应的代码如下
for (int n = 1; n <= N; n++) //遍历商品1~N
{
for (int v = 0; v <= V; v++) //遍历容量0~V
{
if(v>=weight[n]) //背包容量大于等于商品n的容量时
{
dp[n][v]=max(dp[n-1][v-weight[n]]+value[n],dp[n-1][v]); //选择放商品n和不放商品n哪个价值最大
}
else //背包容量小于商品n的容量时
{
dp[n][v]=dp[n-1][v]; //此时商品n一定不能放,此时最大价值就是商品1到i-1中,容量为v时的最大价值
}
}
}
而其实我们也可以用一维数组优化下代码,代码如下
for(int n=1;n<=N;n++)
for(int v=V;v>=w[i];v--)
dp[v]=max(dp[v],dp[v-weight[n]]+value[n]);
而我们怎么知道求得的最大价值其中我们选了哪些商品呢
其实就是从表的右下角开始回溯,如果dp[n][j]和dp[n-1][j]值是一样的,那么我们肯定没装商品n(相当于我们就没考虑商品n)。事实上,我们推dp[n][j]时如果选择不放商品n那么我们就让dp[n][j]=dp[n-1][j]
所以相当于我们根据两个数值是否相等来判断当时我们是否选了商品n
相当于是求最大价值的一个逆过程
如果装了商品n,那么用总容量减去商品n的容量得到可用容量kV再来判断是否放了商品n-1,其实也就是看dp[n-1][kv]和dp[n-2][kv]是否相等,以此类推
int n=N,v=V;
while(n!=0)
{
if(dp[n][v]==dp[n-1][v])
{
cout<<"商品"<<n<<"未被选中"<<endl;
}
else
{
cout<<"商品"<<n<<"被选中"<<endl;
v=v-weight[n];
}
n--;
}
🦄完整代码如下
#include<bits/stdc++.h>
using namespace std;
int dp[15][15];
int N=5,V=10;
int weight[]={0,3,6,3,8,6};
int value[]={0,4,6,6,12,10};
//打印初始状态
void printInit()
{
cout<<"共"<<N<<"个商品,"<<"背包容量为"<<V<<endl;
for (int i = 1; i <= N; i++)
{
cout<<"商品"<<i<<"的重量为"<<weight[i]<<",价值为"<<value[i]<<endl;
}
}
//求最大价值
int maxValue()
{
for (int n = 1; n <= N; n++) //遍历商品1~N
{
for (int v = 0; v <= V; v++) //遍历容量0~V
{
if(v>=weight[n]) //背包容量大于等于商品n的容量时
{
dp[n][v]=max(dp[n-1][v-weight[n]]+value[n],dp[n-1][v]); //选择放商品n和不放商品n哪个价值最大
}
else //背包容量小于商品i的容量时
{
dp[n][v]=dp[n-1][v]; //此时商品n一定不能放,此时最大价值就是商品1到n-1中,容量为v时的最大价值
}
}
}
return dp[N][V];
}
//打印最大价值的商品选择情况
void printSelect()
{
int n=N,v=V;
while(n!=0)
{
if(dp[n][v]==dp[n-1][v])
{
cout<<"商品"<<n<<"未被选中"<<endl;
}
else
{
cout<<"商品"<<n<<"被选中"<<endl;
v=v-weight[n];
}
n--;
}
}
int main()
{
printInit();
cout<<"最大价值为:"<<maxValue()<<endl;
printSelect();
return 0;
}
🚩2.多重背包问题(物品个数有限)
在01背包的基础上每个商品可以有多个
#include<bits/stdc++.h>
using namespace std;
int dp[21][21];
int value[]={0,2,3,2,3,4};
int weight[]={0,1,5,5,4,3};
int number[]={0,2,3,1,2,8}; //每个商品的个数
int N=5,V=10;
int main(){
for (int i = 1; i <= N; i++) //对于每个物品
{
for (int j = 0; j <= V; j++) //对于每个容量
{
for (int k = 0; k <= number[i]; k++) //放几个
{
if(j>=weight[i]*k)
{
dp[i][j]=max(dp[i-1][j-weight[i]*k]+value[i]*k , dp[i][j]);
}
}
}
}
cout<<dp[N][V]<<endl;
return 0;
}
🚩3. 完全背包问题(物品个数无限)
在01背包的基础上每个商品的数量是无限的
#include<bits/stdc++.h>
using namespace std;
int dp[21][21];
int value[]={0,2,3,2,3,4};
int weight[]={0,1,5,5,4,3};
int N=5,V=10;
int main(){
for (int i = 1; i <= N; i++) //对于每个商品
{
for (int j = 0; j <= V; j++) //对于背包容量
{
if(j>=weight[i])
{
dp[i][j]=max(dp[i][j-weight[i]]+value[i],dp[i-1][j]); //和01背包相比就这句不同
//dp[i][j]=max(dp[i-1][j-weight[i]]+value[i],dp[i-1][j]); 01背包
}
else
{
dp[i][j]=dp[i-1][j];
}
}
}
cout<<dp[N][V]<<endl;
return 0;
}