DP算法入门

文章目录

  • 动态规划的引入
    • 什么问题可以使用动态规划?
    • 常用的动态规划的模板:
    • 举个例子
  • 线性DP
    • 例子:走楼梯(低精的)
    • 分析
    • 代码实现方法

说句实话,动规的题目真的是……,变化莫测,可能你在学这个东西的时候只是一个菜鸟,但学完它,你将会脱胎换骨,进入大佬的殿堂,一个新的进阶,在 D P DP DP 中有简单的几分钟就能写完的 走楼梯,也有几个月都写不完的 DP模板。

总之,进入了DP,你将会变得不一样。

请大家在观看之前给作者的博客一个免费的赞吧,或者关注一下作者的洛谷也行的。

作者的洛谷

动态规划的引入

先说一嘴,DP很长,所以我分了几段。

刷题网站:

进阶篇提单

什么问题可以使用动态规划?

(1)最优子结构–原问题的最优解包含子问题最优解

(2)子问题重叠 ≈ ≈ 递归–记忆化搜索,斐波那契数列。

方法:建立答案数组,需要时直接查表。
特征:从下到上。(这点很重要!)
注意:此条件为必要不充分条件。

(3)无后效性

后效性:前面状态未解

常用的动态规划的模板:

(1)背包问题

(2)线性DP(这个好像是最简单的……)

(3)区间DP

(4)树形DP

(5)数位DP–背包类树形DP,二次扫描与换根

(6)状态压缩DP

(7)插头DP–单回路,多回路。

但是有些时候还是会 T L E TLE TLE 怎么办呢?

常用的优化:

(1)倍增优化

(2)数据结构优化

(3)单调队列优化

(4)斜率优化

(5)四边不等式优化

3.动态规划的秘籍是什么呢?(有向无环图)

F I R S T FIRST FIRST : 三要素:

(1)状态

(2)阶段

(3)决策


举个例子

E X A M P L E EXAMPLE EXAMPLE :

使用动态规划求解单源最短路径问题。

算法: 贪心动态规划

分析:

求解k号的最短路径时,只需找与它相连接的节点的路径的最小值。
并且,此问题满足无后效性,所以可以使用DP。

表示:

(1)状态:

d p [ i ] dp[i] dp[i] 表示从原点到各个节点的最短路径

(2)阶段:

拓扑顺序

(3)决策:

选最大值,最小值,求和。
公式: d p [ i ] = m i n ( d p [ j ] + w [ j ] [ i ] ) dp[i]=min(dp[j]+w[j][i]) dp[i]=min(dp[j]+w[j][i])
w [ j ] [ i ] w[j][i] w[j][i] 表示的通往第 i i i 个地点的路径的长度。

此公式就是这个问题的状态转移方程。
只找上一个状态即可。

(4)初始化

d p [ 1 ] = 0 dp[1]=0 dp[1]=0 需要有一个开始的条件。

状态转移方程无法推到的项,不一定一定是第一项。

(5)求解目标

最后这两步是简单但是易错的,需要注意。


其实,以上这点对你的做题方法几乎没什么用,但是这对你认识新问题非常有用,毕竟授之以鱼不如授之以渔吗

学习动态规划还是需要见多识广的。

方法汇总:不能只看自己书上的题目,而要自己多多刷题,努力总结做题方法。

线性DP

具有线性阶段划分的动态规划算法成为线性动态规划(简称线性DP)。若状态包含多个维度(如二维,三维),则每个维度都是线性划分的阶段,也属于线性DP。(说直白点,就是递归和递推……)

概念有点无聊,直接看算法吧。

例子:走楼梯(低精的)

一个楼梯共有 M M M 级台阶,刚开始,我们站在第 1 1 1 级台阶上,若每次只能走上一级或两级台阶,则走上第 M M M 级台阶共有多少种走法?

输入

第一行: 一个整数 N N N

