【线性dp】| AcWing 算法基础班试题总结(一)

898. 数字三角形

题目描述

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大

           7
         3   8
       8   1   0
      2   7   4   4
    4   5   2   6   5

输入格式
第一行包含整数n,表示数字三角形的层数。
接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000

输入样例:

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

输出样例:

30

算法实现

思路一

按照题意,从顶向下走,第i层取决于第i-1层,每层只能走正下或者右下,所以对第i层而言,只有其左上方和正上方的点会到下一层的同一个位置,所以第i层往上的搜索的方向是正上和左上方;
从顶向下走,走到最后时会有n条路径,需要考虑最后一层的n个路径中哪一条路径的和最大。

dp[i][j]记录了从起点(1,1)开始到(i,j)位置的路径的最大值:(一个起点,多个终点),到第n行即停止。下标1开始存储
动态转移方程dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])+a[i][j]

存在两个边界情况一个初始化情况
1. 初始化情况:第一次的数据要初始化,即先初始化一个i-1行,然后后面for循环处理时,可以使用第一次初始化的数据

2. 边界情况:存在 下标 i-1、j-1
dp[i-1][j]:由于每行的个数是递增增加的,第i行的第i个数据,它的正上方第i-1行第i个数据没有
dp[i][j-1]:每行的第一个数据,它的左上方,第i-1行第0个数据也是没有的。
综上情况,由于动态方程中i-1、j-1的存在,dp数组必须从1开始存储,存储数据的数组最好也一样。
由于数据范围从-1e4到1e4,所以为了处理好边界情况,不使不存在的数被选中,只有大的数才能被选中,所以使其尽可能初始化小点。

优化为一维数组:
我们注意到,每次第i行第j列的数据要用到第i-1行第j列、第j-1列的数据,
所以如果正序更新的话,第j列被更新完,更新dp[i][j+1]时,又要用到第i-1行的第j列的数据,但是刚刚已经更新为第i行的数据了,所以采用倒序更新,已经被更新的过的数据就不会再被用了,因为他只会向正上方的数据和左上方的的数据搜索。

#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=510,INF=0x3f3f3f3f;
int a[N][N],dp[N];

int  main()
{
    
    
    int n;
    read(n);
    for (int i=1;i<=n;i++)
        for (int j=1;j<=i;j++) read(a[i][j]);
    //初始化    
    fill(dp,dp+n+1,-INF);  "从dp[0]到dp[n]进行初始化,左闭右开"
    dp[1]=a[1][1];
    
    //开始处理
    for (int i=2;i<=n;i++)
        for (int j=i;j>=1;j--) dp[j]=max(dp[j-1],dp[j])+a[i][j];
    
    //筛选答案
    int res=-INF;
    for (int i=1;i<=n;i++) res=max(res,dp[i]);
    
    printf("%d",res);
    
    return 0;
}

关于第16行、17行的初始化:fill(dp,dp+n+1,-INF); dp[1]=a[1][1];
先把dp数组都初始化为负无穷,INF代表不存在,因为是三角形每层个数是递增的,所以每往下走一层,解锁一个INF,dp[0]即第0列,必为INF,当走到第i行第i列时,此时dp[i]为-INF,要用到dp[i-1][j]dp[i-1][j],这时解锁一个INF,一直到第n行第n列,这是除了第0列全部解锁。

第一次初始化时就要用到第一行,因为第一行没法用动态公式,他没有上一行,所以就初始化第一行的值,且他只有一个值,作为起点,即解锁第一行的INF。

思路二

我们也可以采用相反的思路,从底向上走,这样走到最后时只有一条路径了,直接输出即可,但是起点有很多。
从底向上走时,第i层的数据取决于第i+1层,第i层朝下只能走正下或者右下方,所以要往下的搜索的方向就是正下和右下方了。

dp[i][j]记录从底部为起点到(i,j)位置的路径的最大值:(多个起点、一个终点),终点为dp[0][0] (下标0存储)
动态转移方程dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j]
每行都是从左到右更新,每行更新时,左边用过的数据都不会再被用到了,且求第i行第j列的最大路径值时只需要用到第i+1行第j列和第j+1列的数据,不存在i-1、j-1的情况,所以不用考虑边界情况,也可以从下标0或者下标1存储数据,且可以优化为一维数组:
dp[j]=max(dp[j],dp[j+1])+a[i][j]

下面代码从 下标0 存储数据。

#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=510;
int a[N][N],dp[N];

int main()
{
    
    
    int n;
    read(n);
    for (int i=0;i<n;i++)
        for (int j=0;j<=i;j++) read(a[i][j]);  "第0到第n-1行存储三角形"
"初始化数值,第n-1行,动态方程中dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j],即第一次的i+1行"
    for (int i=0;i<n;i++) dp[i]=a[n-1][i];
"然后从第n-2行进行处理"
    for (int i=n-2;i>-1;i--) 
        for(int j=0;j<=i;j++) dp[j]=max(dp[j],dp[j+1])+a[i][j];
        
    
    printf("%d",dp[0]);
    
    return 0;
}

