首页 > 编程语言 >C++ 算法进阶系列之再聊聊动态规划的两把刷子

C++ 算法进阶系列之再聊聊动态规划的两把刷子

时间:2023-07-30 12:31:46浏览次数:36  
标签:进阶 ctrl int C++ 数组 按键 序列 两把刷子 dp

1. 前言

递归动态规划是算法界的两个扛把子,想进入算法之门,则必须理解、掌握这两种算法的本质。一旦参悟透这2种算法的精髓,再加上对树、图等复杂数据结构的深入理解,可以解决大部分的算法问题。

本文通过几个典型案例,再次聊聊动态规划算法。其实动态规划算法也就 2 把刷子。

  • 找到当前子问题的所有可选择项,在所有选择项中选择最大值或最小值。
  • 此子问题的最优解,作为下一个子问题的可选择项。最终推导出最终结果。每一个子问题只需关心与其有依赖子问题的结果而无需关注其实现过程。

动态规划的基本理念:步步为营,每次选择最好,达到最终是最好的。

如下通过几个案例来理解动态规划如何步步为营。

2. 最多的 A

2.1 问题描述

现假设有一个特殊的键盘包含下面的按键:

  • Key 1: (A):在屏幕上打印一个A

  • Key 2(ctrl-A):选中整个屏幕。

  • Key 3: (ctrl-c):复制选中区域到缓冲区

  • Key 4: (ctrl-v) : 将缓冲区内容输出到上次输入的结束位置,并显示在屏幕上。

现在,你只可以按键 N 次(使用上述四种按键) ,请问屏幕上最多可以显示几个 A呢?

2.2 样例

2.2.1 样例 1

输入:N =3 输出:3 解释:我们最多可以在屏幕上显示三个,A通过如下顺序按键: A,A,A

2.2.2 样例2

输入:N = 7 输出:9 解释:我们最多可以在屏幕上显示 9 个,A通过如下顺序按键 A,A,A,ctrl-A,ctrl-c,ctrl-V, ctrl V

2.3 问题解析

本题是求最值问题,可使用动态规划实现。如前所说,要使用动态规划算法,首先要知道对于每一个子问题而言有那些选择项。

Tips: 于本题而言,不同的按键次数可以认为是一个个子问题。

在屏幕上输出A,也就是让屏幕上的A字符的个数发生变化,可以有2种选择:

  • 直接按下A键。只需要一次按键就能输出`A`。
  • 复制屏幕上的A。先按下ctrl+A、ctrl+C,在缓冲区添加内容 ,然后可以重复按ctrl+v在屏幕上输出字母A

则在不同的按键次数下,哪一种选择最佳?

本题中动态规划算法要做的是:

  • 由小规模状态下的积累得到到大规模状态下的结果。此题要计算的是当按键次数的变化下子母A的个数。
  • 当次数状态量发生变化后,需要选择出最理想方案。

解题流程:

可以先定义一个一维 dp 数组。用来存储不同次数状态下子母A的个数。

1.png

现分析在不同次数下,哪一种选择方案可得到最理想结果。

  • 当按键次数为1时。此状态下只可能通过按下A键输出子母A

2.png

  • 当按键次数为 2时。也只能通过直接按下A键输出子母A,这时屏幕上的字母个数为 dp[2]=dp[1]+1

3.png

  • 当按键次数为3时。通过直接按`A`键,也可以通过复制输出A 。

    直接按下A键的个数为dp[3]=dp[2]+1

4.png

复制输出`A`。因复制的前置条件是要先按下`ctrl+A、ctrl+C`,意味着需要消耗掉2次按键,`ctrl+V`复制的内容应该是`dp[0]`位置字母`A`的个数。

5.png

​ 显然,直接按键输出的字母A更多。在两个方案中选择直接按下子母键为最佳方案。

4.png

  • 当按键次数为4时。

    直接按下A键输入A,此时屏幕上的A字符为4个。

6.png

使用复制方案在屏幕上输出时。复制方式有 2 种:

dp数组位置1ctrl+A、在2ctrl+C。这样复制的是dp[0]位置的子母个数。

7.png

dp数组位置2ctrl+A、在位置3ctrl+C。这样复制的是dp[1]位置的子母个数。

8.png

编码实现:

#include <iostream>
using namespace std;

//一维动态数组
int dp[100]= {0};
//按键次数
int n;
int main(int argc, char** argv) {
   	cin>>n;
	for(int i=1; i<=n; i++) {
		//直接按下A键
		dp[i]=dp[i-1]+1;
		//如果复制
		for( int j=2; j<i; j++ ) {
			dp[i]=max( dp[i],dp[j-2]*(i-j+1) );
		}
	}
    cout<<dp[n];
	return 0;
}

3. 最长递增子序列

