动态规划(DP)(算法笔记)

这篇具有很好参考价值的文章主要介绍了动态规划(DP)(算法笔记)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激。


前言

动态规划(Dynamic Programming,DP)是一种用来解决一类最优化问题的算法思想。简单来说,动态规划将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子向题的解记录下来,这样当下一次碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算。注意:虽然动态规划采用这种方式来提高计算效率,但不能说这种做法就是动态规划的核心。一般可以使用递归或者递推的写法来实现动态规划,其中递归写法在此处又称作记忆化搜索,递推写法是自底向上,递归写法是自顶向下(和面向过程和面向对象相似但不完全一样)。

一、动态规划概述

动态规划粗俗点可以说是对分治算法的优化,一切可以用动态规划解决的问题都可以用分治解决,其核心是状态转移方程。动态规划是解决最优解的算法,一个问题必须拥有重叠子问题和最优子结构,才能用动态规划去解决。重叠子问题很好理解,在递归中又被称为记忆化搜索;最优子结构指原问题的最优解可以由其子问题的最优解构造出来,并不一定是满足分治法那样到达递归边界即为解,而是需要将子问题的最优解记录下来,通过某种关系用子问题的最优解“构造”出原问题的最优解,至于怎么构造,就取决于个人理解了。
所以,动态规划的解题步骤如下:
1.通过原问题分析最优子结构,即将原问题分解为子问题,分析子问题中存在的重叠子问题;
2.设置状态dp记录子问题的最优解,设计状态转移方程描述原问题与子问题之间的递推关系(当然可以用分治法来分析原问题并采用递归)。
3.求解dp,并通过dp来构造原问题的最优解。

二、算法设计

1.上楼||

dp算法,算法笔记,算法,动态规划
首先先把这道题的描述转化为最优解问题,即求最大上楼策略。
1.问题分解:找到题中的子问题,这很容易,就是走一级台阶或者两级台阶,所以可以画出问题分解图如下:
dp算法,算法笔记,算法,动态规划
当然也可以从n向下画;
2.设计状态和状态转移方程:dp[i]记录走到某个台阶时继续向下走的最大方案数,所以可以设计状态转移方程为:dp[i] = dp[i + 1] + dp[i + 2];
3.从底层开始循环,通过状态转移方程扩散到整个数组,dp[0]即为求解。
注:递归实现更简单,这里就不赘述了。
完整代码如下

#include<cstdio>
const int MAXN = 10000;
int dp[MAXN] = {};
int main(){
	int n;
	scanf("%d", &n);
	dp[n] = 0;
	dp[n - 1] = 1;
	dp[n - 2] = 2;
	for(int i = n - 3; i >= 0; i--)
		dp[i] = (dp[i + 1] + dp[i + 2]) % 10007;
	printf("%d", dp[0]);
}

2.最大连续子序列和

dp算法,算法笔记,算法,动态规划
这道题有两种解法,分治法和动态规划,这里先分析动态规划法。

动态规划

1.问题分解:将原问题这样分解:求a1····an-1序列的连续子序列最大值,并判断A[i]的状态是加入序列或者以A[i]为子序列首元素;求a1···an-2······
2.设计状态和状态转移方程:dp[i]为以A[i]为结尾连续序列的最大和,所以可以设计状态转移方程为:dp[i] = max(A[i] , dp[i - 1] + A[i]);通过这个状态转移方程可以认识到并不需要枚举左端点,因为每一个子问题的最优解都可以借由前一个子问题的最优解推导出来;
3.从底层开始循环,通过状态转移方程扩散到整个数组,dp[i]中的最大值就是原问题的最优解,注意不是dp[n]。
完整代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 10000;
int A[MAXN] = {};
int dp[MAXN] = {};

int main(){
	int n;
	scanf("%d", &n);
	for(int i = 0; i <= n - 1; i++) scanf("%d", A + i);
	dp[0] = A[0];
	int MAX = -10e5;
	for(int i = 1; i <= n - 1; i++) {
		dp[i] = max(A[i] , dp[i - 1] + A[i]);
		if(dp[i] > MAX) MAX = dp[i];
	}
	printf("%d", MAX);
}

分治

1.问题分解:
如果将所给的序列A[1…n]分为长度相等的两段A[1…n/2]和A[n/2+1…n],分别求出这两段的最大字段和,则A[1…n]的最大子段和有3种情形:
(1)A[1…n]的最大子段和与A[1…n/2]的最大子段和相同;
(2)A[1…n]的最大子段和与A[n/2+1…n]的最大子段和相同;
(3)A[1…n]的最大子段和是在A[1…n/2]和A[n/2+1…n]上的两个片段,即在A[1…n]的中间位置;
2.解决:
(1)和(2)采用递归解决,对(3)可以从n/2和n/2+1为起点,分别在两个片段中求出最大子序列和,然后求和即为A[1…n]的最大子段和;
3.合并:
比较在分解阶段的3种情况下的最大子段和,取三者中的较大者为原问题的解。
完整代码就先不放了(没时间写了···),对于这本书,最重要的是第一种方法,因为之后的例题都是采用这种思路的。

