这里是KK爱Coding 团队的paoxiaomo,一个现役ACMer,之后将会持续更新算法笔记系列以及笔试题题解系列
本文章面向想打ICPC/蓝桥杯/天梯赛等程序设计竞赛,以及各个大厂笔试的选手
各大大厂近年笔试题汇总以及题解合集可以私信 KK爱Coding
感谢大家的订阅➕ 和 喜欢💗
动态规划是一种解决多阶段决策过程最优化问题的数学方法。不论是信息学竞赛还是大厂面试题都是常客,今天就来讲解一下主要的做题步骤,并通过一些例题来帮助各位理解。
-
理解问题:首先,要充分理解问题的要求和限制条件。明确问题是什么,需要求解的是什么,以及问题的约束条件是什么。
-
定义状态:动态规划的核心在于定义合适的状态。状态应该包含问题的关键信息,并且能够刻画问题的特征。通常情况下,状态可以是一个或多个变量的组合。
-
找到状态转移方程:一旦定义了状态,接下来就是确定状态之间的转移关系,即状态转移方程。状态转移方程描述了问题从一个状态转移到下一个状态的过程,并且可以通过递推的方式求解最终的解。
-
初始化:在开始状态转移之前,需要初始化一些状态。这些初始化的状态通常是最简单、最基础的状态,为后续的状态转移提供基础。
-
状态转移:根据状态转移方程,通过递推或迭代的方式求解出所有状态的值。这个过程可以采用自底向上的方法,从最简单的状态开始逐步求解,也可以采用自顶向下的方法,从问题的初始状态开始递归地求解。
-
求解目标状态:一旦所有状态的值都求解出来了,就可以根据问题的要求找到目标状态的值,即最终的解。
-
分析复杂度:最后,要分析算法的时间复杂度和空间复杂度,确保算法在实际应用中的可行性。
直接看这些步骤可能会不太明白,所以我们可以先通过几道例题来进行讲解。
首先就是最经典的背包问题:
01背包问题
给定一个背包,其容量为m,以及一组物品,每个物品有两个属性:重量(weight)和价值(value)。现在需要从这些物品中选择一些放入背包中,使得放入背包的物品总重量不超过背包容量,同时使得放入背包的物品总价值最大。每个物品只能选择放入一次(即要么放入背包,要么不放入)。
01背包问题解题过程:
-
定义状态:定义一个二维数组dp[i][j],其中dp[i][j]表示在前i个物品中选择,且背包容量为j时,能够获得的最大价值。
-
找到状态转移方程:对于每个物品i,考虑两种情况:
- 如果不放入第i个物品,则dp[i][j] = dp[i-1][j]。
- 如果放入第i个物品,则dp[i][j] = dp[i-1][j-weight[i]] + value[i]。 最终,取这两种情况中的最大值作为dp[i][j]的值。
-
初始化:将dp数组初始化为0,即dp[0][j] = 0和dp[i][0] = 0。
-
状态转移:根据状态转移方程,从第一个物品开始逐步计算dp数组的值,直到计算出dp[n][m],其中n表示物品的数量。
-
求解目标状态:由于背包可以不放满,所以dp[n][m]不一定为最优解,我们直接遍历整个数组dp[n]寻找最大值即为最优解。
-
分析复杂度:动态规划算法的时间复杂度为O(nm),其中n为物品的数量,m为背包的容量。
参考代码(cpp)
int n,m;cin>>n>>m;
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for(int i=1;i<=n;i++){
int weight,value;
cin>>weight>>value;
for(int j=m;j>=0;j--){
if(j<weight){//无法放入背包,则此时什么都干不了
dp[i][j]=dp[i-1][j];
}else{//如果能放入背包,则有两个选择,即两个转移式,要么不放,要么找一个正好比当前枚举到的状态小weight的背包放入
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight]+value);
}
}
}int ans=0;
for(int i=0;i<=m;i++)ans=max(ans,dp[n][i]);
cout<<ans<<endl;
还有一个最适合理解动态规划的题就是最长公共子序列:
最长公共子序列(LCS)
-
定义状态:我们可以定义一个二维数组dp[i][j],其中dp[i][j]表示字符串s1的前i个字符与字符串s2的前j个字符的最长公共子序列的长度。
-
找到状态转移方程:对于字符串s1的第i个字符和字符串s2的第j个字符:
- 如果两个字符相等,则dp[i][j] = dp[i-1][j-1] + 1。
- 如果两个字符不相等,则dp[i][j] = max(dp[i-1][j], dp[i][j-1])。 最终,dp[m][n]即为问题的解,其中m为字符串s1的长度,n为字符串s2的长度。
-
初始化:将dp数组初始化为0,即dp[i][0] = 0和dp[0][j] = 0。
-
状态转移:根据状态转移方程,从第一个字符开始逐步计算dp数组的值,直到计算出dp[m][n],其中m和n分别为字符串s1和s2的长度。
-
求解目标状态:dp[m][n]即为问题的解,表示字符串s1和s2的最长公共子序列的长度。
-
分析复杂度:动态规划算法的时间复杂度为O(mn),其中m和n分别为字符串s1和s2的长度。
string s1,s2;
cin>>s1>>s2;
int n=s1.size(),m=s2.size();
// 创建动态规划数组,dp[i][j]表示s1的前i个字符与s2的前j个字符的最长公共子序列的长度
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (s1[i - 1] == s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
cout<<dp[m][n]<<endl;
最后还有个经典的一维动态规划问题:
青蛙跳台阶
有一只青蛙,它每次可以跳上1级台阶或2级台阶。如果台阶数为n,求青蛙跳上这个n级台阶总共有多少种跳法。
-
定义状态:定义一个一维数组dp[i],其中dp[i]表示跳上第i级台阶的总跳法。
-
找到状态转移方程:青蛙跳上第i级台阶可以从第i-1级台阶跳上来,也可以从第i-2级台阶跳上来。因此,状态转移方程为:dp[i] = dp[i-1] + dp[i-2]。
-
初始化:由于青蛙跳上1级台阶只有一种跳法(跳1级),跳上2级台阶有两种跳法(跳2次1级或一次跳2级),所以可以初始化dp[1] = 1和dp[2] = 2。
-
状态转移:根据状态转移方程,从第3级台阶开始逐步计算dp数组的值,直到计算出dp[n],其中n为台阶的总数。
-
求解目标状态:dp[n]即为问题的解,表示青蛙跳上n级台阶的总跳法。
-
分析复杂度:动态规划算法的时间复杂度为O(n),其中n为台阶的总数。
int n;cin>>n;
if(n<3){
cout<<n<<endl;exit(0);
}
vector<int> dp(n + 1, 0);
// 初始化前两级台阶的跳法
dp[1] = 1;
dp[2] = 2;
// 计算跳上第i级台阶的总跳法
for (int i = 3; i <= n; ++i) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 返回跳上n级台阶的总跳法
cout<<dp[n]<<endl;
不积硅步,无以至千里,只有学好了这些简单的动态规划入门题,才有机会在比赛/笔试中解决难题。