3.1 问题描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

  • 输入:[10,9,2,5,3,7,101,18]

  • 输出:4

解释:最长的上升子序列是 [2,3,7,101],它的长度是 4。 说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。子序列和子串的区别,子串是连续的,子序列不一定是连续的。

### 3.2 问题分析

如何使用动态规划思想解决此问题。

  • 创建一维动态dp数组。记录当数组中的数据规模变化时,其子序列的长度。初始值为 1,数列是自己的子序列。

9.png

  • 从左向右扫描原始数组,扫描到数据 10时,显然,其子序列的个数为 1

10.png

  • 扫描到数据 9时,将其和前面的 10 进行比较,因比其小,故9不能为递增子序列做出贡献,保留原来子序列的个数。

11.png

  • 扫描到2时,其对应dp数组中的值为 1

12.png

  • 扫描到5时,其比10、9小,但比2大,可以成为以2为当前状态值的递增子序列。

13.png

  • 扫描到3时,因3只比2大,此时最长子序列应该是以2结束时的最长子序列加1

14.png

  • 扫描到7时,因 72,5,3都大,则需要在以2、5、3结束时最长子序列中求最大值。动态规划的特点就是,状态的改变时,往往需要在多个选择中选择最佳的。

15.png

  • 同理,当扫描到101,因为它比前面的所有数字都大,则需要在已经填充的dp数组中找出最大值且再加 1

16.png

  • 按相同的原理,最后 dp数组中的值应该如下所示。

17.png

3.3 编码实现

#include <iostream>
#include <cstring>
using namespace std;

//原始数组
int nums[]= {10,9,2,5,3,7,101,18};
//一维动态数组
int dp[100]= {1};

int main(int argc, char** argv) {
	int size=sizeof(nums)/sizeof(int);
	//最长子序列
	int maxVal=1;
	//遍历原始数组
	for( int i=0; i<size; i++ ) {
		dp[i]=1;
		//以 i 为当前位置,向原始数组之间扫描
		for( int j=i-1; j>=0; j-- ) {
			if( nums[i]>nums[j] ) {
				dp[i]=max(dp[i] ,dp[j]+1 ) ;
			}
		}
		if(maxVal<dp[i]) maxVal=dp[i];
	}
	for(int  i=0; i<size; i++)
		cout<<dp[i]<<"\t";
	cout<<"\n最长子序列长度:"<< maxVal<<endl;
	return 0;
}

4. 最小路径和

4.1 问题描述

现有一个二维数组nums,其中的元素都是非负整数,现在你站在左上角,只能向右或者向下移动,需要到达右下角。现在请你计算,经过的路径和最小是多少?

二维数组如下图所示。

18.png

本题是典型的动态规划类题型。

基本流程如下:

  • 基于动态规划的基本思想,先创建一个二维dp数组。存储出发位置到表格中每一个位置的最短路径之和。动态规划的基本套路就是步步为营,如果能保证从出发点到每一个位置的路径和都是最小的,自然能求解出到目标地的最短路径和。

19.png

  • dp表先填充一些显然易见的值,也称为base case。如出发点的最短路径是本身,如下图所示:

20.png

  • dp表中的第一行的值,只受左边值的影响,不存在多个选择,也容易找出来。其值为dp[0][i]=dp[0][i-1]+nums[0][i]

21.png

  • dp表中的第一列的值只受上边值的影响,也不存在多个选择,其值为dp[i][0]=dp[i-1][0]+nums[i][0]

22.png

  • 其它位置的值,需要在上边和左边的值里选择最小值后再与原数组中同位置的值相加。如下图所示A位置可以有2 个选择,选择其中较小的值。

23.png

25.png

  • 以此类推,可得到余下所有位置的值。如下图所示,红色数字表示最终的选择。

26.png

  • 最后绘制出从出发点到目标地的最短路径之和。

24.png

4.2 编码实现

#include <iostream>
#include <cstring>
using namespace std;

//原始数组
int nums[3][3]= { {2,4,3},
	{7,6,2},
	{1,8,5},
};
//二维动态数组
int dp[3][3]= { {0,0,0},
	{0,0,0},
	{0,0,0},
};

int main(int argcwfh, char** argv) {

	//起始位置的路径路径是本身
	dp[0][0]=nums[0][0];
	//第一行值只受左边值影响
	for(int i=1; i<3; i++ ) {
		dp[0][i]=dp[0][i-1]+nums[0][i];
	}
	//第一列值只受上边影响
	for(int i=1; i<3; i++ ) {
		dp[i][0]=dp[i-1][0]+nums[i-1][0];
	}

	//由上向下
	for(int i=1; i<3; i++) {
		for(int j=1; j<3; j++ ) {
			dp[i][j]=nums[i-1][j-1]+min(dp[i][j-1],dp[i-1][j]);
		}
	}

	for(int i=0; i<3; i++) {
		for(int j=0; j<3; j++ ) {
			cout<<dp[i][j]<<"\t";
		}
		cout<<endl;
	}

	return 0;
}