3.最大连续子序列和的最优方案

dp算法,算法笔记,算法,动态规划
这道题在上一道题的基础上,需要记录到达每一个以A[i]为结尾的最大连续子序列的首元素和尾元素,所以可以开一个结构体存储首尾元素位置,随着动态规划的求解不断记录首尾元素,最后将第一个最大子序列和对应的以A[i]为结尾的首尾元素。其中,结构体的记录有三种情况:
if(A[i] > dp[i - 1] + A[i]) Node[i] = {i , i};
if(A[i] < dp[i - 1] + A[i]) Node[i] = {Node[i - 1].i , i};
if(A[i] == dp[i - 1] + A[i]) Node[i] = {Node[i - 1].i , i};
前两个分别代表以A[i]为新的子序列首尾元素和将A[i]加入上一个序列的情况。但是在首尾元素的选择上还存在第三种情况,即A[i] == dp[i - 1] + A[i],这个情况在计算最大和时并不需要考虑在内,但现在是要求具体的首尾元素位置,并且在存在多解的情况下总是选择首元素下标最小的解(在判断最大值时就已经是尾元素下标最小的情况了),那么当碰到A[i] == dp[i - 1] + A[i]时,意味着A[i]和dp[i - 1] + A[i]都可以满足情况,这时为了满足首元素位置最小的情况,就将A[i]加入前一序列,而不是以A[i]为新序列的首元素。
完整代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 10000;
int A[MAXN] = {};
int dp[MAXN] = {};
struct node{
	int i , j;
}Node[MAXN]; //记录当前序列的首位坐标 

int main(){
	int n;
	scanf("%d", &n);
	for(int i = 0; i <= n - 1; i++) scanf("%d", A + i);
	dp[0] = A[0];
	Node[0] = {0 , 0};
	int MAX = -10e5;
	int MAXI = 0;
	for(int i = 1; i <= n - 1; i++) {
		dp[i] = max(A[i] , dp[i - 1] + A[i]);
		if(dp[i] > MAX) {    //记录的就是最小的尾元素下标
			MAX = dp[i];
			MAXI = i;   //记录结尾 
		}
		if(A[i] > dp[i - 1] + A[i]) Node[i] = {i , i};  //以A[i]为首尾
		if(A[i] < dp[i - 1] + A[i]) Node[i] = {Node[i - 1].i , i};    //将A[i]加入上一个序列 
		if(A[i] == dp[i - 1] + A[i]) Node[i] = {Node[i - 1].i , i};  //总是将A[i]加入上一个序列 
	}
	printf("%d %d %d", MAX , Node[MAXI].i + 1 , Node[MAXI].j + 1);
}

三、备注

1.注意体会重叠子问题和最优子结构在递归和递推中的体现;
2.原问题的最优解是由子问题的最优解构建出来的,这一点还是区别于分治法的将问题缩小到最小子问题解决的思路;
3.分治与动态规划的区别(摘自算法笔记):分治和动态规划都是将问题分解为子问题,然后合并子问题的解得原问题的解。但是不同的是,分治法分解出的子问题是不重叠的,因此分治法解决的问题不拥有重叠于间题,而动态规划解决的问题拥有重叠子问题。例如,归并样序和快速排序是分别处理左序列和右序列,然后将左右序列的结果合并,过程中不出现重叠子问题,因此它们使用的都是分治法。另外,分治决的问题不一定是最优化问愿,而动态规划解决的问题一定是最优化问题。
4.贪心与动态规划的区别(摘自算法笔记):贪心和动态规划都要求原问题必须拥有最优子结构。二者的区别在于,贪心法采用的计算方式类似于上面介绍的“自顶向下”,但是并不等待子问题求解完毕后再选择使用哪一个,而是通过一种策略直接选择一个子问题去求解,没被选择的子问题就不去求解了,直接抛弃。也就是说,它总是只在上一步选择的基础上继续选择,因此整个过程以一种单链的流水方式进行,显然这种所谓“最优选择”的正确性需要用归纳法证明。例如对数塔问题而言,贪心法从最上层开始,每次选择左下和右下两个数字中较大的一个,一直到最底层得到最后结果,显然这不一定可以得到最优解。而动态规划不管是采用自底向上还是自项向下的计算方式,都是从边界开始向上得到目标问题的解。也就是说,它总是会考虑所有子问题,并选择继承能得到最优结果的那个,对暂时没被继承的子问题,由于重叠子问题的存在,后期可能会再次考虑它们,因此还有机会成为全局最优的一部分,不需要放弃。所以贪心是一种壮士断腕的决策,只要进行了选择,就不后悔:动态规划则要看哪个选择笑到了最后,暂时的领先说明不了什么。文章来源地址https://www.toymoban.com/news/detail-752025.html

