动态规划初步讲解

前言

本篇水量丰富的Blog会持续更新(maybe)

正题

GXY大佬告诉我,DP入门先学线型DP

然后我乖乖去学了

\(\mathcal{First.1\ }\) 最长不上升子序列

基本求法

最长不上升子序列的定义就是给定我们一个序列,求里面不连续的一串数 (满足不上升,也就是后一个不比前一个大,也就是小于等于的关系) 的最长长度

比如对于

7 5 6 7 2 3 2 1

这串数字来说,它的最长不上升子序列为

7 6 3 2 1

or

7 5 3 2 1

我们建立一个 f[i] 的数组,表示以i开头的最长不上升子序列的长度

然后我们可以得到

7 5 6 7 2 3 2 1
f[1]=5 f[2]=4 f[3]=4 f[4]=4 f[5]=3 f[6]=3 f[7]=2 f[8]=1

但是这样然并卵,因为其中没有规律()

但是(怎么又是但是)我们发现,以每个数为起点的最长不上升子序列的长度至少为 \(1\) !(废话)

所以我们得到了 f 数组初始化的方式

for(register int i=1; i<=n;i++)
{
   	f[i]=1;
}

我们考虑一下怎么求以每个数为起点的最长不上升子序列,首先肯定得对每个数都进行一次查找

所以最外层循环为

 for(register int i=n;i>=1;i--)//后面会解释为啥倒着来,这样方便理解
    {
        DO SOME THING
    }

然后因为我们是倒着查找的,所以要寻找可以“接上”的最长不上升子序列,要向前查找

所以

for(register int i=n;i>=1;i--)
    {
        for(register int j=i+1;j<=n;j++)
        {
            DO SOME THING AGAIN
        }
    }

要“接上”最长不上升子序列,要满足什么样的条件?

肯定得比现在查找的数小!(废话*2)

SO

for(register int i=n;i>=1;i--)
    {
        for(register int j=i+1;j<=n;j++)
        {
            if(a[j]<=a[i])//不上升
            {
                SOME THING IMPORTANT
            }
       	}
    }

下面剩下怎么“接上”是最优的问题

很显然,我们只有两种操作

  • 1.不接上,这样 f[i] 不变

  • 2.接上,就是f[j]+1,因为接上了现在的数,所以长度+1

so

 f[i]=max(f[i],f[j]+1);

然后一个完整的代码

#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
int a[100];
int f[100];
int main()
{
    int n;
    cin>>n;
    for(register int i=1; i<=n;i++)
    {
        scanf("%d",&a[i]);
        f[i]=1;
    }
    for(register int i=n;i>=1;i--)
    {
        for(register int j=i+1;j<=n;j++)
        {
            if(a[j]<=a[i])
            {
                f[i]=max(f[i],f[j]+1);
            }
        }
    }
    int maxx=-1;//无法保证最长不上升子序列在哪个位置,所以整个查询一遍
    for(register int i=1; i<=n;i++)
    {
        maxx=max(f[i],maxx);
    }
    cout<<maxx;
    return 0;
}

例题

P1091 [NOIP2004 提高组] 合唱队形

不要被单调队列的tag吓到,这题和单调队列屁关系没有

很显然合唱队形长这样

不难发现,这就是俩不上升子序列,一个倒过来接在另一个后面,所以这题就可以分解成俩问题了,求序列中一个点,让它两边的不上升子序列最长

#include<cstdio>
#include<bits/stdc++.h>
using namespace std;
int n;
int a[200];
int f[200],ff[200];
inline int dp(int x)
{
    for(int i=1;i<=n;i++) f[i]=ff[i]=1;//都初始化成1
    int l;//l和r表示找到的最长不上升子序列的长度
    for(int i=x-1;i>=1;i--)//从枚举到的数往前枚举
    {
        l=0;//记得归0
        for(int j=i; j<=x;j++)//从i往后找最长不上升子序列
        {
            if(a[j]>a[i]&&f[j]>l)//可以接上并且大于l现在的长度
            {
                l=f[j];//赋值//其实这样等同于l=max(l,f[j])
            }
        }
        f[i]=l+1;//给f[i]赋值
    }
    int maxn=-1;//查找最大的
    for(int i=1; i<=x;i++)
    {
        maxn=max(maxn,f[i]);
    }
    int r;//同上
    for(int i=x;i<=n;i++)
    {
        r=0;
        for(int j=i; j>=x;j--)
        {
            if(a[j]>a[i]&&ff[j]>r)
            {
                r=ff[j];
            }
        }
        ff[i]=r+1;
    }
    int maxm=-1;
    for(int i=x; i<=n;i++)
    {
        maxm=max(maxm,ff[i]);
    }
    return n-maxn-maxm+1;//+1是因为多加了一个x本身
}
int main()
{
    int maxxx=0x7fffffff;
    cin>>n;
    for(register int i=1; i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(register int i=1; i<=n;i++)//从每个数枚举
    {
        maxxx=min(dp(i),maxxx);
    }
    cout<<maxxx;
    return 0;
}

\(\mathcal{Second.2\ }\) 最大子段和

P1115 最大子段和

给我们一个序列,求其中任意连续的一段子序列的最大

暴力的话......直接爆掉,起码 \(O(n^2)\),所以排掉

考虑DP

我们设置一个 f[i] 数组,表示以i结尾的最大子段和

比如对于

1 2 8 4 5 6

这个序列

f[1]=1 f[2]=3 f[3]=11

f[4]=....

不难发现当序列都是正数的时候,直接选取整个序列就是最大子段和,但如果序列中混入几个负数,就难搞了

所以我们使用DP来解决

我们来分析一下状态有哪俩种

  • 1.选之前的数加上自己(因为之前的和有可能是负数也有可能是正数)

  • 2.选自己(因为是连续的)

所以得出状态转移方程

f[i]=max(f[i-1]+a[i],a[i]);

这个 f 数组里,答案不一定存储在最后,所以我们需要设置一个 maxx 来存储

 maxx=max(maxx,f[i]);

然后完整Code

#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
int f[200005];
int n;
int maxx=-9999999;
int a[200005];
int main()
{
    scanf("%d",&n);
    for(register int i=1; i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(register int i=1; i<=n;i++)
    {
        f[i]=max(f[i-1]+a[i],a[i]);
        maxx=max(maxx,f[i]);
    }
    cout<<maxx;
    return 0;
}

To be continue

posted @ 2021-07-17 17:30  Edolon  阅读(24)  评论(0编辑  收藏  举报
Live2D