老壁灯带你入门动态规划

news2024/11/15 12:22:55

1. 什么是动态规划

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。

从字面意义上来理解,就是走一步看一步,边解决问题,边对问题进行整体规划

其实,动态规划的本质就是将问题拆分为小的子问题,对小问题的一步步解决,小问题渐渐积累为较大的问题,最终就可以解决掉整个问题。

这似乎与递归解决问题的思路很像,但是动态规划一般采用的是迭代的方法。

在解决问题的过程中,我们一般会记录下小问题的解决能带给我们的有效信息,从而利用小问题解决大问题,这样就可以避免对某些问题的重复计算。

动态规划不是什么固定的解题套路,而是提供了一种解题的思路。

动态规划的核心

动态规划的核心在于对两个概念的确定:

1. 当前状态的表示(小问题)

2. 状态之间的递推关系(小问题与较大问题之间的联系)

什么问题可以用动态规划解决

如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。

例如,求最大值/最小值,可不可行,是不是,方案个数等

 2. 初次遇见动态规划

光是看上面的介绍,似乎有点晕头转向不知所云。

那么,不妨更着博主的学习路径,来看看动态规划到底是怎么回事。


兑换零钱

这是leetcode上的一道题,刷题链接:. - 力扣(LeetCode)

我便是在遇到这道题时,初次见识了动态规划。 

  

 1. 回溯解法

询问可能性,这让我想到了之前做过的N皇后的问题:C语言解决N皇后问题-CSDN博客

于是我们首先考虑试试回溯的算法。

思路

1. 每次调用函数选择一个硬币,将amount减去该硬币的值之后,传给下一次调用。

2. 依次传递下去,如果某次函数接收到的amount为0,则说明找到了一种可行的组合,此时返回1。

3. 如果某次函数接收到的amount为1,则说明该种组合不可行,此时返回0。

4. 为了避免重复,我们每次传给下一次调用的coins数组都不包括在该硬币位置之前的硬币。

例如,在示例1所给数据下,如果每次传入全部的coins,则会出现诸如2 1 1 1和1 2 1 1的重复情况。 

 代码
int change1(int amount, int* coins, int coinsSize) 
{
    if(amount == 0)
    return 1;
    if(amount < 0||coinsSize <= 0)
    return 0;
    int sum = 0;
    for(int i = 0; i < coinsSize; i++)
    {
        sum += change1(amount - coins[i], coins + i, coinsSize - i);
    }
    return sum;
}
总结

这种解法,虽然极力避免了对重复情况的考虑,但是其递归的本质,还是使其在处理较大数据时会超时。

而且,相比于动态规划,这种解法似乎有点无头苍蝇的感觉,对所有情况进行无差别尝试。

回溯的解法,其运算过程像是树一样,不断展开,每一种情况对应树的一个末梢。

有了这个树状的结构,我们其实可以将每种情况是什么一并打印出来,但是这道题只要求我们求出可行方案的种数。

所以,我们似乎还是做了很多不必要的工作,我们的小问题留下的信息太多了,我们每次计算做的事也太多了。

仔细回顾这道题,我们会发现,如果将我们的思路总结成递归的基本思路,就会是如下这样:

1. 要知道coins组合成amount有多少种方法(W_{aomunt}),我们首先要知道组合成amount-coins[i]有多少种方法(W_{amount-coin[i]})。

2. 那么W_{amount} = \sum_{i=1}^{n} W_{amount-coins[i]}

但是,我们在解决的过程中,不仅把 W_{amount-coin[i]}记录了下来,还将得到其的路径也记录了下来。

而且,在不同的深度中amount-coins[i]的大小可能相同,这也就导致我们还是不可避免地做了很多重复的工作。

于是我在官方的解答中,首次见识了动态规划是如何解决问题的。

2. 动态规划解法

思路

1. 与前面回溯解法总结后的思路相同,我们也要先求得W_{amount-coin[i]},但是我们只记录在解决这个问题得到的信息中,我们需要的,也就是其值。

