动态规划

写在前面:

如果您没有接触过DP,想要学习一下的话,建议您往下看

如果说您学过,只是想过来复习一下,找一些遗忘知识点,那么建议您跳转到Iris_鸢桃吻月&动态规划(DP)&(满满的干货)

当然,只是个建议,怎么选择在于您

进入正题

动态规划

动态规划(DP)不是某种算法,而是一种思想

核心在于:把大问题转化成小问题,利用小问题的解推断出大问题的解

举个栗子:

【例1】//递推

走楼梯

问题描述:今有\(n\)级台阶,初始时站在0级,每次可以向上走1级或2级。问总方案数是多少?

暴力模拟:指数级复杂度 可怕

考虑更优的算法。如果以f[x]表示“从0级走到\(x\)级的方案数”,假设f[1],f[2]...f[n-1]全部已知,如何利用这些信息推出f[n]

走到f[n],要么是从\(n-1\)级走上来的,要么是从\(n-2\)级走上来的。

依据加法原理

\[f[n]=f[n-1]+f[n-2] \]

这就是这个问题的递推式

【例2】//DP

硬币问题

问题描述:今天给你手上有无限的面值为1,5,11元的硬币。给定\(n\),问:至少用多少枚硬币,可以恰好凑出\(n\)元?

例

n=15时,答案是3,构造方法是5+5+5

n=12时,答案是2,构造方法是11+1

f[x]记录“凑出\(x\)元所需要的硬币数”。那么答案歉然就是f[n],如何求出\(f\)数组呢?f[x]等于什么?

提示:

注意思考“f[x]从哪里来”

考虑一个具体的例子:凑出15元

为了凑出15元,我们最开始的时候,可以使用哪枚硬币?

  • 假设用了1元硬币,那么接下来要凑出14元。共1+4=5枚

  • 假设用了5元硬币,那么接下来要凑出10元。共1+2=3枚

  • 假设用了11元硬币,那么接下来要凑出4元。共1+4=5枚

这三种方案,当然是选代价最低的,所以我们在这一次决策中,选择了5元硬币

现在再看来,f[x]是“凑出\(x\)需要的硬币数”,它等于什么?

可供选择的决策方案如下:

  • 先用一个1元硬币,代价1+f[x-1]

  • 先用一个5元硬币,代价1+f[x-5]

  • 先用一个11元硬币,代价1+f[x-11]

再上述方案里面,选择代价最低的就行!

所以有

\(f[x]=min\begin{cases}1+f[x-1]\\1+f[x-5]\\1+f[x-11]\end{cases}\)

相当于

\(f[x]=1+min\begin{cases}f[x-1]\\f[x-5]\\f[x-11]\end{cases}\)

在这里说一个问题

x=10f[x-11]=f[-1],这是不合法的

所以我们就要在得到结果之后判断是否合法

如果不合法

  1. 不进行计算

  2. 将这个不合法的值设置一个值,具体见下

INF一般取为0x3f

即INF=0x3f

当题目要求取min的时候,取INF

当题目要求取max的时候,取-INF

不在乎的话直接设置成0

而且硬币问题有min这个比较函数,这也就是DP与递推最明显的一个特征

走楼梯就是一个非常纯的递推,而硬币问题就是一个DP问题

初步总结:状态

我们用 “大事化小,小事化了” 的思想,解决了上楼梯问题和硬币问题。

大事能转化成小事,是因为大事和小事都有一样的形式:

  • 上楼梯问题

    大问题:爬上n级有多少种方案

    小问题:爬上n-1级有多少种方案、爬上n-2级有多少种方案

    它们都是 “爬上xx级有多少种方案” 这一类问题

  • 硬币问题

    大问题:凑出n元钱的最少硬币数

    小问题:凑出n-1,n-5,n-11元的最少硬币数

    它们都是 “凑出xx元钱所需要最少硬币数” 这一类问题

可见,只有大问题和小问题拥有相同的形式,才能考虑大事化小。

如果满足这个要求,那么我们遇到的每个问题,都可以很纯洁地表达。

我们可以把可能遇到的每种 “局面” 成为状态


例

硬币问题中,要表达“我们需要凑出n元钱”这个局面,可以设计状态;“f[x]表示凑出x元用的最少硬币数”

