(蓝桥杯C/C++)——动态规划(DP)
目录
一、线性DP
1.DP(动态规划)简介
2.动态规划的分析步骤
3.例题讲解
二、二维DP
1.二维DP简介
2.选数异或
三、最长上升子序列LIS
1.LIS简介
2.例题讲解
四、最长公共子序列LCS
1.最长公共子序列
2.最长公共子序列
2.求出具体子序列
一、线性DP
1.DP(动态规划)简介
DP(动态规划)全称DynamicProgramming,是运筹学的一个分支,是一种将复杂问题分解成很多重叠的子问题,并通过子问题的解得到整个问题的解的算法。
在动态规划中有一些概念:
状态:就是形如dp[il[il=val的取值,其中i,i为下标,也是用于描述、确定状态所需的变量,val为状态值。
状态转移:状态与状态之间的转移关系,一般可以表示为一个数学表达式,转移方向决迭代或递归方向。
最终状态:也就是题目所求的状态,最后的答案。
2.动态规划的分析步骤
1.确定状态,一般为“到第i个为止,xx为j(xx为k)的方案数/最小代价/最大价值”,可以根据数据范围和复杂度来推理。
2.确定状态转移方程,即从已知状态得到新状态的方法,并确保按照这个方向一定可以正确躾渥黨瘛得到最终状态。
根据状态转移的方向来决定使用迭代法还是递归法(记忆化)
3.确定最终状态并输出。
3.例题讲解
破损的楼梯
问题描述
小孟来到了一座高耸的楼梯前,楼梯共有N级台阶,从第0级台阶出发。小孟每次可以迈上1级或2级台阶,但是,楼梯上的a级、第02级、第 a1级,以此奔推。共 M 级台阶的台阶面已经坏了,不能上去。
现在,小孟想要到达楼梯的顶端,也就是第N级台阶,但他不能踩到坏了的台阶上。请问有多少种不踩到坏了的台阶上到达顶端的方案数?
由于方案数很大,请输出其对1e9+7取模的结果
例题分析
设状态dp[i]表示走到第i级台阶的方案数。
状态转移方程为dp[i]=dp[i-1]+dp[i-2],如
果i为破损的,则dp[il=0。
可以用一个桶来记录哪些位置是破损的。
从前往后更新,最后输出dp[n]。
#include<bits/sldc++.h>
using namespace std;
const int N = 1e5+10,mod - 1e9+7;
int n,m,x,f[N],vis[N];
signed main()
{
cin >> n >> m;
for(int i=l;i<=m;i ++)
{
cin >> x;
vis[x]= 1;
}
f[0]= 1;
for(int i - 1;i < -n; i++)
{
if(vis[i])
continue;
fi]-f[i -1+f[i - 2];
f[i] %= mod;
}
cout << f[n] << '\n';
return 0;
}
二、二维DP
1.二维DP简介
二维DP就是指dp数组的维度为二维的dp(当然有时候可能会三维四维,或者存在一些优化使得它降维成一维),广义的来讲就是有多个维度的dp,即用于描述dp状态的变量不止一个。
2.选数异或
给定n个正整数a[i],询问你其中有多少个不同子序列进行异或运算的值为x?
由于结果很大,你需要对998244353取模
异或运算:位远算的一种,符号为由,1^1=0,1^0=1,0^0=0.
子序列: 从切始序列中选出若于个数保持原有顺序的序列,
例题分析
设状态dp[i][j]表示到第i个数字为止(但不一定以第i个数字结尾),异或和为i的子序列个数。
对于第i层的状态,转移的方式有“选第i个”和“不选第i个”两种,转移方程为dp[i][j]= dp[i][j] + dp[i - 1]j ^ dp[i-1[j ^ a]]。
最后结果就是dp[n][x]。
#include<bits/sldc++.h>
using namespace std;
using ll = long long;
const int N = 1e5+9;
const ll p = 998244353;
int a[N],dp[N][70];
int main()
{
int n, x; cin >> n >> x;
dp [0] [0 = 1;
for(int i = 1; i <= n; ++ i)
{
for(int j = 0; j < 64; ++j)
{
dp[i][j] = (dp[i-1][j] + dp[i-1][j ^ a[i]]) % p;
}
}
cout << dp[n][m] << '\n';
return 0;
}
三、最长上升子序列LIS
1.LIS简介
LIS(最长上升子序列)是一个经典的DP模型。
子序列指的是一个序列中,按照原顺序选出若干个不一定连续的元素所组成的序列。这节课我们讲解O(n^2)时间复杂度的朴素LIS模型,LIS还有一种利用二分实现的O(nlogn)时间复杂度的模型,大家可以自行去学习,理解起来略有难度。在求解LIS时,一般我们会设dp[i]表示1~i序列中以a[i]结尾的最长上升子序列的长度,状态转移方程为:
dp[i]= max(dp[j] + 1),
if a[i]> a[j]
表示a[i]要插入到不同子序列后面的情况。
a | 1 | 3 | 4 | 2 | 5 | 3 | 7 | 2 |
dp | 1 | 2 | 3 | 2 | 4 | 3 | 5 | 2 |
2.例题讲解
小明是蓝桥王国的勇士,他晋升为蓝桥骑士,于是决定不断突破自我
这天蓝桥首席骑士长给他安排了 N 个对手,他们的战力值分别为….a1,a2,···an,且按顺序挡在小明的前方,对于这些对手小明可以选择单挑也可以选择群战
作为热血豪放的勇士,小明从不走回头路,且只愿意挑战战力值越来越强的对手
请你算算小明最多会挑战多少名对手
#include<bits/sldc++.h>
using namespace std;
const int N = 1e3+9;
int a[N],dp[N];
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n; ++i)
cin >> a[i];
for(int i = 1;i <= n; ++i)
{
dp[i] = 1;
for(int j =1;j < i; ++j)
{
if(a[i] > a[j])
dp[i] = max(dp[i], dp[j] + 1);
}
}
int ans = 0;
for (int i =1;i < n; ++i)
ans = max(ans, dp[i]);
cout << ans << '\n';
return 0;
}
四、最长公共子序列LCS
1.最长公共子序列
LCS(Longest Common Subsequence 最长公共子序列)是一个经典的DP模型。
这节课我们讲解O(n^2)时间复杂度的LCS模型。
LCS问题是给定两个序列A和B,求它们的最长公共子序列。
在求解LCS时,一般我们会设dp[i][j]表示A[1~i]序列和B[1~j]序列中(不规定结尾)的最长公共子序列的长度,状态转移方程为:
if ali]=b[j]:dp{i][j]= dp{i-1][j-1]+1
else dp[i][j]= max(dp[i-1][jl, dp[i][j-1]);
解释一下:当ali]=b[j]时,可以将他们作为插入到LCS的后面,使得长度变长1,当a[i]!=b[说明此时LCS不会延长,那就要从dp[i-1][j]和dp[i][j-1]中取大的作为最长的长度。
a | 1 | 3 | 4 | 2 | 5 |
b | 1 | 4 | 3 | 5 | 2 |
dp | 1 | 2 | 3 | 4 | 5 |
1 | 1 | 1 | 1 | 1 | |
2 | 1 | 1 | 2 | 2 | 2 |
3 | 1 | 2 | 2 | 2 | 2 |
4 | 1 | 2 | 2 | 2 | 3 |
5 | 1 | 2 | 2 | 3 | 3 |
1 | 3 | 2 |
2.最长公共子序列
给定一个长度为 N 数组 a 和一个长度为 M 的数组 b
请你求出它们的最长公共子序列长度为多少。
例题分析
设dp[i][j]表示序列a[1~i],b[1~j]中最长公共子序列长度,
状态转移方程为:
if ali]=b[j]:dp[i][j] = dp[i-1][j-1]+1
else: dp|i][j]= max(dp[i-1][j], dp[i][j-1]);最后输出dpln]lm]即可。
#include <bits/stdc+.h>
using namespace std;
const int N = 1e3+9,
int n, m,a[N], b[N], dp[M][N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1;i <= n; ++i)
cin >> a[i];
for(int i= 1; j<= m; ++i)
cin >> b[i];
for(int i = 1;i <= n; ++i)
for(int j = 1; j <= n; ++j)
{
if(a[i] == b[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j],dp[i ][j - 1]);
}
cout << dp[n][m] << '\n';
return 0;
}
2.求出具体子序列
a | 1 | 3 | 4 | 2 | 5 |
b | 1 | 4 | 3 | 5 | 2 |
dp | 1 | 2 | 3 | 4 | 5 |
1 | 1 | 1 | 1 | 1 | |
2 | 1 | 1 | 2 | 2 | 2 |
3 | 1 | 2 | 2 | 2 | 2 |
4 | 1 | 2 | 2 | 2 | 3 |
5 | 1 | 2 | 2 | 3 | 3 |
1 | 3 | 2 |
(i,j)从(n,m)往回走,当a[i]=b[j]时,往左上角走变为(i-1,j-1),此时找到了一个公共元素a[i](或blj]),否则往左边或上边走(选择较大的方向)。
走到新位置后重复以上判断,直到走出边界
#include <bits/stdc+.h>
using namespace std;
using ll = long long;
const ll p= 1e9 + 7;
const int int = 1e9, N = 1e3 + 9;
int a[N], b[N], dp[N][N];
int main()
{
int n, m;
cin >> n >>m;
for(int i = 1; i <= n; ++i)
cin >> a[i];
for(int i = 1; i <= m; ++i)
cin >> b[i];
for(int i = 1; i <= n; ++i)
{
for(int j= 1; j<= m; ++ j)
{
if(a[i] == b[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i][j-1],dp[i - 1][j]);
}
}
vector<int> v;
int x = n, y = n;
while(x && y)
{
if(a[x] == b[y])
{
v.push_back(a[x]);
x --, y--;
}
else (dp[x - 1][y] > dp[x][y - 1])
x --;
else y--;
}
reverse(v.begin(),v.end());
for(const auto &i : v)
cout << i << ' ';
return 0;
}