2. 状态的表示:我们用一个一维数组来记录每种金额的组合数,元素下标就代表金额。这样,对于每种可能出现的金额(amount-coins[i]),我们就仅会做一次计算。

3. 状态之间的关系:也就是W_{amount} = \sum_{i=1}^{n} W_{amount-coins[i]}。只要知道较小金额的组合种数,就可以依次递推得到较大金额的组合种数。

4. 结果:我们要求的,其实也就是下标为amount的元素的大小。

5. 下标为0的元素值赋为1,因为得到总金额为0的组合有且仅有一枚硬币都没有的组合。

6. 数组默认初始化为0,这样一来,如果不存在可以组成amount-coins[i]的组合,那么W_{amount-coin[i]}W_{aomunt}的贡献就为0。

代码
int change2(int amount, int* coins, int coinsSize)
{
    int dp[amount + 1];
    memset(dp, 0, sizeof(dp));
    dp[0] = 1;
    for(int i = 0; i < coinsSize; i++)
    {
        for(int j = coins[i]; j <= amount; j++)
        {
            dp[j] += dp[j - coins[i]];
        }
    }
    return dp[amount];
}
总结

动态规划的算法通常采用迭代来实现,这不仅解决了递归,回溯的缺陷,而且真正意义上做到了杜绝重复子问题的运算。

并且,在解决问题的过程中,充分利用了子问题留下的有效信息来解决当前问题。

递归时,只能利用自己所在分支的信息,而动态规划可以对全局的信息进行利用。

但是,动态规划的不足就在于其实在不好想到,且没有固定的套路,是考验个人能力的好手段。

3. 尝试自己解决动态规划问题

这是牛客网上的一道题,刷题链接:公共子串计算_牛客题霸_牛客网

 思路

1. 这道题是要我们找两个字符串的公共子串,这使得我会想起之前研究过的kmp算法(从一无所有的角度出发,带你一步步实现kmp算法-CSDN博客)(对这道题不是很重要,不了解也可),这么一想,似乎得到next数组的思想就像是动态规划。

2. 我们依然想用一个数组来存储有效信息,这个数组的下标表示当前的状态(我正在解决哪个问题),其对应的值就是我们要留下的有效信息。

3. 我们用i和j分别指向两个字符串,当所指两个字符匹配成功时,当前公共子串的长度就加1。所以,要想知道当前公共子串的长度,我们就要知道在这两个字符匹配成功之前公共子串的长度。

4. 那么,这道题用一维数组似乎无法很好地表示当前的状态,因为我们要知道匹配之前公共子串的长度,那么其对应的i和j就都需要能表示出来,我们才能找到它。

5. 状态的表示:于是我们采用二维数组(dp[][])来定义当前状态,元素的下标分别为i和j,表示当前公共子串的末尾在两个字符串中分别在第i个和第j个位置上。

6. 状态之间的关系:如果当前两个字符配对成功则dp[i][j] = dp[i-1][j-1] + 1;如果匹配失败,则dp[i][j] = 0。

7. 结果:我们定义一个变量maxLen来记录目前最长的公共子串长度,如果某位置上dp[i][j] > maxLen,则maxLen = dp[i][j]。将每个位置的公共子串都检查完之后maxLen即是要求结果。

代码

#include <stdio.h>
#include <string.h>

int main() 
{
    char arr1[151] = {0};
    char arr2[151] = {0};
    scanf("%s", arr1);
    scanf("%s", arr2);
    int len1 = strlen(arr1);
    int len2 = strlen(arr2);
    int dp[len1+1][len2+1];
    int maxLen = 0;

    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= len1; i++)
    {
        for(int j = 1; j <= len2; j++)
        {
            dp[i][j] = arr1[i-1] == arr2[j-1] ? dp[i-1][j-1] + 1 : 0;
            maxLen = maxLen > dp[i][j] ? maxLen : dp[i][j];
        }
    }

    printf("%d\n", maxLen);
    
    return 0;
}

总结

