动态规划
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果(以上引自百度百科)
那么简单的来说动态规划实际就是要把握状态与状态之间的关系,状态转移的一种思想,那么在dp的学习中都需要注意哪些问题呢
- 1.理解状态和状态转移方程(其中对于状态方程的把握需要技巧)
- 2.理解最优子结构与重叠子结构
- 3.熟练运用递推法与记忆化搜索求解数字三角形问题
- 4.熟悉DAG上动态规划的常见思路,与两种状态定义方法与刷表法
- 5.掌握记忆化搜索在实现中所要注意的地方
- 6.掌握记忆化搜索和递堆中输出方案的方法
- 7.掌握递推中滚动数组的使用方法
- 8.熟练解决经典dp问题(背包问题,数字三角形,多阶段决策问题等)
解决dp问题的思想
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式
常见的dp模型
- 背包
- 线性dp
- 区间dp
- 计数类dp
- 数位统计dp
- 状态压缩dp
- 树状dp
解决dp问题的一般思路
背包问题
贴一个大佬的视频
https://www.bilibili.com/video/BV1qt411Z7nE?from=search&seid=16049643527401494369
基本上覆盖了所有的背包问题
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?
这里需要记住一些公式
0-1背包问题
最大化
受限于
基本上不同的背包问题可通过不同的变化转化为 0-1背包
那么先来看0-1背包
0-1背包问题
我们有n种物品,物品j的重量为wj,价格为pj。
我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W。
如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题(引自百度百科)
01 背包问题
用这道题的给出的样例来理解01背包问题的话就是现在有一个背包他所能容纳物品的体积为5,现在有四件宝物其属性如图,且每个宝物只有唯一的一个,现在要找到一个组合方案使所拿宝物的总价值最大根据样例可以看出刺激2,3件宝物组合时价值最大
考虑方法
背包问题主要在于状态的计算即f(i,j),我们可以把f(i,j)的状态分成两种情况
第一种:不含 第i个物品,那么此时最大的情况就为f(i-1,j)
第二种:包含第i个物品,那么此时我们可以考虑不论加不加上i 最大和最小的个个状态仍不变,即最大的仍为最大,那么我们在所有的选法中去掉i,而此时我们能选的物品体积便会变成j-vi(vi一定被选上),所以便有了此时的最大值为f(i-1,j-vi)+wi。所以最终的最大值就是两种情况的最大值再取一次最大值
那我们现在来看看这道题的代码的写法
#include <bits/stdc++.h>
//0-1背包二维写法
using namespace std;
const int N = 1010;
int n;//物品数量
int m;//背包能承受的总体积;
int v[N];//商品体积
int w[N];//商品价值
int f[N][N];//状态
int main(){
cin>>n>>m;
for(int i =1; i <= n; i++)
cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)//不从零开始是因为从前零件里选不了物品,相当于一件都不选,价值为零
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if(j >= v[i])
f[i][j] = max(f[i][j],f[i - 1][j - v[i]] + w[i]);
}
cout<<f[n][m]<<endl;
return 0;
}
那么是不是二维有点太复杂了呢,我也觉得,但我又不太知道怎么优化f[n]成为一维数组,那么我们删掉二维中的一维看看情况
#include <bits/stdc++.h>
//0-1背包二维写法
using namespace std;
const int N = 1010;
int n;//物品数量
int m;//背包能承受的总体积;
int v[N];//商品体积
int w[N];//商品价值
int f[N];//状态
int main(){
cin>>n>>m;
for(int i =1; i <= n; i++)
cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)//不从零开始是因为从前零件里选不了物品,相当于一件都不选,价值为零
for(int j = 0; j <= m; j++)
{
f[j] = f[j];//变成了恒等式,可以删去
if(j >= v[i])//而这个判断条件可以和循环合并,即j在v[i]之后才有意义for(int j = v[i]; j <= m; j++)
f[j] = max(f[j],f[j - v[i]] + w[i]);//问题是这里和原来的状态相同吗
}
cout<<f[n][m]<<endl;
return 0;
根据图中的问题我们来分析一下 我们原本的代码为f[i][j] = max(f[i][j],f[i - 1][j - v[i]] + w[i]); 而这里我们去掉了i-1后实际上计算的是第i层的状态实际上是不等价的,实际上此时我们是从小到大迭代j,
因为j-v[i]严格小于j,所以j-v[i]会被先算,即改动后的j-v[i]实际是第i-1层的j-v[i],此时的i-1已经被更新,我们算的实际机上是第i层的j-v[i],其实最主要的点在于f[j]所对应的一定是原本二维里的f[i][j],而此时f[j-v[i]]小于j,所以这里既然小的会被更新,那我们就把j循环逆着进行,即从大到小;
#include <bits/stdc++.h>
//0-1背包二维写法
using namespace std;
const int N = 1010;
int n;//物品数量
int m;//背包能承受的总体积;
int v[N];//商品体积
int w[N];//商品价值
int f[N];//状态
int main(){
cin>>n>>m;
for(int i =1; i <= n; i++)
cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)//不从零开始是因为从前零件里选不了物品,相当于一件都不选,价值为零
for(int j = m; j >= v[i]; j--)
{
f[j] = max(f[j],f[j - v[i]] + w[i]);
}
cout<<f[m]<<endl;
return 0;
}
那么这便是0-1背包最终的优化代码 。真的是太不容易了
大家要是不太理解二维变一维的过程可以评论。
也可以直接私信我。
有什么问题也请大家多多指正,我及时修改。
感谢各位大佬
完全背包问题
完全背包问题与0-1背包十分类似,但n个物品可以重复的的去选择,如下图所示,每个分割的小块代表的是第i个物品选择的次数
他的考虑方法也与0-1背包类似
那么我们很轻松就可以推出状态方程为
那话不多说我们先看看题目
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 0; k * v[i] <= j; k++)
f[i][j] = max(f[i][j],f[i - 1][j - v[i] * k] + k * w[i]);
cout<<f[n][m]<<endl;
return 0;
}
但当我信心满满提交的时候发现超时了
那么有没有别的考虑方法呢
没有
是不可能的
我们先来从最原始出发
那么f[i][j-v[i]]等于什么呢
我们可以把j替换成j-v[i];便可以得到
很像高中时候的错位相减消元;
下面的每一项都比原来的每一项多w
那么原来的第二个参数便可以改成f[i,j-v]+w;
所以我们便可以得到这样的递推公式
那么我么就可以写出来了
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;
int f[N][N],w[N],v[N];
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if(j>=v[i]) f[i][j] = max(f[i][j],f[i][j - v[i]] + w[i]);
}
cout<<f[n][m]<<endl;
return 0;
}
那么现在时间复杂度就明显降了一级,就不会tle了,那么我们的完全背包就做出来了!!!
那么问题来了可以优化吗?能把二维吧变为一维吗?
那么我们用之前的方法暴力去掉一维试试看
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;
int f[N],w[N],v[N];
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
f[j] = f[j];//等式右边先进行运算,那么右边的j实际上是i-1层的j 与二维等式等价所以可以删去
if(j>=v[i]) f[j] = max(f[j],f[j - v[i]] + w[i]);//这里j - v[i]小于j 所以一定先算那么这个j - v[i]实际上就是第i层的j - v[i],因为j - v[i]比j小,且先算那么第i层在这之前一定未算过j - v[i],与二维情况等价所以也可以直接删去,而判断条件,则可以直接放入循环的初始条件中;
}
cout<<f[n][m]<<endl;
return 0;
}
根据上面代码块的分析,我们便可以得到优化后的代码
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;
int f[N],w[N],v[N];
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= m; j++)
{
f[j] = max(f[j],f[j - v[i]] + w[i]);
}
cout<<f[m]<<endl;
return 0;
}
那么我们优化后的完全背包也终于解决了。真不容易啊
其他的背包问题会放在后续的笔记中更新
绝不是因为懒