上楼梯问题中,设计状态:“f[x]表示走上x级的方案数”

设计完状态之后,只要能 利用小状态的解求出大状态的解 ,就可以动手把题目做出来!

【例3】

LIS问题

问题描述:数组的“最长上升子序列”是指:最长的那一个单调上升的子序列。(或者说你可以理解为:去取出数列中某些数,可以不连续,保持原有顺序)

例如:数组a:[1,3,4,2,7,6,8,5]的最长上升子序列是1,3,4,7,8

如何求数组的最长上升子序列的长度

分析

想用大事化小来完成这道题,必须先设计状态。

如何设计状态,来完整地描述当前遇到的局面?

设计状态

f[x]表示“以a[x]结尾的上升子序列,最长有多长”

那么答案就是f[1],f[2]...f[n]里面的最大值。

问题来了,如何求出\(f\)数组?

提示:思考f[x]从哪里来

f[x]表达的是“以a[x]结尾的上升子序列长度”。

这个最长的子序列,一定是把a[x]接在某个上升子序列尾部形成的

例

数组a:[1,3,4,7,6,8,5]

考虑f[8],它的来源有:

   自己的一个元素作为一个序列。长度为1
    
   接在a[1]后面。长度为f[1]+1=2
   
   接在a[2]后面。长度为f[2]+1=3
   
   接在a[3]后面。长度为f[3]+1=4
   
   接在a[5]后面。长度为f[5]+1=3

此时,稍有常识的人都会看出,要得到f[x],只需要看a[x]能接在那些书的后面。

也就是

\[f[x]=\max\limits_{p<x,a[p]<a[x]}\left\{f[p]+1\right\} \]

其中p<x,a[p]<a[x]的含义是:枚举在\(x\)前面的,a[p]又比a[x]小的那些\(p\),因为a[x]可以接到这些数的后面,形成一个更长的上升子序列

初步总结:转移(核心)

在前面的三个立体种,我们都是先设计好状态,然后给出了一套用小状态推出的大状态的方法

从一个状态的解,得知另一个状态的解,我们称之为 “状态转移”

这个转移的式子成为 “状态转移方程”

硬币问题中,状态转移方程是:

\(f[x]=1+min\begin{cases}f[x-1]\\f[x-5]\\f[x-11]\end{cases}\)

LIS问题中,状态转移方程是:

\[f[x]=\max\limits_{p<x,a[p]<a[x]}\left\{f[p]+1\right\} \]

小结:状态和转移

总结刚刚学习的内容。

如果我们想用大事化小的思想解决一个问题,我们需要:

  1. 设计状态。把面临的每一个问题,用状态表达出来

  2. 设计转移。写出状态转移方程,从而利用小问题的解推出大问题的解

前三个问题中,我们设计转移的时候,考虑的都是“这个局面是从哪里过来的”

这是一种常见的思路:当前状态未知。需要用已经解决的状态,来推出当前的状态的解

DP还有另一种设计转移的思路:当前状态的解已知。需要利用这个解,去更新它能走到的状态。

这两种思路,一种是考虑“我从哪里来”,一种是考虑“我到哪里去”。

这两种手段都是能解决问题的!

上楼梯问题再讨论

如何用“我到哪里去”的转移手段,结局上楼的问题?

\[f[x]→f[x+1] \]

\[→f[x+2] \]

那如何用“我到哪里去”的转移手段,解决硬币问题?

\[f[x]→f[x+1] \]

\[→f[x+5] \]

\[→f[x+11] \]

其中f[x]初始值为INF

           原值   更新值
          
f[x+1]=min(f[x+1],f[x]+1)

f[x+5]=min(f[x+5],f[x]+1)

f[x+11]=min(f[x+11],f[x]+1)

那又如何用“我到哪里去”的转移手段,解决LIS问题?

\[f[x]→f[p] \]

其中\(p>x,a[p]>a[x]\)

\[f[x]=\max\left\{f[p],f[x]+1\right\} \]

f[p]的初始值为-INF

小结:设计转移

设计转移有两种方法:

  • pull型(我从哪里来):对于一个没有求出解的状态,利用能走到它的状态,来得出它的解。

  • push型(我到哪里去):对于一个已经求好了的状态,拿去更新它能走到的状态

(前方高能……)

DP三连