动态规划是很值得学习的东西,也是需要费力攻克的难关,绝不是看一两篇文章就能完全掌握的。

本文章只是带领读者初步了解动态规划这种解题思想,能够入门,方便进行更加深入的学习。

博主也是刚刚接触到这类题型,顿时感到未来的路还很长,希望在了解更多之后还有机会分享动态规划学习的文章。

接下来,可以通过背包问题,进行更加深入地学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1549197.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32学习笔记(7_2)- ADC模数转换器代码

无人问津也好&#xff0c;技不如人也罢&#xff0c;都应静下心来&#xff0c;去做该做的事。 最近在学STM32&#xff0c;所以也开贴记录一下主要内容&#xff0c;省的过目即忘。视频教程为江科大&#xff08;改名江协科技&#xff09;&#xff0c;网站jiangxiekeji.com 本期开…

MyBatis入门01

MyBatis入门01 文章目录 MyBatis入门01前言一、搭建环境1.新建一个普通的maven项目2.删除src目录3.导入maven依赖&#xff1a;mysql驱动&#xff08;操作jdbc&#xff09;&#xff0c;juint&#xff0c;mybatis注意&#xff1a;要假如builder标签&#xff0c;预防配置文件不可导…

3.27作业

1、完成下面类 #include <iostream> #include <cstring> using namespace std;class myString { private:char *str; //记录c风格的字符串int size; //记录字符串的实际长度 public://无参构造myString():size(10){str new char[size]; …

算法(6)KMP+trie

KMP&#xff1a; 最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili 该视频使用python书写代码&#xff0c;不会python的小伙伴也可以看看了解kmp的大致思路。 问题描述&#xff1a; kmp&#xff1a;字符串匹配算法&#xff0c;用来找一个长字符串中出现了几次小字符串&#xf…

SpringBoot整合Redis:缓存击穿--互斥锁解决

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏Redis实战与进阶 本专栏讲解Redis从原理到实践 …

【旅游】泉州攻略v1.0.0

一、泉州古城 泉州市距离深圳大约520公里&#xff0c;从深圳北站出发&#xff0c;高铁大约3小时30分。 到达泉州西站后&#xff0c;往东南方向大约8公里&#xff0c;就可以到达主要的旅游景点泉州古城。 古城很适合使用一天玩耍&#xff0c;核心路线如下&#xff1a; 一路的景…

#Linux系统编程(关于解决Source Insight自动补全的问题)

&#xff08;一&#xff09;发行版&#xff1a;Ubuntu16.04.7 &#xff08;二&#xff09;记录&#xff1a; &#xff08;1&#xff09;参考博文 Source Insight 4.0 添加函数库的头文件实现函数的自动补齐 - 简书 (jianshu.com)https://www.jianshu.com/p/96595eefb988 &am…

关于内存函数的介绍

1.memcpy 2.memmove 3.memset 4.memcmp 其中&#xff0c;重点讲解memcpy 以及memmove。 1.C 库函数&#xff1a; void *memcpy(void *str1, const void *str2, size_t n) 函数作用&#xff1a;在 str2 复制 n 个字节到 str1。 其中&#xff0c;str1用于指向存储复制内容…

前端学习<二>CSS基础——04-CSS选择器:伪类

伪类&#xff08;伪类选择器&#xff09; 伪类&#xff1a;同一个标签&#xff0c;根据其不同的种状态&#xff0c;有不同的样式。这就叫做“伪类”。伪类用冒号来表示。 比如div是属于box类&#xff0c;这一点很明确&#xff0c;就是属于box类。但是a属于什么类&#xff1f;…

onnxruntime 中的 Gather 算子

上一篇文章中介绍了 Division by Invariant Integers using Multiplication 的原理&#xff0c;很多框架均才用该算法优化除法运算。onnxruntime 是已知实现中最为简洁的&#xff0c;因此本文结合 onnxruntime 的 Gather 实现进行介绍。 Gather 算子是一个索引类算子&#xff0…

