总之,进入了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。(说直白点,就是递归和递推……)
概念有点无聊,直接看算法吧。
一个楼梯共有 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[i−1]+dp[i−2] 这个方程满足无后效性,所以可以使用动态规划。(就是斐波那契数列)
确定问题的过程:
(1)确定状态 : d p [ i ] dp[i] dp[i]表示走上第 i i i 层台阶有多少种走法。
(2)划分状态 : 台阶的阶数
(3)决策选择 : 走上第 i i i 层台阶之前的状态为站在第 i − 1 i-1 i−1 层台阶或第 i − 2 i-2 i−2 层台阶上。
(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[i−1][j]+dp[i][j−1]
那么,今天就先讲到这里,我们下次再见吧!
大家要持续追更哦。