动态规划(DP)的学习总结(二、线性DP、LIS、LCS、最大值+路径、多重背包及优化、剪枝、单调队列、倍增)

不经一番寒彻骨,怎得梅花扑鼻香。 ——黄蘖禅师《上堂开示颂》

继第一篇后,博主又做到几道好题,打算这篇讲讲。

首先,因为上一篇讲过01背包,所以先给大家推荐一道很好的01背包练习题:P2340 [USACO03FALL] Cow Exhibition G

因为此题的题解讲的已经很好了,所以我就不再解释了,但是向大家推荐一下这位李老师的题解:题解 P2340 【[USACO03FALL]Cow Exhibition G】 - 洛谷专栏,讲的很全面,内容很仔细,大家可以好好琢磨。

目录

一、最长上升子序列(LIS)

二、最长公共子序列(LCS)

三、最大值+路径问题

四、多重背包

五、单调队列

六、剪枝

         七、倍增


一、最长上升子序列(LIS)

先看一道模板题:

此题摘自:B3637 最长上升子序列 - 洛谷

B3637 最长上升子序列

题目描述

这是一个简单的动规板子题。

给出一个由 ​ 个不超过 ​ 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。

最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。

输入格式

第一行,一个整数 ​,表示序列长度。

第二行有 ​ 个整数,表示这个序列。

输出格式

一个整数表示答案。

输入输出样例 #1

输入 #1

6
1 2 4 1 3 4

输出 #1

4

说明/提示

分别取出 ​1、2​、3​、4​ 即可。

给一段正整数序列,求最长上升子序列

假设用数组a[N]存储这段序列,dp[N]存储我们找到的上升序列

即我们需要判断,假设当我们遍历到第i个数时,这个数是否能够插入到我们已经找到的上升子序列中,即设 dp[j] 为我们已经找到的上升总序列的第 j 位,如果a[i]>dp[j-1]&&a[i]<dp[j],那么就可以把a[i]插入上升序列中

//这是关键部分
if(a[i]<dp[j]&&a[i]>dp[j-1]){
    dp[j]=a[i];
}

什么意思呢?我们举个栗子吧:

也就是,我们原来的上升序列是[1,2,4],但是我们发现a[5]也就是序列中的3刚好大于上升序列的第2位,同时小于第3位,那我们就可以把3这个数最为第3位,用另一个更小的数代替了原来的数,这样就能得到最长上升序列了。

那么就剩下一个问题,怎么记算最长上升序列的长度呢?

很简单,我们用cnt来记录dp数组的长度就ok了,即发现满足上升序列的数,就令cnt+1,同时dp[cnt]=a[i]。

代码如下:

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
#define mod 1e9+7
const int N=1e6+5;
const int INF=0x3f3f3f3f;
int n,a[N],b[N];
int dp[N];
int ans,cnt;
​
void sloved(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    dp[1]=a[1];
    cnt=1;
    for(int i=2;i<=n;i++){
        if(a[i]>dp[cnt]){
            cnt++;
            dp[cnt]=a[i];
        }
        for(int j=1;j<=n;j++){
            if(a[i]<dp[j]&&a[i]>dp[j-1]){
                dp[j]=a[i];
            }
        }
    }
    cout<<cnt<<endl;
}
signed main(){
    R;
    int T;
    //cin>>T;
    T=1;
    for(int i=1;i<=T;i++){
        sloved();
    }
    return 0;
}

此代码时间复杂度为​

令外,还有一道拓展题,用了一个叫Dilworth 定理 的东西:

此题摘自:P1020 [NOIP 1999 提高组] 导弹拦截 - 洛谷

P1020 [NOIP 1999 提高组] 导弹拦截

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

一行,若干个整数,中间由空格隔开。

输出格式

两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入输出样例 #1

输入 #1

389 207 155 300 299 170 158 65

输出 #1

6
2

说明/提示

对于前 ​ 数据(NOIP 原题数据),满足导弹的个数不超过 ​ 个。该部分数据总分共 ​ 分。可使用​ 做法通过。 对于后 ​ 的数据,满足导弹的个数不超过 ​ 个。该部分数据总分也为 ​ 分。请使用 ​ 做法通过。

对于全部数据,满足导弹的高度为正整数,且不超过 ​。

此外本题开启 spj,每点两问,按问给分。

