这篇文章带你一次看懂动态规划,看完就做题

目录

前言

一、动态规划题目特点:

二、例题讲解

1.为什么是动态规划?

2.动态规划如何思考:

2.1第一步:确定状态:

        2.1.1最后一步:

         2.1.2子问题:

2.2第二步:转移方程:

2.3第三步:初始状态和边界情况:

             2.3.1初始状态:

            2.3.2边界情况:

2.3第四步:确定计算顺序:

3.样例的实现:

4.典例的实现:

总结



前言

        各位看官早上好中午好晚上好,今天为大家带来的是动态规划的入门级介绍,本文其实是我自己看了多篇讲解后自己做的总结笔记,用讲解的口吻复述,希望也能为你带来些许收获

期待ing

动态规划是很多朋友刚学习算法时比较先接触的一种算法。

那么什么是动态规划呢?

        《算法导论》是这样解释动态规划的:动态规划与分治法相似,都是通过组合子问题的解来求解原问题答案,将问题划分为互不相交的子问题,递归的求解子问题,最后合并子问题的答案,得到原问题的答案。

翻译成人话就是:计算并存储小问题的解,并将这些解组合成大问题的解。

一、动态规划题目特点:

学习动态规划我们需要明白:当题目写了什么时?我们要知道这个题可能需要用到动态规划了。

毕竟题目上一般不会说请你用动态规划做这题啥的。

   涉及的题干主要有三类:

   1、计数类,如:有多少种方式走到右下角?

          有多少种方法选出K个数使得合为SUM?

 2、求最大最小值,如:从左上角走到右下角路径的最大数字和

          求最长上升子序列长度

  3、求存在性,如:取石子游戏,先手是否必胜?

           能不能选出K个数使得合为SUM?

二、例题讲解

        我们直接用一道例题来为大家讲解这种算法

        

描述:

        给出不同面额的硬币以及一个总金额. 写一个方法来计算给出的总金额可以换取的最少的硬币数量. 如果已有硬币的任意组合均无法与总金额面额相等, 那么返回 -1.

        你可以假设每种硬币均有无数个
        总金额不会超过10000
        硬币的种类数不会超过500, 每种硬币的面额不会超过100

样例输入:
[2, 5, 7]
27
输出: 5
解释: 27 = 5 + 5 + 5 + 5 + 7

1.为什么是动态规划?

        那么在本题中,我们是如何判断出需要用到动态规划的呢?没错,就是  “ 最少 ” 

        为便于理解,我们用样例来讲解这一思想。

        样例输出的问题就是:

                你有三种硬币,面值分别为2元、5元和7元,每种硬币都足够多

                买一本书需要27元,如何用最少的硬币组合,正好不需要对方找钱。

2.动态规划如何思考:

        就三个确定:确定状态、确定转移方程、确定初始条件和边界情况

2.1第一步:确定状态:

        状态可以说是动态规划的核心,是动态规划能够解决问题的根本机理

        简单来说、我们在动态规划中都会开一个数组用来储存问题的解,假设这个数组是f[ ],状态就是数组中的每个元素f[ i ]代表的意义,这也类似与我们解应用题时候设的未知数x,y,z。

        那么怎么确定状态呢?确定状态其实就两件事:

        2.1.1最后一步:

        在本题中,虽然我们不知道最优策略到底用到了多少枚硬币、也不知道他到底是怎么做到的,假设最优策略为用了K枚硬币、那么我们就可以确定这K枚硬币:a1,a2…ak的面值加起来是27

        那么在最后一步,我们用到了最后一枚硬币ak

        除去这枚硬币,前面硬币的面值之和就为27-ak。

        示意如图:

        

        同时我们要知道,因为我们找到的是最优策略,所以拼出27-ak这么大的面值使用的硬币数一定是最少的,不然与最优矛盾。

         2.1.2子问题:

        所以我们要求解的问题就变成了:最少用多少枚硬币可以拼出27-ak。

        那么这个时候原问题的27就缩小为了27-ak,问题的规模缩小了,这样的问题我们称为子问题。

        这时状态就可以确定了:

                我们设状态f[x]=最少用多少枚硬币拼出x

        还有一个问题,最后一枚硬币是多少呢?

        这当然是我们已知的,在样例中,最后一枚硬币只可能是给出的2、5或7了。

        那么最后一枚硬币ak与最优解 f[x] 就只有这三种情况。

        

那么最有方案用到的最少硬币数

  f[27]=min\left \{ f[27-2]+1,f[27-5]+1,f[27-7]+1\right \}

   有的朋友看到这个方程就会觉得:这不是递归吗?那我拿递归不就做出来了,没什么大不了的啊.但是我们要想清楚了,如果用递归,我们实际上会将前面用到的某些数字多次计算,例如f[20],你可以想一想要算几次,再想想f[20]前面的那些数,很恐怖的好吧!

  那么这里就显示出我们动态规划算法的巧妙了:将计算过的结果保存下来,用到时直接计算即可。

2.2第二步:转移方程:

设状态f[x]=最少用多少枚硬币拼出x。

 则f[x]=min\left \{ f[x-2]+1,f[x-5]+1,f[x-7]+1\right \}

2.3第三步:初始状态和边界情况:

写出状态转移方程,并不是就已经万事大吉了