综上所述,如果你想用DP解决一个问题,要干的事情可以总结为DP三连

  1. 我是谁?(如何设计状态)

  2. 设计转移(二选一)

  • 我从哪里来?(pull型转移)

  • 我到哪里去?(push型转移)

  1. 开始和最后怎么办,初始未确定的值,初值是什么

  2. 如何处理特判

————————————斐波那契数列————————————

众所周知,斐波那契数列是

\[F[1]=1 \]

\[F[2]=1 \]

\[F[n]=F[n-1]+F[n-2] \]

假设严格按照定义,写一个递归的代码,复杂度是什么情况?

不用说,肯定会炸

朴素代码

int fib(int n)
{
	if(n==1||n==2) return 1;
	return fib(n-2)+fib(n-1);
}

我们遇到的最大的麻烦,是很多fib值被重新计算了

假设现在在计算fib(7),明明fib(5)只需要计算一次就可以;

但是fib(7)要调用fib(6)fib(5),fib(6)要调用fib(5),所以fib(5)莫名其妙被调用了两次

如何避免这种情况???

答案就是 记 忆 化

调用fun(x)时:

  • 如果fun(x)没有被计算过,则计算fun(x),并存储到mem[x]

  • 如果fun(x)被计算过,则直接返回mem[x]

记忆化搜索的复杂度是多少呢??

等于直接递推

好好用

小结:记忆化搜索

  • 按顺序递推记忆化搜索,是DP的两种高效实现方式

  • 记忆化搜索一般配套“我从哪里来”的转移方式

记忆化搜索的优势

  • 如果转移顺序不太好确定,则记忆化搜索可以帮你省一堆事

  • 有时候,记忆化搜索更节省时间。空间。因为不可能达到的状态是不会被搜索到的

小结

决策类的DP,设计状态时,需要满足两个原则:

  • 最有子结构

    大问题的最优解,一定是从小问题的最优解推出来的

    例如硬币问题,凑出\(15\)元的最有解是有凑出\(4\)\(10\)\(14\)元的最优解(而不是最坏解)而来

  • 无后效性

    现在的决策,只和过去的结果有关,而和过去的决策无关

    例如硬币问题(它的出镜率好高哦

    要在n=15时做出决策有关,只需要知道f(4)f(10)f(14)的值,而并不关心这些值是怎么来的

—————————————\(01\)背包问题—————————————

DP三连

  • 我是谁?

    设计状态:dp[k][m]表示“只考虑钱k个物品,用m的容量能装下的最大价值”

  • 我从哪里来?

    dp[k][m]的来历:要么取了第k个物品,要么没有取。因此

    \(dp[k][m]=max\begin{cases}dp[k-1][m]\\dp[k-1][m-v[k]]+w[k]\end{cases}\)

第一种情况表示没取,第二种情况表示取了

\(Code\)

for(int k=1;k<=n;k++)
	for(int m=0;m<=V;m++)
	{
		dp[k][m]=dp[k-1][m];//没取
		if(m-v[k]>=0)
			dp[k][m]=max(dp[k][m],dp[k-1][m-v[k]]+w[k]);//取了
	}
  • \(01\)背包的空间优化

回顾\(01\)背包的状态转移方程

\(dp[k][m]=max\begin{cases}dp[k-1][m]\\dp[k-1][m-v[k]]+w[k]\end{cases}\)

我们注意到,dp[k][]只与dp[k-1][]有关,从而dp[][]数组的第\(1\)行、第\(2\)行……第\(k-2\)行全部都被浪费了。

占据了空间,但以后不会被访问到。

下面是优化过后的代码

for(int k=1;k<=n;k++)
	for(int m=V;m>=0;m--)//注意!!!
		if(m-v[k]>=0)
			dp[m]=max(dp[m],dp[m-v[k]]+w[k]);

在第二层的循环中,我们是按照倒着的顺序进行访问的

因为只与上一层有关,所以我们只调用上一层的值

因为m-v[k]>=0

所以m>=v[k]

所以dp[m-v[k]]一定在dp[m]之前

为了防止前面的已经被覆盖,所以我们采用倒着访问的顺序

\(Over\)

跪求资瓷QAQ

posted @ 2021-08-15 20:53  晨曦时雨  阅读(148)  评论(0)    收藏  举报
-->