首先给大家说明一下Dilworth定理: 它表明在任何有限偏序集中,最大反链的元素个数等于最小链划分中链的数目。这个定理的对偶形式也是成立的,即任何有限偏序集中最长链中元素的数目等于最小反链划分中反链的数目。

简单来说就是:一个序列的不上升子序列的个数,等于其最长上升子序列的长度;一个序列的不下降子序列的个数,等于其最长下降子序列的长度。

如果你已经明白这条定理,请你先思考一下这道题的解法吧。

即我们可以分别求这段序列的最长下降子序列和最长上升子序列的长度,而最长下降子序列的长度也可以理解为这段序列的逆序列的最长上升子序列的长度。

但是如果我们用上道题作为模板解决这道题的话时间复杂度​会TLE,所以我们还需要加以优化。

从哪里入手优化?还记得我们模板中的关键部分吗?找到一个更小的数代替原来的数那里,模板用的是枚举,而我们可以将枚举改用时间复杂度更低的二分查找,二分的时间复杂度为​。

还记得二分的定义吗?在数组中找到第一个大于等于x(你要找的数)的数,因为原数组是无序的,所以不能查找,但是dp数组是上升的,即通过二分在dp数组中找到一个大于等于a[i]的数,然后判断是否插入。

以下是关键代码:

for(int i=1;i<=n;i++){
     if(h[i]<=f[cnt]){
         f[++cnt]=h[i];
     }else{
         int l=1,r=cnt;
         while(l<r){
             int mid=l+r >>1;
            if(f[mid]<h[i]) r=mid;
            else    l=mid+1
        }
         f[l]=h[i];
     }
 }

因为我们没插入第一个值,所以别忘了将f[0]初始化为极大值0x3f。

下面为此题代码:

#include <bits/stdc++.h>
using namespace std;
//-------------------------------------------------------------------------------------------
#define int long long 
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
const int mod=9982443653;
const int N=1e5+10;
const int INF=0x3f3f3f3f;
const int inf=0x3f;
//--------------------------------------------------------------------------------------
int h[N],f[N];
int n,cnt,x;
void sloved(){
    while(cin>>x){
        h[++n]=x;
    }
    f[0]=INF;
    for(int i=1;i<=n;i++){
        if(h[i]<=f[cnt]){
            f[++cnt]=h[i];
        }else{
            int l=1,r=cnt;
            while(l<r){
                int mid=l+r >>1;
                if(f[mid]<h[i]) r=mid;
                else    l=mid+1;
            }
            f[l]=h[i];
        }
    }
    cout<<cnt<<endl;
    cnt=0;
    f[0]=-INF;//这里求最长下降子序列,所以f[0]初始化为极小值
    for(int i=1;i<=n;i++){
        if(h[i]>f[cnt]){
            f[++cnt]=h[i];
        }else{
            int l=1,r=cnt;
            while(l<r){
                int mid=l+r >>1;
                if(f[mid]>=h[i])    r=mid;
                else    l=mid+1;
            }
            f[l]=h[i];
        }
    }
    cout<<cnt<<endl;
}
signed main(){
    R;
    int T;
    //cin>>T;
    T=1;
    for(int i=1;i<=T;i++){
        sloved();
    }
    return 0;
}

此代码时间复杂度为​。

二、最长公共子序列(LCS)

最长公共子序列与最长上升子序列类似,或者说完全可以转换为最长上升子序列。

但是在这之前,先介绍一下最长公共子序列: 在两个字符串序列中以相同顺序出现,但不要求连续(非子串)的最长子序列。例如,对于序列 "ABCBDAB" 和 "BDCAB","BCAB" 是它们的一个LCS。

如果你还没懂,请先记住子序列的定义: 字符串S的子序列 是从S中将若干元素提取出来并不改变相对位置形成的序列。

也就是说:

  1. 两段字符串分别按顺序(一定是按顺序的!!!)提取一段公共子序列(公共即两个字符串中都出现的字符);

  2. 找到所有的公共子序列并计算子序列长度;

  3. 所有公共子序列中长度最长的即答案。

举个小栗子:

此例来自[wiki]最长公共子序列

s1="abcde";
s2="acde";
//1. 公共子序列为:a,c,d,e,ac,ad,ae,cd,ce,de,acd,ace,ade,cde,acde;
//2. 公共子序列的长度有:1,2,3,4;
//3. 4最长,即最长公共子序列为acde,长度为4;

