浅谈个人学DP的经历和感受
动态规划的定义!
首先,我们看一下官方定义:
定义:
动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
————————————————
版权声明:本文为CSDN博主「BS有前途」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ailaojie/article/details/83014821
不会也不要急,每个人刚学动态规划的时候都很懵逼!!!
我第一次接触动态规划大概是额,一个月之前了吧,当时老师完全给我讲懵了。然后课听完了脑子一片空白。
然后又请教@SXY大佬开小灶,总算讲明白了一点,但是我还是害怕呀!因为我根本就不熟,所以一个月之内基本没怎么做。
直到今天我觉得是时候解决这个问题了,于是我打开了(小破谷)伟大的洛谷,找到了动态规划题单,冒着满头大汉开始了征程……
个人的理解
上面第一段是动态规划的基本定义(废话),本蒟蒻的理解是动态规划分为两种。
第一种是递推(之前的博文已经讲过),
第二种是化解大问题为小问题,求小问题的最优解,以达到全局最优解。
与贪心不同的是,动态规划并不是求得每步的最优解就能达到全局最优解,而是从整体来看,选择取舍,这也直接导致他比贪心更加难以理解。(有种哲学意味?)
今天本蒟蒻主要谈谈第二种。
动态规划怎么做啊?
相信很多刚入c++这个大坑的同学都有这个疑问,该怎么“从大局考虑”呢?
我们先别急,看看定义。对于最简单、基础的动态规划(不是背包,背包本蒟蒻不会!),题目往往是问你:有一张图,每个点有一定的分值和通往其他点的路径,然后出题人会问你:怎么走可以使得总得分最多呢?
既然我们如果从头走的话,并不知道选择某条路径最后的得分情况,所以我们不能瞎选(贪心),那怎么办呢?
逆向思维!
如果我们不能从头走,那我们就从后往前走,从倒数第二个点开始枚举所有情况,求得该点之后所有路径的最优解,再枚举倒数第三个点的最优情况,把倒数第二个点的最优情况衔接上……
这样一直推到第一个点,就是最优解啦!!!
那么,用代码如何实现呢?
(敲黑板)
下面真的是重点了!!!(神犇、大佬请迅速撤离,以免窥探蒟蒻的世界!!!)
状态转移方程!!!
所谓状态转移方程(不是数学方程啊喂!)就是存储当前情况的状态的一个式子(废话)。
举个例子,有A、B、C、D四个城市,现在我们在A市,我去了C市然后又去了B市,最后到了D市。
那么我的状态就是A->C->B->D
在具体的动态规划中,我们用这个来存我们当前的最优解情况。
可能还没有听懂,没关系(因为我也不知道我BB了些啥),接下来我们上题目来实战演练!!!
实战演练!!!
我们先上一个简单的题!
ybt1281
一个数的序列bi,当b1<b2<...<bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1,a2,...,aN)我们可以得到一些上升的子序列(ai1,ai2,...,aiK)这里1≤i1<i2<...<iK≤N比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列。
如(1,7),(3,4,8)等等。这些子序列中最长的长度是4,比如子序列(1,3,5,8)。
你的任务,就是对于给定的序列,求出最长上升子序列的长度。
求上升、下降子序列的题目是很经典的动态规划,也是最基础的,我们就拿这个题开刀!
题目很好理解,先上本蒟蒻代码
//1281 #include<iostream> #include<algorithm> using namespace std; int QAQ[1001];//存储数列 int QWQ[1001];//状态数组!(核心) int main() { int n,maxn=0; cin>>n; for (int i=1;i<=n;i++) { cin>>QAQ[i]; QWQ[i]=1;//所有数的初始长度为1(即以该数为开头和结尾的序列,这也是本题的状态) } for (int i=2;i<=n;i++)//从第二个开始枚举 { for (int j=1;j<i;j++)//枚举到i,即在第i个数之前的所有数 { if(QAQ[i]>QAQ[j])//如果你枚举的第j个数比第i个数小,那么以j为末尾的序列可以长度+1,并以i为末尾 { QWQ[i]=max(QWQ[i],QWQ[j]+1);//看看是这个以这个数为结尾的子序列长,还是把它接到以QAQ[j]为结尾的子序列长,并取得最长的解,存储到这个数的QWQ(状态数组中)作为以这个数为结尾的最长子序列 }//简单来说,当我们枚举到某一个数的时候,就找到一个能接在这个数前面的最长子序列,然后把这个数接在这个最长序列的后面,这个数的状态变为以它为结尾的最长子序列 } } for (int i=1;i<=n;i++) { if (QWQ[i]>maxn) { maxn=QWQ[i];//找到状态数组所记录的最长子序列 } } cout<<maxn;//输出他 return 0; }
我把这种方法叫做“枚举屁股,找一个最大的头”。
(本蒟蒻建议如果实在理解不了,就画图操作,我就是需要画图找状态转移方程的)
下一题
ybt1260、洛谷1020(升级版)
这个题算是一个最最最最经典的动态规划和贪心结合的题了。
先上题目
【题目描述】
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
这个题和上面的大同小异,上面是求最长上升子序列,这个是求最长不下降子序列,所以只需要把上面的判定条件改成>,然后再写一个贪心就好了。
洛谷的题经过了改装,把数据范围扩大了,会TLE,所以需要二分查找,本蒟蒻不会,请各位大佬自行查阅题解。
上本蒟蒻代码
#include<iostream> #include<algorithm> using namespace std; int QAQ[101000]; int QWQ[101000]; int high[101000]; int main() { int i=0,maxn=0,m=0,flag=0,ans=1; while (cin>>QAQ[i]) { QWQ[i]=1; i++; } for (int y=1;y<i;y++) { for (int j=0;j<y;j++) { if (QAQ[j]>=QAQ[y]) { QWQ[y]=max(QWQ[j]+1,QWQ[y]); } } } for (int y=0;y<i;y++) { if (maxn<QWQ[y]) { maxn=QWQ[y]; } } cout<<maxn<<endl; for (int y=0;y<i;y++) { if (y==0) { high[y]=QAQ[y]; } for (int j=0;j<i;j++) { if (high[j]>=QAQ[y]) { high[j]=QAQ[y]; flag=1; break; } } if (flag==0) { ans++; high[ans]=QAQ[y]; } flag=0; } cout<<ans; return 0; }
没啥特点,改判断符号就行了。
抬走下一位!
洛谷P1216
题目描述
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在上面的样例中,从 7→3→8→7→5 的路径产生了最大
输入格式
第一个行一个正整数 r ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
我们还是故技重施,枚举屁股,找一个最大的
//P1216 #include<iostream> #include<algorithm> using namespace std; int QAQ[1010][1010];//初始化数组 int main() { int n; cin>>n; for (int i=1;i<=n;i++) { for (int j=1;j<=i;j++) { cin>>QAQ[i][j];//读入数据 } } for (int i=n-1;i>=1;i--) { for (int j=1;j<=i;j++) { QAQ[i][j]+=max(QAQ[i+1][j],QAQ[i+1][j+1]);//状态转移方程,即从倒数第二行开始枚举,看看他往最后一行的哪个数走得到一个较大值,把这个较大值更新为这个位置的数值 }//再枚举上一行,老方法,看看往下一行哪个数走得到较大值,更新这个位置的数值,由于这个题比较特殊(我也不知道特殊在哪)我没开状态数组,直接用的原数组存状态,但是安然无恙的AC了 } cout<<QAQ[1][1];//枚举到第一行,第一个数的值即为最大值 return 0; }
好了,重头戏终于来了,洛谷的一道黄题!(普及/提高-)
我今天花了一个半小时终于做出来了(本蒟蒻不容易啊)
洛谷P2196
题目描述
在一个地图上有N个地窖(N≤20),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。
输入格式
有若干行。
第1行只有一个数字,表示地窖的个数N。
第2行有N个数,分别表示每个地窖中的地雷个数。
第3行至第N+1行表示地窖之间的连接情况:
第3行有n-1个数(00或11),表示第一个地窖至第2个、第3个、…、第n个地窖有否路径连接。如第3行为1 1 0 0 0 … 0,则表示第1个地窖至第2个地窖有路径,至第3个地窖有路径,至第4个地窖、第5个、…、第n个地窖没有路径。
第4行有n-2个数,表示第二个地窖至第3个、第4个、…、第n个地窖有否路径连接。
… …
第n+1行有1个数,表示第n-1个地窖至第n个地窖有否路径连接。(为0表示没有路径,为1表示有路径)。
输出格式
有两行
第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。
第二行只有一个数,表示能挖到的最多地雷数。
不多BB,上代码!!!
//P2196 #include<iostream> #include<algorithm> using namespace std; int mapp[25][25]; int boom[25]; int QAQ[25]; int main() { int n,maxn=0,st,l=0,ans; cin>>n; for (int i=1;i<=n;i++) { cin>>boom[i]; QAQ[i]=boom[i];//初始化状态数组,即只在一个地窖中挖,能挖到的地雷数 } for (int i=1;i<n;i++) { for (int j=i+1;j<=n;j++) { cin>>mapp[i][j];//初始化小地图,记录路径的通断 } } for (int i=n;i>=1;i--)//从编号最大的地窖开始往前枚举 { for (int j=i-1;j>=1;j--) { if (mapp[j][i]==1)//因为是倒着枚举的,所以把ij反过来 { QAQ[j]=max(boom[j]+QAQ[i],QAQ[j]);//如果从第i个地窖过来,并挖完该地窖的地雷,可以得到比当前地窖状态(地雷数)更多的地雷,那么把这个地窖的状态更新为该地窖的地雷数+第i个地窖的地雷状态(第i个地窖之前的最优解) } } } for (int i=1;i<=n;i++) { if (QAQ[i]>maxn) { maxn=QAQ[i];//找到状态数组中最多的地雷数,输出他 st=i;//记录能挖到最多地雷的地窖,把他当做开头 } } cout<<st<<" ";输出开头 for (int i=st;i<n;i++)//从开头开始枚举,因为之前的每一步状态都是最优解,所以状态数组中存的地雷数应该最大(注意不是原来的地雷数组(bomb),不然会变成贪心!),我们找到这个最大值,然后输出这个最大值的地窖编号。 { for (int j=i;j<=n;j++) { if (mapp[i][j]==1&&QAQ[j]>l)//用l找出最大值 { l=QAQ[j]; ans=j;//ans即为答案 } } if (l!=0)//因为丧心病狂的出题人会给你出几个宝藏地窖,就是单独的地窖,不能走,里面几百颗雷,所以要特判,因为只有当检测到路径时,l不为0,所以没有路径时不满足if条件,避免了乱输出。 { l=0; i=ans-1;//第ans个地窖中有最多地雷,下一步从第ans个地窖向后枚举,因为回去的时候i++,所以这里-1 cout<<ans<<" ";//输出编号 } } cout<<endl<<maxn; return 0; }
这个题比较好理解的方法是(小声BB):你可以把动态数组里的数理解为选择这条路,后面最多还能挖到多少雷。这样会好理解,后面的路径也要好找一些。
在一本通上还有另外一个题和这个题大同小异,这个是@SXY大佬教我记录路径的时候用的例题,可惜我把他教记录路径的方法的都忘了,555……
ybt1262
【题目描述】
在一个地图上有nn个地窖(n≤200),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径,并规定路径都是单向的,且保证都是小序号地窖指向大序号地窖,也不存在可以从一个地窖出发经过若干地窖后又回到原来地窖的路径。
某人可以从任意一处开始挖地雷,然后沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使他能挖到最多的地雷。
【输入】
第一行:地窖的个数;
第二行:为依次每个地窖地雷的个数;
下面若干行:
xiyi //表示从xi可到yi,xi<yi。
最后一行为"00"表示结束。
不多BB,上大佬代码,由于太过高端,并且讲解年代久远,本蒟蒻无法给出注解,请各位大神自行理解。。。
//1262 #include<iostream> #include<algorithm> using namespace std; int f[201][2],a[201],maxn,l; bool mapp[201][201],flag; int main() { int n; cin>>n; int x,y; for (int i=1;i<=n;i++) { cin>>a[i]; f[i][0]=a[i]; } while(x!=0&&y!=0) { cin>>x>>y; mapp[x][y]=1; } for (int i=n-1;i>0;i--) { for (int j=n;j>i;j--) { if(mapp[i][j]==1&&a[i]+f[j][0]>f[i][0]) { f[i][0]=a[i]+f[j][0]; f[i][1]=j; } } } for (int i=1;i<=n;i++) { if(f[i][0]>maxn) { maxn=f[i][0]; l=i; } } while (l!=0) { if (flag==1) { cout<<"-"; } flag=1; cout<<l; l=f[l][1]; } cout<<endl<<maxn; return 0; }
额,今天的博客就水这么长吧,半夜了。本蒟蒻也要休息了。。。(话说我好像写了2个多小时???求各位看官的赞!)