然后 N N N 行,表示楼梯的级数。

输出:

n n n 行,每行对应一个答案。

分析

在这到题目中,爬楼梯的状态是不会改变的。

我们要定义这么一个东西:

状态 d p [ i ] dp[i] dp[i] 表示第 i i i 层有多少种走法。

决策 : 其实就是状态转移方程 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i1]+dp[i2] 这个方程满足无后效性,所以可以使用动态规划。(就是斐波那契数列)

确定问题的过程:

(1)确定状态 : d p [ i ] dp[i] dp[i]表示走上第 i i i 层台阶有多少种走法。

(2)划分状态 : 台阶的阶数

(3)决策选择 : 走上第 i i i 层台阶之前的状态为站在第 i − 1 i-1 i1 层台阶或第 i − 2 i-2 i2 层台阶上。

(4)边界条件 : d p [ 1 ] = 0 dp[1]=0 dp[1]=0 d p [ 2 ] = 1 dp[2]=1 dp[2]=1 d p [ 3 ] = 2 dp[3]=2 dp[3]=2(本来就在第一阶)

(5)求解目标 : d p [ m ] dp[m] dp[m]

代码实现方法

( 1 ) (1) (1) 递归

时间复杂度 O ( 2 k ) O(2^k) O(2k) 几乎是一个完全树。

( 2 ) (2) (2) 记忆化递归

可以避免重复,不重复求解。

( 3 ) (3) (3) 动态规划

效果等于记忆化搜索

( 4 ) (4) (4) 优化空间复杂度

O ( n ) O(n) O(n) 变为 O ( 1 ) O(1) O(1)

( 5 ) (5) (5) 带上一点点打表

时间大优化!

总体代码:

#include
#include//memset的清零头文件
using namespace std;
const int maxn=40000;//数据范围
int m,T,dp[maxn+5];
int solve_1(int n){//递归容易TLE
	//time∈[920ms,1000ms]
	if(n<=3)return n-1;//递归的结束条件
	return solve_1(n-1)+solve_1(n-2);
}
int solve_2(int n){//记忆化递归,效果等同于动态规划
	//time=15ms
	//加上memset--31ms
	if(dp[n]!=0)return dp[n];
	if(n<=3)return dp[n]=n-1;
	return dp[n]=solve_2(n-1)+solve_2(n-2);
}
int solve_3(int n){//动态规划,15ms
	dp[1]=0;dp[2]=1;dp[3]=2;
	for(int i=4;i<=n;i++)dp[i]=dp[i-1]+dp[i-2];
	return dp[n];
}
int solve_4(int n){//动态规划,15ms
	//空间优化
	if(n<=3)return n-1;
	int s_1=1,s_2=2;
	for(int i=4;i<=n;i++){
		s_2=s_1+s_2;
		s_1=s_2-s_1;
	}
	return s_2;
}
void solve(){//动态规划(打表法)其实好像是提前把每一个数算出来
	//time=1ms到0ms
	dp[1]=0;dp[2]=1;dp[3]=2;
	for(int i=4;i<=maxn;i++)dp[i]=dp[i-1]+dp[i-2];
}
int main(){
	scanf("%d",&T);
	solve();
	for(int i=1;i<=T;i++){
		scanf("%d",&m);
		//printf("%d\n",solve_1(m));
		//memset(dp,0,sizeof(m));
		//如果每次变化,需要初始化为0
		printf("%d\n",solve_2(m));
		printf("%d\n",solve_3(m));
		printf("%d\n",solve_4(m));
		printf("%d\n",dp[m]);
	}
	return 0;
}

延伸思考:

过河卒

动态转移方程:

d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j]=dp[i-1][j]+dp[i][j-1] dp[i][j]=dp[i1][j]+dp[i][j1]

那么,今天就先讲到这里,我们下次再见吧!

大家要持续追更哦。

你可能感兴趣的:(DP算法,算法,c++)