ok,相信到这里你已经非常明白最长公共子序列的含义了,下面来试着做一做模板题吧:

此题摘自P1439 【模板】最长公共子序列 - 洛谷

P1439 【模板】最长公共子序列

题目描述

给出 ​ 的两个排列 ​ 和 ​ ,求它们的最长公共子序列。

输入格式

第一行是一个数 ​。

接下来两行,每行为 ​ 个数,为自然数 ​ 的一个排列。

输出格式

一个数,即最长公共子序列的长度。

输入输出样例 #1

输入 #1

5 
3 2 1 4 5
1 2 3 4 5

输出 #1

3

假设输入的两段字符串为a,b。

首先,要想知道哪个字符是他们公共的且可以按相同顺序提出,我们肯定能想到枚举,但肯定会炸T,所以我们就要换一种想法。

拿样例来说吧:

a={3,2,1,4,5};
b={1,2,3,4,5};
//很容易看出最长公共子序列为3,4,5;
//通过观察发现,只需要数字在a中比在b中出现的早或者同时出现就可以了;

这要怎么实现?

我们不妨将a中的数字给个出场顺序

for(int i=1;i<=n;i++){
    int x;
    cin>>x;
    a[x]=i;     
}
for(int i=1;i<=n;i++){
    cin>>b[i];
}

那么就变成了:

a:
序列:3 2 1 4 5
顺序:1 2 3 4 5
b:
序列:1 2 3 4 5
顺序:3 2 1 4 5

现在再来看怎么找符合条件的数字的问题,是不是a中出现的早的顺序号,在b中体现的就是小,而且之后的符合条件的数字也都是顺序号在b中体现为递增的。

那么问题就转换为了在b中找最长的递增序列。

哦!OMG,这这这不就是最长上升子序列了吗!?

so,这样问题就很奇妙的转换为了你会做的求最长上升子序列长度了。

因为这题的​,​会TLE,所以要用二分查找。

代码如下:

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
#define mod 1e9+7
const int N=1e5+10;
const int INF=0x3f3f3f3f;
int n,a[N],b[N],dp[N];
int ans,cnt;

int bound(int x){
    int l=1,r=cnt;
    while(l<r){
        int mid=(l+r)>>1;
        if(b[x]<b[dp[mid]]){
            r=mid;
        }else{
            l=mid+1;
        }
    }
    return l;
}

void sloved(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        int x;
        cin>>x;
        b[x]=i;
    }
    cnt=1;
    for(int i=1;i<=n;i++){
        if(b[a[i]]>b[dp[cnt]]){
            cnt++;
            dp[cnt]=a[i];
        }else{
            dp[bound(a[i])]=a[i];
        }
    }
    cout<<cnt-1<<endl;
}
signed main(){
    R;
    int T;
    //cin>>T;
    T=1;
    for(int i=1;i<=T;i++){
        sloved();
    }
    return 0;
}

我的代码是将b中的数字给顺序的,与前面讲的相反。

三、最大值+路径问题

如果你已经能够做一些简单的DP题后,就会刷到一种题,它不仅要最大值,它还要你输出你选的路径。

那么这种题就会很困扰,主要是我困扰,挠了1个多小时头发都要掉光了也想不到,所以这次给大家也讲一讲这种记录路径题要怎么做。

这里有两种方法:

  1. 动态规划还原;

  2. dfs(其实也可以说是记忆化搜索啦)

虽然有两种,但是博主只想讲第一种。会一种就可以啦 其实是我懒

但是在这之前还是先看例题:

此题摘自:P1854 花店橱窗布置 - 洛谷

P1854 花店橱窗布置

题目描述

某花店现有 ​ 束花,每一束花的品种都不一样。至少有同样数量的花瓶,被按顺序摆成一行。花瓶的位置是固定的,从左到右按 ​ 顺序编号,​ 是花瓶的数目。

花束可以移动,并且每束花用 ​ 的整数标识。所有花束在放入花瓶时必须保持其标识数的顺序。例如,假设杜鹃花的标识数为 ​,秋海棠的标识数为 ​,康乃馨的标识数为 ​,即杜鹃花必须放在秋海棠左边的花瓶中,秋海棠必须放在康乃馨左边的花瓶中。每个花瓶只能放一束花。