895、896 最长上升子序列

题目描述

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。(不要求连续)

输入格式
第一行包含整数N。
第二行包含N个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
−109≤数列中的数≤109
Case1:1≤N≤1000,
Case2:1≤N≤100000

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4   即子序列 1 2 5 6

算法实现

朴素做法

状态表示:f[i]表示从第1个数字开始算,到第i个数字a[i]结尾的最大的上升序列。f[i]的值表示以a[i]结尾的所有上升序列中属性为最大值的那一个。

状态计算(集合划分):j∈(0,1,2,…,i-1), 在a[i] > a[j]时(严格递增)
i f ( a [ i ] > a [ j ] )     f [ i ] = m a x ( f [ j ] + 1 ) , j = 0 、 1 、 2 … … i − 1 if(a[i]>a[j])   f[i] = max(f[j] + 1),j=0、1、2……i-1 if(a[i]>a[j])  f[i]=max(f[j]+1)j=012i1

有一个特殊情况,若前面没有比a[i]小的,即找不到前面数字小于自己的时候,这条语句就不会执行,所以要给f[i]事先初始化好,此时f[i]为1,也是f[i]的最小值。(即自己是当前已知序列中最大的数,以自己为结尾的情况)。

由于有下标i-1的存在,所以最好都从下标1开始存储数据。

第一个位置dp[1]必为1,所以必初始化为1,其他情况下都是保底是1个即dp[i]=1,这是最差的情况。
所以要将整个dp数组都初始化为1,每往后走一个,就解锁一个dp[i]。
第一个位置初始化完成后,即可从第2个位置开始走for循环。

当全部遍历一遍后,每一个位置都有可能是f[i]的最大值,所以要遍历一遍数组求出f[i]的最大值。

时间复杂度
O(n2) 状态数(n) * 转移数(n)

#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=1010;
int a[N],dp[N];

int main()
{
    
    
     int n;
     read(n);
     for (int i=1;i<=n;i++) read(a[i]);

     //预处理,将所有情况下的先初始化为1,即必存在序列中只有1个的情况且该值最小
     fill(dp+1,dp+n+1,1); "dp[1]~dp[n]初始化为0,dp[0]的位置用不到,可以不初始化。"

    "第一个位置固定dp[1]为1,它前面没有数,从第二个位置开始递推"
    for (int i=2;i<=n;i++)
       for (int j=i-1;j>=1;j--)
           if (a[i]>a[j]) dp[i]=max(dp[i],dp[j]+1);

    //找到答案res
    int res=0;
    for (int i=1;i<=n;i++) res=max(res,dp[i]);
    printf("%d",res);

    return 0;
}

单调栈+二分

时间复杂度
O(nlogn) 状态数(n) * 转移数(logn)

样例:

7
3 1 2 1 8 5 6

如果是单调递增栈的话,这样的最终结果是:1 5 6

我们采用单调递增栈的思想:

对于每一个arr[i],找到单调栈中第一个大于等于arr[i]的数,stk[j]>=arr[i],即单调栈中大于等于arr[i]的最小的数stk[j],所以它的前一个位置stk[j-1]必然满足性质:stk[j-1]<arr[i],然后用arr[i]代替stk[j]即可。

因为题目要求的是严格单调递增,如果只是stk[j]>arr[i]就替换,那么stk[j-1]可能等于arr[i],因此stk[j-1]==stk[j],这样的栈就是一个非降栈,而不是一个严格递增栈了。

存在两种特殊情况:

  1. 单调栈中不存在大于等于arr[i]的数,即arr[i]比栈中所有的数都要小,如果是单调栈中要弹出所有的数,然后把arr[i]放在栈底,这里直接更新栈底的值为arr[i],其他位置都不动。
  2. arr[i]大于单调栈中的任何数,这时候直接入栈,放在栈顶即可。 (其实这就是stk[1]>=arr[i]而已)

arr : 3 1 2 1 8 5 6

① 初始化,第一个数3加入栈中:stk : 3

② 3>=1,栈中3大于等于arr数组中的1,所以1替换3,stk : 1

2 >= 1当前栈中最后一个元素,2加入栈中,stk : 1 2

④ 1 >= 1 , 栈中1大于等于arr数组中的1,所以1替换1,stk : 1 2

8 >= 2 当前栈中最后一个元素,8加入栈中,stk : 1 2 8

⑥ 8 >= 5,栈中8大于等于arr数组中的5,所以5替换8,stk : 1 2 5

6 >= 5,当前栈中最后一个元素,6加入栈中,stk : 1 2 5 6

stk 的长度就是最长递增子序列的长度,4

状态表示:f[i]表示长度为i的最长上升子序列,末尾最小的数字。 f[i]的值表示长度为i的最长上升子序列所有结尾中,结尾数值最小min的, 即f[i]=t度为i的子序列末尾最小元素是t。