到了这里,关于动态规划(DP)(算法笔记)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • DP算法:动态规划算法

    (1)确定初始状态 (2)确定转移矩阵,得到每个阶段的状态,由上一阶段推到出来 (3)确定边界条件。 蓝桥杯——印章(python实现) 使用dp记录状态,dp[i][j]表示买i张印章,凑齐j种印章的概率 i表示买的印章数,j表示凑齐的印章种数 情况一:如果ij,不可能凑齐印章,概

    2024年02月07日
    浏览(37)
  • 算法——动态规划(DP)——递推

    动态规划常用于解决优化问题。 动态规划通常以自底向上或自顶向下的方式进行求解。 自底向上的动态规划从最简单的子问题开始,逐步解决更复杂的问题,直到达到原始问题。 自顶向下的动态规划则从原始问题出发,分解成子问题,并逐步求解这些子问题。 动态规划算法

    2024年01月20日
    浏览(43)
  • ★动态规划(DP算法)详解

    什么是动态规划:动态规划_百度百科 内容太多了不作介绍,重点部分是无后效性,重叠子问题,最优子结构。 问S-P1和S-P2有多少种路径数,毫无疑问可以先从S开始深搜两次,S-P1和S-P2找出所有路径数,但是当这个图足够大,那就会超时。 动态规划旨在用 空间换时间 ,我们

    2024年02月04日
    浏览(38)
  • acwing算法基础之动态规划--线性DP和区间DP

    线性DP:状态转移表达式存在明显的线性关系。 区间DP:与顺序有关,状态与区间有关。 题目1 :数字三角形。 解题思路:直接DP即可, f[i][j] 可以来自 f[i-1][j] + a[i][j] 和 f[i-1][j-1] + a[i][j] ,注意 f[i-1][j] 不存在的情况(最后一个点)和 f[i-1][j-1] 不存在的情况(第一个点)。

    2024年02月04日
    浏览(38)
  • 动态规划——区间DP 学习笔记

    不含四边形不等式优化。 线性动态规划的局限性在于,它只能顺推或倒退,而不能有子区间依赖的问题。 区间动态规划是线性动态规划的扩展,它将问题划分为若干个子区间,并通过定义状态和状态转移方程来求解每个子区间的最优解,最终得到整个区间的最优解。 区间动

    2024年02月08日
    浏览(31)
  • 动态规划——状压DP 学习笔记

    前置知识:位运算 动态规划的过程是随着阶段的增长,在每个状态维度上不断扩展的。 在任意时刻,已经求出最优解的状态与尚未求出最优解的状态在各维度上的分界点组成了 DP 扩展的“轮廓”。对于某些问题,我们需要在动态规划的“状态”中记录一个集合,保存这个“

    2024年02月08日
    浏览(42)
  • 动态规划——树形DP 学习笔记

    前置知识:树基础。 树形 DP,即在树上进行的 DP,最常见的状态表示为 (f_{u,cdots}) ,表示以 (u) 为根的子树的某个东东。 本文将讲解一些经典题目(树的子树个数、树的最大独立集、树的最小点覆盖、树的最小支配集、树的直径、树的重心、树的中心),以及一些常见形

    2024年02月08日
    浏览(27)
  • 动态规划——数位DP 学习笔记

    引入 数位 DP 往往都是这样的题型:给定一个区间 ([l, r]) ,求这个区间中满足某种条件的数的总数。 简单的暴力代码如下: 而当数据规模过大,暴力枚举就 (mathbb T) 飞了,因此引入数位 DP: 概念 数位(digit):对于十进制,即把一个数字按照个位、十位、百位等,一位

    2024年02月08日
    浏览(38)
  • 动态规划——斜率优化DP 学习笔记

    适用于求解最优解(最大、最小)问题。 可以将转移方程可以化为 (left[begin{array}{rl} 仅与 space i space 有关 是我们想要最大/最小化的 \\\\ 仅与 space j space 有关 是已知的 \\\\ 与 space i space 和 space j space 都有关 是两项相乘 end{array}right]) 三部分的, 都可以考虑用斜率优化

    2024年02月08日
    浏览(40)
  • 动态规划——矩阵优化DP 学习笔记

    前置知识:矩阵、矩阵乘法。 斐波那契数列 在斐波那契数列当中, (f_1 = f_2 = 1) , (f_i = f_{i - 1} + f_{i - 2}) ,求 (f_n) 。 而分析式子可以知道,求 (f_k) 仅与 (f_{k - 1}) 和 (f_{k - 2}) 有关; 所以我们设矩阵 (F_i = begin{bmatrix} f_{i - 1} f_{i - 2} end{bmatrix}) 。 设矩阵 (text{Ba

    2024年02月08日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包