看着上面这个式子,我们还要思考两个问题:

     1、当x-2、x-5或者x-7小于0怎么办?数组可没有负数啊。

     2、总不可能一直算下去,什么时候停下来呢? 

2.3.1初始状态:

        初始状态就是我们用转移方程算不出来的状态需要手动定义

        在本题中,我们需要定义f[0]=0,有了f[0],那么f[2]、f[5]、f[7]就都有了,这就是转移方程计算的起点了。

 2.3.2边界情况:

        边界情况说白了就是要防止数组越界,很简单,如果计算时马上要用到负数,我们直接叫停,表示拼不出来即可,那么我们就可以把拼不出来的数记为正无穷即可。

 那么我们发现:

         f[1]=min\left \{ f[-1]+1,f[-4]+1,f[-6]+1\right \}=+\infty

 没错,我们成功记录出了拼不出来的数字。

2.3第四步:确定计算顺序:

 在本题以及大多数题中都是从小到大计算的

        确定原则:让每次计算时需要用到的数已知   

3.样例的实现:

 以上所有知识性内容就讲解完毕了,俗话说的好:纸上得来终觉浅,绝知此事要躬行。

鼓励大家有条件的先自己想一想怎么解决样例这一小问题。

先解决样例,算算2,5,7拼27最优解、

#include<iostream>
#include<limits.h>          //c++用到无穷大时的头文件
using namespace std;
int main()
{
	int coin[3]={2,5,7};    //这里记录我们的硬币种类
	

	int f[28];              //我们要用到f[27],故数组开到28
	f[0]=0;                 //初始条件不能忘
	for(int i=1;i<=27;i++)
		{
		f[i]=INT_MAX;
		for(int j=0;j<3;j++)
		if(i>=coin[j]&&f[i-coin[j]]!=INT_MAX)    //这里要注意!
		{
			f[i]=min(f[i-coin[j]]+1,f[i]);
		}
		}
	if(f[27]==INT_MAX)
	{
		f[27]=-1;
	}
	cout<<f[27];

	return 0; 
}

        注意我代码中标注需要注意的地方,想一想自己是否想到了这个判断句?

        加这两个判断的原因:

                                防止数组越界

如果我们只用这一式子

                f[x]=min\left \{ f[x-2]+1,f[x-5]+1,f[x-7]+1\right \}

理论上是可行的

        但是我们不可避免的要给无穷大加一,给无穷大加一当然是无穷大,学过数学的都知道,但是计算机不知道,编译系统不知道,在编译系统中给正无穷大加一大部分是会掉到负无穷大去的

        前半句:i>=coin[j]也就是说我们目前要拼的面额数应该是不小于我们准备将要用到的硬币的,我们不可能用5块钱拼2块钱,那注定不行。 

        后半句:f[i-coin[j]]!=无穷大、同样的以避免用到拼不出的面额来避免用到无穷大。

输出结果:

                      到这里我们对于这一思想就有了一个初步的了解。

              

4.典例的实现:

             这个题相对复杂一些,我们放到下面,有兴趣的可以了解一下

#include<iostream>
#include<limits.h>
using namespace std;
int main()
{
	int n,a;
	int coins=0;
	int coin[500];
	while(cin>>a)
	{
		coin[coins]=a;
		coins++;
		if(cin.get()=='\n')
		{
		break;
		}
	 } 
	
	cin>>n;
	int *f=new int[n+1];
	f[0]=0;
	for(int i=1;i<=n;i++)
		{
		f[i]=INT_MAX;
		for(int j=0;j<coins;j++)
		if(i>=coin[j]&&f[i-coin[j]]!=INT_MAX)
		{
			f[i]=min(f[i-coin[j]]+1,f[i]);
		}
		}
	if(f[n]==INT_MAX)
	{
		f[n]=-1;
	}
	cout<<f[n];
	delete []f;

	return 0; 
}

总结

                算法特点:时间复杂度n*m,相对较低。

                                  与递归相比,没有重复计算,非常快。

                祝大家学习进步    我去打游戏了

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
习webpack是为了掌握前端代码打包工具的使用。随着前端工程化的发展,webpack已经成为了前端开发中最常用的打包工具之一。习webpack可以帮助我们对项目的模块化管理、代码压缩与优化、资源加载等方面进行更加灵活和高效的处理。 这篇文章主要介绍了webpack的基础知识和使用方法。首先,它解释了webpack的基本概念,如入口和出口文件、loader和plugin等。然后,它详细介绍了如何配置webpack的各个参数和插件,包括如何使用Babel处理ES6语法,如何使用Less或Sass处理CSS,如何使用图片压缩插件等。 接着,文章讲解了webpack的打包原理和优化技巧。它介绍了webpack的模块化加载机制,以及如何使用Code Splitting和Dynamic Import等功能来提高页面加载速度。同时,它还提到了如何使用webpack进行代码分割和缓存优化,以及如何使用Tree Shaking和代码压缩插件来减小项目的体积。 最后,文章介绍了webpack的常见问题和解决方法。它列举了一些常见的错误和警告信息,并提供了相应的解决方案。此外,它还提供了一些常用的webpack插件和工具的推荐,帮助我们更好地进行前端开发。 总的来说,这篇文章提供了一个很好的入门教程,可以帮助我们快速掌握webpack的基本概念和使用方法。但是,要想在实际项目中灵活应用webpack,还需要深入习官方文档和相关资料,并结合实际项目进行实践。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不似少年游'

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值