动态规划(DP)的学习总结
即今江海一归客,他日云霄万里人。 ----------高适。

目录
动态规划( Dynamic Programming, DP ), 是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法 。
说白了,其实就是把原问题拆开,分出许多个子问题,将子问题一层层叠加(看情况,有时是不能叠加的),从而找到原问题答案。(感觉还不如不解释(/▽\)~)
总而言之,DP是一种很重要的算法,在其他各种算法(如:某些图论题,线段树,并查集,DFS,数论,概率DP等等)中也有很重要的作用,所以可以说DP是一种最基础的算法,处于核心的地位。
不过DP学习起来也是不容易的,凭博主的感觉来说,呃·······有点抽象,有些时候你甚至不知道为什么这样写能对(没错,这就是菜狗博主的感受,看到大佬们的题解的时候总会感叹大佬们的智商强的可怕 (/// ̄皿 ̄)○~)
但是,博主虽然受到了因不会DP所以被DP题折磨而看到题解后又觉得不过如此而怀疑自己的巨大打击,仍然没有放弃,甚至愈挫愈勇(啊嘞?怎么感觉自己这么像抖M?0.0,终于还是被调教成了吗?),最后邪恶的魔龙DP最终还是被平凡的勇者打败 ヾ(≧▽≦*)o。
最后,因为学习算法是一条充满各种困难的道路,所以博主真挚地希望大家都能坚持下去,成为心中幻想者的勇者,击败一路上的所有BOSS,就像开头的诗写的那样,不忘初心的前进吧!
一、前置内容:贪心+记忆化搜索
贪心是很重要的算法(真的很重要!!!),但是记忆化搜索的话,个人觉得其实没那么重要,因为有位大佬曾说过“ 一切记忆化搜索都可以转化为动态规划,但不是所有的动态规划都能转化为记忆化搜索 ”,所以这里不会详讲。
贪心( greedy algorithm )
具体请点击这里查看wiki解释。
贪心算法 :是用计算机来模拟一个「贪心」的人做出决策的过程。这个人十分贪婪,每一步行动总是按某种指标选取最优的操作。而且他目光短浅,总是只看眼前,并不考虑以后可能造成的影响。
一般会搭配排序一起用,而排序内容会根据题目的内容决定,比如:根据价格,性价比等由大到小等等。
以下是一道贪心例题:
摘自: 今年暑假不AC - HDU 2037 - Virtual Judge
对于这题,我们可以试着代入一下,如果是你的话要看那种节目才能看到最多的完整节目呢?节目时间最短吗?
举个栗子:
4 1 2 //节目一 2 3 //节目二 2 4 //节目三 3 5 //节目四
首先看节目一,之后我们有两个选择,节目二或节目三,如果选择看节目二,那么刚好能看到节目四,二如果选择看节目三,则不能看到节目四。也就是说,我们要先把当前看的节目看完才能看后边的节目,而为了能看后边更多的节目,就要先看最早结束的。
所以这题的贪心想法就是看结束时间最早的,我们可以定义一个pair数组或者结构用来存储每个节目的开始时间和结束时间,然后按结束时间升序排序,再通过遍历比较后边节目的开始时间与当前节目的结束时间,如果开始时间再结束时间之后,则+1即可,代码如下:
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#include<vector>
using namespace std;
#define ll long long
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
#define mod 1e9+7
const int N=5001;
const int INF=0x3f3f3f3f;
int T;
bool cmp(const P& a,const P& b){
return a.second<b.second;
}
void sloved(){
vector<P> t(T);
for(int i=0;i<T;i++){
cin>>t[i].first>>t[i].second;
}
sort(t.begin(),t.end(),cmp);
int cnt=0,te;
for(int i=0;i<T;i++){
if(i==0){
cnt++;
te=t[i].second;
}else{
if(t[i].first>=te){
cnt++;
te=t[i].second;
}
}
}
cout<<cnt<<endl;
}
signed main(){
R;
//cin>>T;
//T=1;
while(cin>>T,T){
sloved();
}
}
记忆化搜索(mfs)
一说到搜索,就不得不扯到两种搜索算法,深度优先搜索(DFS)和广度优先搜索(BFS),这两种算法都是有模板的学习起来比较简单,大家可以点击这里自行查看学习。
那么你发现了吗?所谓记忆化搜索与上述两种搜索不同的地方?没错,就是你想的那样,多了一个前缀“记忆化”,那么什么叫记忆化呢?词如其名,就是会记忆(雾),其实就是在搜索的基础上多了个可以记忆,当你理解了所谓的“记忆”,那么你也就能理解动态规划的思想了。
先给各位端上一道mfs题吧:
P1434 [SHOI2002] 滑雪
题目描述
Michael 喜欢滑雪。这并不奇怪,因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael 想知道在一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:
1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9
一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度会减小。在上面的例子中,一条可行的滑坡为 (从 开始,在 结束)。当然 ------ 更长。事实上,这是最长的一条。
输入格式
输入的第一行为表示区域的二维数组的行数 和列数 。下面是 行,每行有 个数,代表高度(两个数字之间用 个空格间隔)。
输出格式
输出区域中最长滑坡的长度。
输入输出样例 #1
输入 #1
5 5 1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9
输出 #1
25
说明/提示
对于 的数据,。
当你学过两种搜索之后,这题的思路就是DFS找到最大值输出,但是这个题的起始点并不明确,每个点都可能是起始点,所以我们要将每个点都搜索,这样不可避免的就会搜索到重复的点,从而增加时间复杂度,为了不再搜索重复的点,我们需要通过存储记忆一下每个点的值。
参考:题解 P1434 【[SHOI2002]滑雪】 - 洛谷专栏
首先,我们定义s[i][j]为从点( i , j )出发走的最大距离,然后每次DFS记忆一次就ok了。
通过做过 马的遍历 - 洛谷 P1443 - Virtual Judge 这道题,我们知道怎么判断下一步应该走哪,没做过也没关系,接下来解释一下。明白之后大家也可以尝试做一做这道题,算是一道非常简单的模板题。
每个点有四个方向,上,下,左,右,故我们定义一个方向数组go[4][2]
int go[4][2]={{0,1},{0,-1},{-1,0},{1,0}}//分别代表上,下,左,右
for(int i=0;i<4;i++){
int xx=x+go[i][0];//下一点的横坐标
int yy=y+go[i][1];//下一点的纵坐标
}
然后判断是否在地图内:
if(xx>=1&&xx<=R&&yy>=1&&yy<=C)
以及判断可以走的条件:
int a[N][N]//a[x][y]表示点(x,y)为起始点能走的最大距离
if(a[xx][yy]<a[x][y])//从高往低走
我们知道DFS是通过递归实现的,所以只需分别向四个方向搜索取最大值记录即可。‘
f[x][y]=max(f[x][y],dfs(xx,yy)+1);
这样我们就存储了已经搜索过的点的最大距离了qwq。
当然最后别忘了起始点也算所以答案要+1。
代码如下:
#include <bits/stdc++.h>
using namespace std;
//-------------------------------------------------------------------------------------------
#define int long long
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
const int mod=9982443653;
const int N=110;
const int INF=0x3f3f3f3f;
const int inf=0xcf;
//--------------------------------------------------------------------------------------
int r,c;
int a[N][N],f[N][N];
int go[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
int ans;
int mfs(int x,int y){
if(f[x][y]>0) return f[x][y];
for(int i=0;i<4;i++){
int xx=x+go[i][0],yy=y+go[i][1];
if(xx<1||xx>r||yy<1||yy>c) continue;
if(a[xx][yy]>a[x][y]){
f[x][y]=max(f[x][y],mfs(xx,yy)+1);
}
}
return f[x][y];
}
void sloved(){
cin>>r>>c;
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++){
ans=max(ans,mfs(i,j));
}
}
cout<<ans+1<<endl;
}
signed main(){
R;
int T;
//cin>>T;
T=1;
for(int i=1;i<=T;i++){
sloved();
}
return 0;
}
那么再解释一下动态规划思想吧:
和记忆化搜索很像,DP同样需要记忆每个点的状态,和记忆化搜索不同的是,动态规划并需要递归,而是通过每个点的遍历以及状态的转移,或者说叠加,以获得目标值,但是因为动态规划无后效性( 过去的事情不会影响未来,只关注当前的状态 ),所以需要考虑后效性的问题。
滑雪这道题也是可以用DP解法的,学完DP后大家可以自行尝试一下。
二、背包问题
背包DP分为两种,分别是0-1背包和完全背包。
我们在例题中解释两者的区别。先看以下例题,摘自:P1048 [NOIP 2005 普及组] 采药 - 洛谷
我们知道,0和1在代码中分别表示假和真,那么在0-1背包中也就代表对于某件商品的选或不选。
我们看一下原题,从题中再解释01背包的含义:
P1048 [NOIP 2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 个整数 ()和 (),用一个空格隔开, 代表总共能够用来采药的时间, 代表山洞里的草药的数目。
接下来的 行每行包括两个在 到 之间(包括 和 )的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
输入输出样例 #1
输入 #1
70 3 71 100 69 1 1 2
输出 #1
3
说明/提示
【数据范围】
-
对于 的数据,;
-
对于全部的数据,。
【题目来源】
NOIP 2005 普及组第三题
如果刚刚对于接触DP的同学来说,一看到这题首先能想到的可能是贪心按性价比排序,但是对于这道题贪心只会过一部分数据,那么就要考虑dp了。
想要在总时间T内采到最大价值,我们要考虑遍历这些草药的时间与价值,当然肯定需要先存储,然后记录采到每个物品时前已经采取到的最大价值,我们叫它此时的状态。
const int N=1000+10;
int T,M;
int t[N],v[N];
int dp[N][N];
为了记录,我们定义了一个二维数组dp[i][j],表示当采到第i个草药时,时间到此时的采到的最大价值,(注意,i代表的是第几个草药,j表示的是时间),假设采到第i个草药时(我们默认剩下的时间足够采第i个草药),如果我们不采,此时的价值为dp[i-1][j],如果我们采(也就是放入背包),那此时价值就是dp[i-1][j-t[i]]+v[i],也就是,我们的时间j要减掉采第i个草药的时间,同时价值加上第i个草药的价值,因为我们想要最大价值,所以接下来比较采与不采第i个草药的价值,取最大的价值,也就是dp[i][j]=max(dp[i][j],dp[i-1][j-t[i]]+v[i]),这就是这道题的状态转移方程(也叫递推式)。
一般来讲,我们的j都是将总容量(或时间等统称为容量)从大到小遍历的,需要消耗多少就减去多少。
这样我们就可以写出时间复杂度为O(T*M)的代码啦ヾ(≧▽≦*)o
但是如果遇到很大的T和M的数据,二维数组会很占空间,导致MLE,因此,我们不得不考虑怎么把空间复杂度降下来,也就是怎么把二维数组降维,通过观察递推式,我们可以注意到,欸?dp[i][j]好像只与dp[i-1][j]有关诶?没错!就是这样,也就是说我们只需要第i个草药以及第i-1个草药的状态就可以了,那么也就是说我们根本不需要i-1之前的状态,那么我们就可以直接取消第一维i的记录了,这就叫做滚动数组。
以下是这道题的代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
#define mod 1e9+7
const int N=1001;
const int INF=0x3f3f3f3f;
int T,M,v[N],t[N];
int dp[N];
void sloved(){
cin>>T>>M;
for(int i=1;i<=M;i++){
cin>>t[i]>>v[i];
}
for(int i=1;i<=M;i++){
for(int j=T;j>=t[i];j--){
dp[j]=max(dp[j],v[i]+dp[j-t[i]]);
}
}
cout<<dp[T]<<endl;
}
signed main(){
R;
int T;
//cin>>T;
T=1;
for(int i=1;i<=T;i++){
sloved();
}
return 0;
}
以上就是01背包问题的解释,接下来我们看完全背包:
题目摘自:P1616 疯狂的采药 - 洛谷
P1616 疯狂的采药
题目背景
此题为纪念 LiYuxiang 而生。
题目描述
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
. 每种草药可以无限制地疯狂采摘。
. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 和代表山洞里的草药的数目 。
第 到第 行,每行两个整数,第 行的整数 分别表示采摘第 种草药的时间和该草药的价值。
输出格式
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
输入输出样例 #1
输入 #1
70 3 71 100 69 1 1 2
输出 #1
140
说明/提示
数据规模与约定
-
对于 的数据,保证 。
-
对于 的数据,保证 ,,且 ,。
这道题与上道题的区别在于每种草药可以无限制地疯狂采摘,也就是并非 取或不取 了,而是 可以取0件,1件,2件等无限制了。
对于这道题我们的状态转移方程与上道一样,与答案只在于怎么可以无限制地采药。
我们先回想一下上道题是怎么实现只取一个的?
我们通过对容量的逆序遍历,通过与上衣状态比较来决定取或不取,也就是取的话也仅仅取一个,(唉~博主解释能力水平有限,请诸位自行移至大佬题解的解释:题解 P1616 【疯狂的采药】 - 洛谷专栏)。
那么我们想知道一个物品要取几件,只需要所有满足条件的容量比较并记录就好了,也就是,我们可以正序遍历容量,参考代码:
for(int i=1;i<=M;i++){
for(int j=1;j<=M;j++){
if(j>=v[i]) dp[j]=max(dp[j],v[i]+dp[j-t[i]]);
}
}
综上,就是背包问题的解释了。
本篇参考:
题解 P1434 【[SHOI2002]滑雪】 - 洛谷专栏
学习算法的路很难坚持,会受各种东西的影响而半路退缩,博主希望大家都能不忘初心,一路前行,坚持下去!
感谢陪伴,与君共勉。
感谢各位观看喵~
2148

被折叠的 条评论
为什么被折叠?