每个花瓶的形状和颜色也不相同,因此,当各个花瓶中放入不同的花束时,会产生不同的美学效果,并以美学值(一个整数 ​)来表示,空置花瓶的美学值为 ​。在上述的例子中,花瓶与花束的不同搭配所具有的美学值,可以用如下的表格来表示:

花瓶 1花瓶 2花瓶 3花瓶 4花瓶 5
杜鹃花
秋海棠
康乃馨

根据表格,杜鹃花放在花瓶 ​ 中,会显得非常好看,但若放在花瓶 ​ 中,则显得很难看。

为了取得最佳的美学效果,必须在保持花束顺序的前提下,使花的摆放取得最大的美学值,如果具有最大美学值的摆放方式不止一种,则输出任何一种方案即可。

输入格式

输入文件的第一行是两个整数 ​ 和 ​,分别为花束数和花瓶数。

接下来是矩阵 ​,共 ​ 行,每行 ​ 个整数,​ 表示花束 ​ 摆放在花瓶 ​ 中的美学值。

输出格式

输出文件的第一行是一个整数,为最大的美学值;接下来一行 ​ 个整数,为那束花放入那个花瓶的编号。

输入输出样例 #1

输入 #1

3 5
7 23 -5 -24 16
5 21 -4 10 23
-21 5 -4 -20 20

输出 #1

53
2 4 5

把这题看成两问:

  1. 求最大值

  2. 输出路径

先正常的用DP做出第1问

设dp[f][v]为将插f束花插入v个花瓶中的最大美学值。

当插第i朵花,插到第j个花瓶时,因为每朵花插入不同花瓶有不同的美学值,所以还要加一层遍历 l 为插入第几个花瓶,即dp[i][j]=max(dp[i][j],dp[i-1][l-1]+a[i][l])

代码如下:

 memset(dp,0xcf,sizeof(dp));//初始化为极大值
    cin>>f>>v;
    for(int i=1;i<=f;i++){
        for(int j=1;j<=v;j++){
            cin>>a[i][j];
        }
    }    
    for(int i=0;i<=v;i++){   //将已知的赋值
        dp[0][i]=0;          //插入0朵花时美学值为0
    }
    for(int i=1;i<=f;i++){
        for(int j=i;j<=v;j++){
            for(int l=i;l<=j;l++){
                dp[i][j]=max(dp[i][j],dp[i-1][l-1]+a[i][l]);
            }
        }
    }
    cout<<dp[f][v]<<endl;

接下来看第二问

我们先看一下通过第1问求出来的dp画出来的表格

i \ j12345
000000
1723232323
20xcf28283346
30xcf0xcf242453

标粗的即路径,2,4,5

因为最大值一定在每行的后边,所以我们从最后一行最后以列向前搜索,如果dp[i][j]!=dp[i][j-1],那么dp[i][j]就是最大值,j就是第i朵花插入的花瓶,然后再去搜索第 i-1 朵花,直到 i==0 搜索结束。由于我们是从后向前搜的路径,所以我们利用递归实现输出。

代码如下:

void print(int f,int v){     
    if(f==0)    return;
    while(dp[f][v]==dp[f][v-1]){    //如果相等就将花瓶向前推,直到找到第一个为此值的花瓶
        v--;
    }
    print(f-1,v-1);
    cout<<v<<' ';
}

其实这题是一道多重背包题~

既然提到了多重背包那就顺便介绍一下好了。

四、多重背包

先放个WIKI解释:背包 DP - OI Wiki

这个就不多说了,我们主要看下面的优化方法:单调队列。

五、单调队列

关于这个方法,已经有讲得更好的了,就放个坐标大家自行去学习吧, 单调队列(C/C++)-CSDN博客

然后这里还是放一道练习题给大家:

摘自:P1725 琪露诺 - 洛谷

P1725 琪露诺

题目描述

在幻想乡,琪露诺是以笨蛋闻名的冰之妖精。

某一天,琪露诺又在玩速冻青蛙,就是用冰把青蛙瞬间冻起来。但是这只青蛙比以往的要聪明许多,在琪露诺来之前就已经跑到了河的对岸。于是琪露诺决定到河岸去追青蛙。