记录每个长度为i的上升子序列结尾的最小值,因为若比它大的值的后面可以插入,那么这个小的值的后面肯定也可以插入,甚至作用范围更广。用f数组记录该值,f数组的下标即对应上升子序列的长度。

f[i]一定是一个单调递增的数组

状态计算

一、寻找f数组中大于等于a[i]的最小者,a[i]代替当前位置,它的前一个位置必小于a[i]。

一个新的数x插入后,可以在f数组中查找第一个大于等于x的位置,然后进行替换,且该位置的前一个位置的数肯定是小于x的最大值,因此可以在该位置后面插入,满足递增性质,然后在查找到的第一个大于等于x的位置进行更新。(一般情况下)

存在右边界情况:

当f数组中不存在大于等于a[i]的数时,根据二分查找的结果,是指向数组的最后一个位置,但这个位置不是我们要找的大于等于a[i]的位置,它仍小于a[i],这不符合题意,处理措施:

  1. 在f数组后面加一个INF,当数组中不存在大于等于a[i]的数时,就指向INF处,代表要插入的位置,然后插入,然后再在后面补一个INF,由于数组的长度是不断的更新的,最后的位置并不确定,因此需要反复操作。
  2. 进行if判断,如果f数组不存在大于等于a[i]的数时,就直接加入数组中,否则就用二分查找要替换的位置。注意:当a[i]小于数组中任何的数时,不是边界情况,因为二分查找时会指向f数组第一个数,它即是f数组中大于a[i]的第一个数。

我们下面采取措施2。

  1. f数组不存在大于等于a[i]的数也是最长子序列长度增加的条件。
  2. 当初始化完第一个数时,第二个数不会进行二分查找,要么是往后插入,要么是替换。
  3. 利用二分进行查找时,要求数组有序,而f数组必然是一个单调递增的数组。
  4. f[j]>=a[i] 才进行替换,注意是大于等于,这由严格递增决定,这样才能保证f[j-1]<f[j]。
#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=1e5+10;
int a[N],f[N];

int main()
{
    
    
    int n;
    read(n);
    for (int i=1;i<=n;i++) read(a[i]);

    f[1]=a[1];
    int len=1;
    for (int i=2;i<=n;i++)
        if (f[len]<a[i]) f[++len]=a[i];
        else {
    
    
            int l=1,r=len,mid;
            while (l<r) {
    
    "找到f数组中大于等于a[i]的最小者,a[i]代替当前位置,它的前一个位置必小于a[i]"
                mid=l+r>>1;
                if (f[mid]>=a[i]) r=mid;
                else l=mid+1;
            }
            f[r]=a[i];
        }

    printf("%d",len);

    return 0;
}

二、直接找到f数组中小于a[i]的最大者,a[i]代替它的下一个位置

当找到f数组中小于a[i]的最大者时,它的下一个位置必然是大于等于a[i]的,因此直接插入或者替换即可,这样就不用考虑右边界带来的问题了,但是会引起左边界的问题

当f数组中不存在完全小于a[i]的数时,即f[1]>=a[i],这样二分查找结束的时候,仍会指向f数组的开头,但这个位置实际上是我们待替换的位置,而不是小于a[i]的最大值的位置,处理措施同样有两种:

  1. 给f[0]的地方初始化一个很小的数据,使得任意的a[i]大于f[0]都成立,这样当f[1]~f[n]不满足条件时,就会指向f[0],这时候a[i]就要替换f[1]了,这个不像右边界处理的时候,f[0]的位置是固定的。
  2. 直接if判断,我要找的是f[k]<a[i]的k的最大值,但是如果f[1]>=a[i]时,a[i]就直接替换f[1],否则就二分查找k的位置。

完全小于,不取等于,否则就不是严格递减,然后下一个位置大于等于,进行替换。

措施1:

#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=1e5+10;
int a[N],f[N];

int main()
{
    
    
    int n;
    read(n);
    for (int i=1;i<=n;i++) read(a[i]);

    int len=1;
    f[1]=a[1];
    f[0]=-2e9;
    for (int i=2;i<=n;i++) {
    
    
        int l=0,r=len,mid;
        while (l<r) {
    
    "找到f数组中小于a[i]的最大者,a[i]代替它的下一个位置"
            mid=l+r+1>>1;
            if (f[mid]<a[i]) l=mid;
            else r=mid-1;
        }
        "找到后,f[r]的位置是小于a[i]的,f[r+1]的位置必然大于a[i]或者不存在,可以进行更新"
        f[r+1]=a[i];
        "判断是否序列长度增大"
        len=max(len,r+1);
    }

    printf("%d",len);

    return 0;
}

措施2:

    int len=1;
    f[1]=a[1]; 
    for (int i=2;i<=n;i++) {
    
    
        if (f[1]>=a[i]) f[1]=a[i];
        else {
    
    
        int l=1,r=len,mid;
        while (l<r) {
    
    
            mid=l+r+1>>1;
            if (f[mid]<a[i]) l=mid;
            else r=mid-1;
        }
        f[r+1]=a[i];
        len=max(len,r+1);
        }
    }

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114376881