5. 总结

递归、动态规划是算法世界的两大剑客,两者互通款曲,解决同一个问题时,一个站在问题域的正方向,一个站在问题域的反方向。灵活运用且掌握这两大算法,是通向算法界的必修之路。

标签:进阶,ctrl,int,C++,数组,按键,序列,两把刷子,dp
From: https://blog.51cto.com/gkcode/6899789

相关文章

  • C++程序获取python脚本控制台输出的一种方法
    作者:朱金灿为什么大多数人学不会人工智能编程?>>>  最近要使用C++程序调用python脚本,调用方法是通过启动python进程来调用,其中遇到的一个问题是在C++程序中需要获取python脚本的控制台输出信息。经过摸索使用_popen函数实现了。下面用python脚本和C++调用示例程序来说明。py......
  • VS选择Visual C++中的控制台项目和空项目、Windows桌面应用程序三者之间有什么区别?
    在VisualStudio中创建C/C++项目时,可以选择控制台项目、空项目和Windows桌面应用程序,它们有以下区别:控制台项目(ConsoleApplication):这种项目类型适用于命令行应用程序的开发。它提供一个命令行界面,可以在控制台中进行输入和输出操作,通常用于简单的控制台程序,如计算器、文件......
  • 雀魂07 进阶技巧
    在无人被飞的情况下,东场每个人一个庄位,而南场每个人是两个庄。东场运气>技术,南场正好相反 制定振听规则的意义在于防守判断与减少见逃行为的发生。所以,在出牌的后期,要如果自己的牌处于听的状态,但是为了防止其他人优势和。可以放弃和牌,进行防守。出牌的时候,参考别人打出过的牌,......
  • C++运算符重载
    1.概念赋予运算符更多的功能。2.内容赋值运算符+-*/%运算符自增自减运算符输出流运算符的重载<<输入流运算符的重载>>函数调用运算符()下标运算符[]成员访问运算符->,*3.赋值运算符这个一般是用已存在的对象赋值给另一个已存在的对象。//如存在Comp......
  • C++中fork函数的使用及原理
    C++中fork函数的使用及原理,在C++中,fork函数用于创建一个新的进程称为子进程,该进程与原始进程几乎完全相同。fork函数的基本概况fork()函数调用成功之后,会有两个返回值。当前进程,也就是父进程返回子进程的pid,子进程返回0。如果函数调用错误,返回为-1。#include<stdio.h>#include......
  • C++中的exec()函数
    exec()函数在C++中是一个进程控制函数,用于创建新进程执行其他程序或命令行指令。exec()函数可以替换当前进程的代码和数据,创建新的进程运行其他程序。exec()函数有多个版本,例如execl、execv、execle、execve等,根据不同的参数类型和个数来使用。前言fork函数之后,如果想要把子进程换......
  • C++实现简单的ls命令以及原理
    C++实现简单的ls命令及其原理,C++实现ls命令可通过调用系统函数实现读取目录中的文件名和属性,再通过标准输出进行显示。对控制参数的处理一共有7个可选参数,分别是-a、-l、-R、-t、-r、-i、-s,这些参数可以相互自由组合,因此可以设计一种机制,就是直接把它们全部用循环一次性做或运算,......
  • C++实现工资管理中的随机教师信息生成功能
    使用C++实现工资管理中的随机教师信息生成功能,想要做一个教师工资管理系统,就必须得准备好数据,但是这些数据如果用手一行一行地敲,那么工作量是非常大的,因此,我就产生了用C语言实现直接生成大量的教师基本信息的想法,需要的朋友可以参考下。教师的基本信息typedefstructteacher{......
  • 【webpack系列】从基础配置到掌握进阶用法
    前言本篇文章将介绍一些webpack的进阶用法,演示内容继承自上一篇文章的内容,所以没看过上一篇文章的建议先学习上一篇内容再阅读此篇内容,会更有利于此篇的学习~文件指纹文件指纹指的是打包输出的文件名后缀,一般用来做版本管理、缓存等webpack的指纹策略有三种:hash、chunkhash、content......
  • 笔记|《面向对象编程技术与方法(C++)》电子工业出版社
    第一章概述C++多态:https://blog.csdn.net/K346K346/article/details/82774937第二章编程基础数据类型枚举:https://www.runoob.com/w3cnote/cpp-enum-intro.html联合:https://www.runoob.com/cprogramming/c-unions.html作用域运算符:c++入门学习篇(1)之::作用域符解析c++条......