小河可以看作一列格子依次编号为 ​ 到 ​,琪露诺只能从编号小的格子移动到编号大的格子。而且琪露诺按照一种特殊的方式进行移动,当她在格子 ​ 时,她只移动到区间 ​ 中的任意一格。你问为什么她这么移动,这还不简单,因为她是笨蛋啊。

每一个格子都有一个冰冻指数 ​,编号为 ​ 的格子冰冻指数为 ​。当琪露诺停留在那一格时就可以得到那一格的冰冻指数 ​。琪露诺希望能够在到达对岸时,获取最大的冰冻指数,这样她才能狠狠地教训那只青蛙。

但是由于她实在是太笨了,所以她决定拜托你帮它决定怎样前进。

开始时,琪露诺在编号 ​ 的格子上,只要她下一步的位置编号大于 ​ 就算到达对岸。

输入格式

第一行三个正整数 ​。

第二行共 ​ 个整数,第 ​ 个数表示编号为 ​ 的格子的冰冻指数 ​。

输出格式

一个整数,表示最大冰冻指数。

输入输出样例 #1

输入 #1

5 2 3
0 12 3 11 7 -2

输出 #1

11

这题就是一个滑动窗口,需要用到单调队列求最大值

因为只能移动到[i+l,i+r],所以第一个值为v[l],从此处开始形成窗口,同时用dp数组记录当前最大值,然后每次判断对尾是否大于最大值,如果小于,就将队尾弹出;还要判断队首是否还在窗口内,如果不在就弹出,注意判断条件应为<i-r,

这样就保证了每个窗口中的队首就是当前窗口的最大值,然后累加得出答案。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
//-------------------------------------------------------------------------------------------
#define int long long 
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
const int mod=9982443653;
const int N=2e5+10;
const int INF=0x3f3f3f3f;
const int inf=0xcf;
//--------------------------------------------------------------------------------------
int n,l,r,ans;
int f[N],a[N];
struct Node{   //存储冰冻值和编号
    int v,co;
};
deque<Node> dq;//双端队列
void sloved(){
    cin>>n>>l>>r;
    for(int i=0;i<=n;i++){
        cin>>a[i];
    }
    memset(f,-INF,sizeof(f));
    ans=-INF;
    f[0]=0;
    int p=0;
    for(int i=l;i<=n;i++){
        while(!dq.empty()&&dq.back().v<f[p]){    //判断是否弹出队尾
            dq.pop_back();
        }
        dq.push_back((Node){f[p],p});
        while(i-dq.front().co>r){    //判断是否弹出队首
            dq.pop_front();
        }
        f[i]=a[i]+dq.front().v;   //叠加最大值
        p++;
    }
    for(int i=n-r+1;i<=n;i++){
        ans=max(ans,f[i]);       //找出最大值
    }
    cout<<ans<<endl;
}
signed main(){
    R;
    int T;
    //cin>>T;
    T=1;
    for(int i=1;i<=T;i++){
        sloved();
    }
    return 0;
}

六、剪枝

这是也一种优化方法

wiki解释在这:优化 - OI Wiki

一般就是如果已经有解就跳出搜索

其实大部分搜索都用到剪枝来优化的

因为剪枝的方法有很多,不同题就有不同的剪枝,所以没办法细讲。

这里就放一道有意思的DP+剪枝题给大家体会吧:

P1874 快速求和 - 洛谷

P1874 快速求和

题目背景

2023-10-08 update: 新增两组 hack。

2023-12-16 update: 新增两组 hack。

题目描述

给定一个数字字符串,用最小次数的加法让字符串等于一个给定的目标数字。每次加法就是在字符串的某个位置插入一个加号。在里面要的所有加号都插入后,就像做普通加法那样来求值。

例如,考虑字符串12,做 ​ 次加法,我们得到数字 ​。如果插入 ​ 个加号,我们得到 ​,因此,这个例子中,最少用 ​ 次加法就得到数字 ​。

再举一例,考虑字符串303和目标数字 ​,最佳方法不是3+0+3。而是3+03。能这样做是因为一个数的前导 ​ 不会改变它的大小。

输入格式

第一行:一个字符串 ​。

第二行:一个整数 ​。

输出格式

一行一个整数表示最少的加法次数让 ​ 等于 ​。如果怎么做都不能让 ​ 等于 ​ ,则输出 ​。

输入输出样例 #1

输入 #1

99999
45

