其他分享
首页 > 其他分享> > 动态规划入门(一)

动态规划入门(一)

作者:互联网

动态规划是很多人的烦恼,尤其是竞赛党(OIer),因为动态规划既是重点,又是难点,让很多人头痛不已。
看看这张表在这里插入图片描述

由此可见,动态规划在NOIP中占到了不小的一部分,并且难度比较大。
那么,我们来一点一点地学习动态规划,先从入门开始。

动态规划入门

动态规划(dp)是研究多步决策过程最优化问题的一种数学方法。在动态规划中,为了寻找一个问题的最优解(即最优决策过程),将整个问题划分成若干个相应的阶段,并在每个阶段都根据先前所作出的决策作出当前阶段最优决策,进而得出整个问题的最优解。即记住已知问题的答案,在已知的答案的基础上解决未知的问题
动态规划在时间复杂度上虽略差于贪心,但结果比贪心准确。
动态规划有两种实现方法,一种是递推,另一种是记忆化搜索。两种方法时间复杂度完全相同,但是,递推的效率要比记忆化搜索高不少,而且以后的大量优化技巧都建立在递推上(滚动数组、单调队列、斜率优化……)。所以,我们一般用递推来写动态规划。

其实,动态规划,说白了,就是高级一点的递推

能采用动态规划求解的问题的一般要具有3个性质:
1、最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
2、无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
3、有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

解题步骤:
1、拆分问题;
2、定义状态并找出初状态;
3、状态转移方程(这一条是不是很像递推)。

例题

通过三道题来学习一下

1.斐波纳契数列

没错,又是它!斐波纳契数列,让你求第n项。
有人会问:这不是递归的经典例题吗?跟动态规划有什么关系?
确实,这是一道经典的递归例题。

int dfs(int n)
{
	if(n<=2) return 1;
	else return dfs(n-2)+dfs(n-1);
}

是不是so easy!
如果我说:禁用递归!怎么办?
一样很简单,用个数组就完事:

dp[0]=dp[1]=0;
for(int i=2;i<=n;i++)
	dp[i]=dp[i-1]+dp[i-2];
cout<<dp[n];

更简单!
我为什么要举这么简单,又似乎跟动态规划没关系的例子呢?
似乎没关系……
回想动态规划的基本思想
斐波纳契数列求第n项,是不是要先求第n-1和第n-2项?求第n-1项是不是要求第n-2和第n-3项……以此类推,便从第0项和第1项开始,慢慢往后推。那不就是好动态规划一样吗?再强调一下动态规划的基本思想:记住已知问题的答案,在已知的答案的基础上解决未知的问题
结合这道题,仔细体会,是否有点悟了呢?
在这道题中,第0项和第1项就是已知问题的答案,而第n项则是未知的问题。

再看一道题:

2.数字三角形

题目描述

观察下面的数字金字塔。

       7 
    3   8 
     8   1   0 
  2   7   4   4 
   4   5   2   6   5

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。
每一步可以走到左下方的点也可以到达右下方的点。
输入格式
第一个行包含 R(1<= R<=1000) ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
所有的被供应的整数是非负的且不大于100。
输出格式
单独的一行,包含那个可能得到的最大的和。
样例数据
input

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

output

30

样例解释
在上面的样例中,从7 到 3 到 8 到 7 到 5 的路径产生了最大,和为7+3+8+7+5=30。
分析
从上面的数据来看,贪心肯定是不行的。那么,这道题怎么写呢?
递推
别忘了,动态规划就相当于高级的递推。
把这个三角形一行一行地划分为多个阶段。令f(i,j) 为第i行第j列上的点到最后一行的最大和。

用逆推即可。状态转移方程为:f[i][j]=max(f[i+1][j],f[i+1][j+1])+f[i][j]。结果为f[1][1]。

#include<bits/stdc++.h>
using namespace std;
int f[1010][1010];
int main()
{
	int r;
	cin>>r;
	for(int i=1;i<=r;i++)
		for(int j=1;j<=i;j++)
			cin>>f[i][j];
	for(int i=r-1;i>=1;i--)
		for(int j=1;j<=i+1;j++)
			f[i][j]+=max(f[i+1][j],f[i+1][j+1]);//递推式。核心句子
	cout<<f[1][1];
	return 0;
}

代码很短,也比较简单,但是你是否能完全理解?
思考:在这道题中,已知问题的答案和未知的问题分别是什么呢?
已知问题的答案:最后一层和倒数第二层的和
未知的问题:那个可能得到的最大的和

3.导弹拦截

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭,由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹的枚数和导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,每个数据之间有一个空格)。
计算这套系统最多能拦截多少导弹?
如果要拦截所有导弹最少要配备多少套这种导弹拦截系统?

输入格式
第一行,数字n表示n个导弹(n<=200)
第二行,n个数字,表示n个导弹的高度
输出格式
第一行,一个整数,表示最多能拦截的导弹数
第二行,一个整数,表示要拦截所有导弹最少要配备的系统数
样例数据
input

8
389  207  155  300  299  170  158  65

output

6
2

分析
第二问不多讲,用贪心即可:每颗导弹来袭时,使用能拦截这颗导弹的防御系统中上一次拦截导弹高度最低的那一套来拦截。若不存在符合这一条件的系统,则使用一套新系统。
下面我们来看看第一问:
题目第一问可以这样理解:给你一串数字,让你自选一个开始,然后 选择之后的数字,每次都选择不能大于上一次,求最多选择多少个数字。即求 “最长不上升子序列” 的长度
这也是一个经典的dp问题。
首先我们需要选择一个点,作为你要拦截的那串导弹的终点(为什么是找终点而不是找起点呢?可以通过“无后效性”分析得知。至于怎么分析的嘛……暂时不管,记住就行
用一个数组f[i]表示拦截到底i颗导弹时已经拦截几个了,如果第j个导弹比第i个导弹低,就说明前面肯定没有拦截到第j个。我们可以枚举1~i-1这些导弹,选择一个f数值最大的,把第i个导弹作为它的后缀,也就是说:
f[i]=max{f[j]}+1;1<=j<i;
这个就是状态转移方程

#include<bits/stdc++.h>
using namespace std;
int a[50000],f[50000];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
 	   cin>>a[i];
	int ans=0;
    for(int i=1;i<=n;i++)
    {
        f[i]=1;
        for(int j=1;j<i;j++)
            if(a[i]<=a[j]) 
				f[i]=max(f[i],f[j]+1);
        ans=max(ans,f[i]);
    }
    cout<<ans<<endl;
    int sum=0,minn;
    for(int i=1;i<=n;i++)
    {
        minn=40000;
        if(a[i]!=-1)
        {
            sum++;
            minn=a[i];
            for(int j=i;j<=n;j++)
            {
                if(a[j]<=minn&&a[j]!=-1) 
                {
                	minn=a[j];
                	a[j]=-1;
                }
            }
        }
    }
    cout<<sum;
    return 0;
}

是不是感觉这道题的难度很大
你能思考出来:在这道题中,已知问题的答案和未知的问题又分别是什么呢?

总结

动态规划不是一时半会儿就能掌握的,刚才讲的只是最最最基础的东西,非常简单。动态规划的路有很长,毕竟它是三大算法之一。想精通动态规划不是一件简单的事,我们要一步一步、脚踏实地地深入学习,相信一定能突破这道难关。加油!!!在这里插入图片描述

标签:入门,int,导弹,问题,拦截,动态,规划
来源: https://blog.csdn.net/lijiayou_2026/article/details/117607272