🍨🍨🍨这一章,我们来看一些常见的动态规划题型,包括递推求解、最大子段和、最长上升子序列(LIS)、最长公共子序列(LCS)、背包类问题、记忆化搜索、字符串相关的动态规划等内容。希望能帮助大家更好的掌握计算机考研机试中所涉及到的动态规划问题。加油!( •̀ ω •́ )✧
目录
🧊🧊🧊8.1 递推求解
首先,什么是动态规划呢?
动态规划就是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或叫分治)的方式去解决。其实就是分解问题,分而治之。
说一个我们众所周知的例子——爬楼梯上楼问题:N 阶楼梯上楼,一次可以走两阶或一阶,问有多少种上楼方式。
按照动态规划思想,我们来分析下问题:每次都有两种跳法,分别是走一阶或两阶。如果当前是第 n 个台阶,那么跳法就是第(n-1)台阶的跳法数加上第(n-2)台阶的跳法数。如果换成公式是 F(n) = F(n-1)+F(n-2)。F(n)代表第 n 阶台阶的跳法的数量。
这就相当于找到了一个递推公式,然后来进行计算。
🥥例题:DreamJudge 1413
#include <stdio.h>
int main() {
int i,N;
long long a[90];
while(scanf("%d",&N) != EOF) {
a[1]=1;
a[2]=2;
for(i=3;i<=N;i++)
a[i]=a[i-1]+a[i-2];
printf("%lld\n",a[N]);
}
return 0;
}
🥥练习题目:
DreamJudge 1197 吃糖果
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
while(cin>>n)
{
int dp[50]={0};
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++) dp[i]=dp[i-1]+dp[i-2];
cout<<dp[n]<<endl;
}
return 0;
}
DreamJudge 1033 细菌的繁殖
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,day,cnt=1;
cin>>n;
int dp[1010]={0};
dp[0]=dp[1]=1;
for(int i=2;i<=1005;i++)
{
dp[i]=dp[i-1]+4*cnt;
cnt++;
}
while(n--)
{
cin>>day;
cout<<dp[day]<<endl;
}
return 0;
}
DreamJudge 1726 不连续 1 的子串数量
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
int dp[100][2]={0};
cin>>n;
dp[1][0]=1;//第1位是0
dp[1][1]=1;//第1位是1
for(int i=2;i<=n;i++)
{
dp[i][1]+=dp[i-1][0];//当前位为1,那么前一位得是0才能不连续
dp[i][0]+=dp[i-1][1]+dp[i-1][0];//当前位为0,那么前一位可以是0也可以是1
}
cout<<dp[n][1]+dp[n][0]<<endl;
return 0;
}
🧊🧊🧊8.2 最大子段和
🥥题型总结:
1、直接求最大子段和的值
2、要求记录最大子段值的起始坐标和终止坐标或者起始值和终止值
3、首尾可以连接成环的最大子段和的值
🥥例题:DreamJudge 1172
由于 N 很大,所以我们不能暴力的枚举起点和终点,使用动态规划的思想,我们发现一个特点,即我们只需要从一个正数开始不断的累加,然后更新其中的最大值即可。
#include <bits/stdc++.h>
using namespace std;
int dp[1000010];
int a[1000010];
long long maxx;
int main() {
int n ;
while(cin >> n){
for(int i = 0; i < n; i++)
cin >> a[i];
dp[0] = a[0];
maxx = a[0];//最小的情况就是不选那么答案就是 0
for(int i = 1; i < n; i++){
dp[i] = max(dp[i-1] + a[i], a[i]);
if(maxx < dp[i]) {//如果累加到更大的值则更新
maxx = dp[i];
}
}
cout << maxx << endl;
}
return 0;
}
接下来看一道进阶应用例题:
🥥例题:DreamJudge 1642
首先,看到题目的数据范围,第一反应就是这个题只能用 O(n)时间复杂度的算法。
那么可供我们选择的就不多了。一种方式是深挖题目,可以发现其实题目就是让我找出一段 0 比 1 的个数多的最多的区间,我们可以用满分篇中讲到的毛毛虫算法来进行不断蠕动解决。那么现在我们有没有别的解决方法呢?和本节讲到的最大子段和有什么关系吗?
我们把 01 字符串的 0 变成 1,1 变成-1,然后构成一个 1 和-1 的字符串。对这样一个字符串去求它的最大子段和即可,最后再把 1 的个数加上就是最终的答案。
#include <stdio.h>
#include <string.h>
int dp[10000005];
int a[10000005];
char s[10000005];
int _max(int x, int y) {
return x > y ? x : y;
}
int main() {
int n;
while (scanf("%d", &n) != EOF) {
scanf("%s", s);
for (int i = 0; i < n; i++) {
if (s[i] == '0') a[i] = 1;
else a[i] = -1;
}
memset(dp, 0, sizeof(dp));
dp[0] = a[0];
int maxx = 0;//可以为空
for (int i = 0; i < n; i++) {
dp[i] = _max(dp[i - 1] + a[i], a[i]);
if (maxx < dp[i]) maxx = dp[i];
}
int ans = 0;//统计 1 的个数
for (int i = 0; i < n; i++)
if (s[i] == '1') ans++;
printf("%d\n", maxx + ans);
}
return 0;
}
🥥练习题目:
DreamJudge 1334 最大连续子序列 🍰
输入样例:
6 -2 11 -4 13 -5 -2 10 -10 1 2 3 4 -5 -23 3 7 -21 6 5 -8 3 2 5 0 1 10 3 -1 -5 -2 3 -1 0 -2 0
//摘自N诺用户:为欢几何
#include<bits/stdc++.h>
using namespace std;
long long dp[1000010];
long long a[1000010];
long long maxm;//记录区间最大值
long long s=0,d=0;//记录区间始末位置
long long temp_start=0;
int main(){
long long n;
while(cin>>n){
if(n==0) break;
int sum=0;
for(long long i=0;i<n;i++){
cin>>a[i];
if(a[i]<0) sum++;
}
if(n==1){
cout<<a[0]<<" "<<a[0]<<" "<<a[0]<<endl;
}else if(sum==n){
cout<<"0 "<<a[0]<<" "<<a[n-1]<<endl;
}else{
dp[0]=a[0];
maxm=dp[0];//最小的情况非空,那么就是某一个值
s=1;
d=1;
for(long long i=1;i<n;i++){
if(dp[i-1]+a[i]<a[i]){
dp[i]=a[i];
temp_start=i;
}else{
dp[i]=dp[i-1]+a[i];
}
if(dp[i]>maxm){
maxm=dp[i];
s=temp_start;
d=i;
}
}
cout<<maxm<<" "<<a[s]<<" "<<a[d]<<endl;
}
}
return 0;
}
DreamJudge 1703 最大子串和
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
int a[110],dp[110];
int s,d;
while(cin>>n){
for(int i=0;i<n;i++) cin>>a[i];
dp[0]=a[0];
for(int i=1;i<n;i++) dp[i]=max(dp[i-1]+a[i],a[i]);//如果加上我比我自己更大,那么选我入区间是对的选择
int temp=dp[0];
for(int i=1;i<n;i++){//找到dp最大的位置,区间结尾就是这里
if(temp<dp[i]){
temp=dp[i];
d=i;
}
}
s=d;
int ans=temp;
while(true){//向前找起点
temp-=a[s];
if(temp==0) break;//起点
s--;
}
for(int i=s;i<=d;i++){
cout<<a[i]<<" ";
}
cout<<endl<<ans<<endl;
}
return 0;
}
🧊🧊🧊8.3 最长上升子序列(LIS)
最长上升子序列(Longest Increasing Subsequence, 简称 LIS)是 dp 中比较经典的一个算法模型,它有一种朴素的算法 O(n^2)和一种优化版的算法 O(nlogn)实现, 通过它, 我们可以进一步了解 dp 的思想。
我们先来看 O(n^2)的算法 LIS_nn() :
首先确定状态转移方程 dp[i]代表以第 i 项为结尾的 LIS 的长度:
dp[i] = max(dp[i], max(dp[j]) + 1) if j < i and a[j] < a[i]
根据上面的状态转移方程可以写出下面的代码:
int LIS_nn() {
int ans = 0;
for (int i = 1; i <= n; ++i) {
dp[i] = 1;
for (int j = 1; j < i; ++j) {
if (a[j] < a[i]) { //要满足上升的条件
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]);
}
return ans;
}
刚才的算法是否还有优化的空间呢?
在刚才的内层 for 我们从前往后找一个最大的 LIS 值,仔细想一下是否可以发现这个值一定是单调递增的呢? 由于这个值是单调递增的,所以我们就没必要使用从前往后遍历的方法,可以使用二分查找来
优化这个寻找的过程。
于是可以实现O(nlogn)算法的LIS_nlgn()函数:
int LIS_nlgn() {
int len = 1;
dp[1] = a[1];
for (int i = 2; i <= n; ++i) {
if (a[i] > dp[len]) {
dp[++len] = a[i];
}
else {
int pos = lower_bound(dp, dp + len, a[i]) - dp;
dp[pos] = a[i];
}
}
return len;
}
上面的代码是求最长上升子序列的长度,也可以求最长上升子序列的累加值。
🥥例题:DreamJudge 1257
将上面的求长度的 LIS 模板稍作修改即可得到求累加和的 LIS。
#include <bits/stdc++.h>
using namespace std;
int dp[1001], a[1001], n;
int LIS_nn() {
int ans = 0;
for (int i = 1; i <= n; ++i) {
dp[i] = a[i];
for (int j = 1; j < i; ++j) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + a[i]);
}
}
ans = max(ans, dp[i]);
}
return ans;
}
int main() {
while (cin >> n) {
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
cout << LIS_nn() << endl;
}
return 0;
}
🥥练习题目:
DreamJudge 1256 拦截导弹 🍰
//摘自N诺用户:Happy0111
#include<bits/stdc++.h>
using namespace std;
const int maxn=30;
long long a[maxn];
long long dp[maxn];
int k;
int LIS(){
int len=1;
dp[len]=a[1];
for(int i=2;i<=k;i++){
if(dp[len]<=a[i]){
dp[++len]=a[i];
}else{
int pos=upper_bound(dp+1,dp+len,a[i])-dp;
dp[pos]=a[i];
}
}
return len;
}
int main(){
while(scanf("%d",&k)!=EOF){
if(k==0){
printf("0\n");
continue;
}
for(int i=1;i<=k;i++){
scanf("%lld",&a[i]);
a[i]=-a[i];
}
int ans=LIS();
printf("%d\n",ans);
}
}
DreamJudge 1253 合唱队形 🍰
//摘自N诺用户:fxl
#include <bits/stdc++.h>
using namespace std;
int n,a[105],dp_h[105],dp_t[105];
int main(){
while(cin>>n){
for(int i=0;i<n;i++){
cin>>a[i];
}
//从前往后找以a[i]结尾的最长上升子序列,存入dp_h
for(int i=0;i<n;i++){
dp_h[i]=1;
for(int j=0;j<i;j++){
if(a[j]<a[i]){
dp_h[i] = max(dp_h[j]+1,dp_h[i]);
}
}
}
//从后往前找以a[i]结尾的最长上升子序列,存入dp_t
for(int i=n-1;i>=0;i--){
dp_t[i]=1;
for(int j=i+1;j<n;j++){
if(a[j]<a[i]){
dp_t[i] = max(dp_t[j]+1,dp_t[i]);
}
}
}
int maxn=0;
for(int i=0;i<n;i++){
maxn=max(maxn,dp_h[i]+dp_t[i]);
}
maxn=maxn-1;//a[i]前后都被计算了,多算了一次所以要减1
cout<<n-maxn<<endl;
}
return 0;
}
DreamJudge 1836 最长递减子序列 🍰
用deque单调队列暴力只能过20%的数据
#include<bits/stdc++.h>
using namespace std;
int main() {
int n,m,maxlen=1;
int a[105],b[105],dp[105];//a记录原始数据,b记录最长递减序列,dp记录到当前位置的最长递减序列长度
cin>>n;
for(int i=1;i<=n;i++){//输入数据
cin>>a[i];
dp[i]=1;
}
for(int i=1;i<=n;i++){//更新dp
for(int j=i;j>0;j--)
if(a[j]>a[i]){//如果我的前边有人比我大,那么我就可以放到那个数的后边(+1的由来,1是我)
dp[i]=max(dp[j]+1,dp[i]);
}
maxlen=max(maxlen,dp[i]);//每次到一个新位置就更新一次
}
m=maxlen;
memset(b,0,sizeof(b));
for(int i=n;i>0;i--){//倒着遍历一遍,找到各个数并放入b数组
if(dp[i]==m) b[m--]=a[i];
for(int j=i+1;j<=n;j++){
if(dp[i]==dp[j]&&a[i]>b[dp[i]+1])//?没看懂
b[dp[i]]=a[i];
}
}
for(int i=1;i<=maxlen;i++) cout<<b[i]<<" ";//输出最长递减序列
cout<<endl;
return 0;
}
这个代码里有个没看懂的点,有没有懂的uu帮忙解答一下,谢谢!🌹🌹🌹
创作不易,点个赞吧~点赞收藏不迷路,感兴趣的宝子们欢迎关注该专栏~
勤奋努力的宝子们,学习辛苦了!宝子们可以收藏起来慢慢学哦~🌷🌷🌷休息下,我们下部分再见👋( •̀ ω •́ )✧~