输出 #1

4

这道题我觉得这份题解写的就很好 ----> 题解 P1874 快速求和 - 洛谷专栏

我就简单地在这基础上解释一下剪枝是怎么发挥作用的吧

先给大家看一下num数组的值

j\i12345
1999999999999999
29999999999
3999999
4999
59

for(int i=1;i<=len;i++){
        for(int k=0;k<=n;k++){
            for(int j=i-1;j>=0&&num[j+1][i]<=n;j--){
    //这里num[j+1][i]<=n 表示如果从j+1到i的子串大于需要的总和n就跳出循环
                if(k>=num[j+1][i]){//这里也是判断是否大于当前遍历的和k,如果小于就更新f数组值,即加一个加号
                    f[i][k]=min(f[i][k],f[j][k-num[j+1][i]]+1);
                }
            }
        }
    }

以上两处用到剪枝优化,总而言之就是:如果分成的所有子串中如果某个子串大于需要的和就跳出。

解释无力,大家在还是刷题中自行参透吧

其实也可以看大佬的解释啦

七、倍增

Wiki解释:倍增 - OI Wiki

废话不说,放题:倍增 - OI Wiki

P3147 [USACO16OPEN] 262144 P

题目描述

贝西喜欢在手机上下载游戏来玩,尽管她确实觉得对于自己巨大的蹄子来说,小小的触摸屏用起来相当笨拙。

她对当前正在玩的这个游戏特别感兴趣。游戏开始时给定一个包含 ​ 个正整数的序列(​),每个数的范围在 ​ 之间。在一次操作中,贝西可以选择两个相邻且相等的数,将它们替换为一个比原数大 1 的数(例如,她可以将两个相邻的 7 替换为一个 8)。游戏的目标是最大化最终序列中的最大数值。请帮助贝西获得尽可能高的分数!

输入格式

第一行输入包含 ​,接下来的 ​ 行给出游戏开始时序列的 ​ 个数字。

输出格式

请输出贝西能生成的最大整数。

输入输出样例 #1

输入 #1

4
1
1
1
2

输出 #1

3

说明/提示

在示例中,贝西首先合并第二个和第三个 1,得到序列 1 2 2,然后将两个 2 合并为 3。注意,合并前两个 1 并不是最优策略。

这题有点倍增的味道,还结合了DP

假设f[i][j]为从j位置开始,能和出i的位置

首先记录给定数字的位置,即

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

然后从2开始搜索,找到并记录能够和出i的位置,即如果f[i][j]!=0就说明能生成的最大整数为i,令ans=i

那么就剩下一个问题:怎么找到位置的?

如果有两个相同的数字,就能和出这个数字+1,要想和出2,就要找两个1的位置。

f[2][1],先找到第一个1的位置,即f[1][1],再找以第一个1的位置为左端点后第二个1的位置,即f[1][f[1][1]],那么第二个以的位置就是f[2][1]=f[1][f[1][1]]

如果f[i][j]==0,就要看是否能生成i

那么递推式就是:f[i][j]=f[i-1][f[i-1][j]]

代码如下:

#include <bits/stdc++.h>
using namespace std;
//-------------------------------------------------------------------------------------------
#define int long long 
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define endl '\n'
const int mod=998244353;
const int N=270000;
const int INF=0x3f3f3f3f;
const int inf=0x3f;
//--------------------------------------------------------------------------------------
int n,ans;
int f[60][N];
void sloved(){
    cin>>n;
    for(int i=1;i<=n;i++){
        int in;
        cin>>in;
        f[in][i]=i+1;
    }
    for(int i=2;i<=58;i++){
        for(int j=1;j<=n;j++){
            if(!f[i][j]){//不为0就要找两个i-1
                f[i][j]=f[i-1][f[i-1][j]];
            }
            if(f[i][j]){//不为0说明有位置,即能生成i
                ans=i;
            }
        }
    }
    cout<<ans<<endl;
}
signed main(){
    R;
    int T;
    //cin>>T;
    T=1;
    for(int i=1;i<=T;i++){
        sloved();
    }
    return 0;
}

好像解释的不太清楚,但是还请大家多多琢磨一下吧~

题解参考:https://www.luogu.com.cn/problem/solution/P3147

题解 P3147 【[USACO16OPEN]262144】 - 洛谷专栏

感谢观看~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值