unity学习(72)——编译游戏发生错误4——GAME_STATE状态

1.经过一天的冷静&#xff0c;我感觉问题出在mapHandler的update中。 如果还没有初始化对象&#xff0c;就开始读取对象的内容&#xff0c;一定会有异常的。 2.之前已有GameState结构体&#xff0c;我一直没当回事&#xff0c;这次用到了 3.从user切换到map场景的过程中会触发如…

号码采集协议讲解

仅供学习研究交流使用 需要的进去拿源码或者成品

【区块链】C语言编程实现三叉Merkle树

目录 1. Merkle树简介2. 构建Merkle树3. 生成SPV路径4. 验证SPV路径5. 三叉Merkle树创建、SPV生成及验证总程序6. 程序运行结果 1. Merkle树简介 如上图所示&#xff0c;Merkle 树的叶子节点为交易序列&#xff0c;对每一笔交易进行 Hash&#xff08;SHA 256算法&#xff09; 之…

vivado 在远程主机上启动作业、ISE命令图、实施类别,战略描述和指令映射

在远程主机上启动作业 一旦配置了远程主机&#xff0c;使用它们启动Vivado作业就很容易了。下图显示了启动运行对话框。启动跑步时&#xff0c;选择“在远程上启动跑步”hosts或Launch在群集上运行&#xff0c;然后选择特定的群集。这些作业将使用您的要执行的预配置设置。 作业…

针对COT控制模式下低ESR电容造成次谐波振荡问题的片内斜波补偿方案

COT模式&#xff1a;MOS管固定导通时间控制模式&#xff0c;关断时间由输出反馈电压与内部基准源的相较值决定。 RBCOT控制模式&#xff1a;Ripple-Based COT基于纹波的固定导通时间控制方法&#xff0c;特别的是环路控制部分主要有固定导通时间发生装置及比较器组成。RBCOT控…

DreamPolisher、InternLM2 、AniArtAvatar、PlainMamba、AniPortrait

本文首发于公众号&#xff1a;机器感知 DreamPolisher、InternLM2 、AniArtAvatar、PlainMamba、AniPortrait DreamPolisher: Towards High-Quality Text-to-3D Generation via Geometric Diffusion We present DreamPolisher, a novel Gaussian Splatting based method wit…

PPP实验

一、实验拓扑图 二、实验要求 1、R1和R2使用PPP链路直连&#xff0c;R2和R3把2条PPP链路捆绑为PPP MP直连 2、按照图示配置IP地址 3、R2对R1的PPP进行单向chap验证 4、R2和R3的PPP进行双向chap验证 三、实验步骤 1、PPP MP&#xff1a; &#xff08;1&#xff09;R2配置&#x…

C语言从入门到实战----数据在内存中的存储

1. 整数在内存中的存储 在讲解操作符的时候&#xff0c;我们就讲过了下⾯的内容&#xff1a; 整数的2进制表⽰⽅法有三种&#xff0c;即 原码、反码和补码 有符号的整数&#xff0c;三种表⽰⽅法均有符号位和数值位两部分&#xff0c;符号位都是⽤0表⽰“正”&#xff0c;⽤…

dji esdk开发(4)SDK互联互通(与云端进行小数据通信)

Edge SDK 提供接口可以通过上云 API 与和机场建立连接的云端服务器进行小数据交互,即向云端服务器发送自定义小数据与接收来自云端服务器的自定义小数据。 注意: 使用该接口发送和接收数据上下行通道最大带宽不应超过 0.5Mb/S。 1、云端低速通道介绍 使用自定义小数据通道需…

C++类和对象、面向对象编程 (OOP)

文章目录 一、封装1.抽象、封装2.类和对象(0)学习视频(1)类的构成(2)三种访问权限(3)struct和class的区别(4)私有的成员变量、共有的成员函数(5)类内可以直接访问私有成员&#xff0c;不需要经过对象 二、继承三、多态1.概念2.多态的满足条件3.多态的使用条件4.多态原理剖析5.纯…