最长上升子序列模型就像它的名字一样,用来从区间中找出最长上升的子序列。它主要用来处理区间中的挑选问题,可以处理上升序列也可以处理下降序列,原序列本身的顺序并不重要。
895. 最长上升子序列(活动 - AcWing)
896. 最长上升子序列 II(活动 - AcWing)
我们就这两个题来说一下最长上升子序列的两种实现方式:
1.动态规划实现
最长上升子序列首先是一个动态规划问题,所以我们先从动态规划的角度来考虑。
先来考虑状态表示,定义f[i]表示以元素i结尾的上升子序列的长度集合,而f[i]的值表示这些值中的最大值(对于这个f[i]的定义,我们可以考虑第i个元素的具体值是否会在后面被用上的角度来考虑是定义以i结尾还是前i个,那么就相当于从状态更新的角度来考虑状态的定义,而状态的更新也取决于状态定义,两者相辅相成)
然后就是状态计算,这里从倒数第二个元素的位置来考虑,也即考虑当前这个元素可以接在谁后面,那么就对应了当前的状态可以由谁来更新。显然它前面的元素都可以用来更新它,那么就需要遍历前面的部分进行更新:
到这里这道题实际上就解决了。我们分析下时间复杂度是O(n^2),对于数据范围比较小的情况肯定是没问题的。
#include
using namespace std;
int f[200010],a[200010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
f[i]=1;
for(int j=1;j<=i-1;j++)
{
if(a[j]
2.贪心做法
很明显O(n^2)的时间复杂度还是有局限性的,我们来考虑是否可以更优。
动态规划相当于从前面来定当前位置的状态,相当于从前往后考虑,而贪心则是侧重于这一步的决策会对后面产生的影响角度来考虑,做出产生最优影响的策略。不过显然我们不能从当前只有一个序列,放不放的角度来考虑,这样显然太片面了,在此之前显然会产生很多个不同的序列,而且这些序列有长有短,所以我们就假设现在已经产生了若干个上升序列,当前这个元素是自己新开一个序列,还是接在之前某一种已经产生的序列后面。这样显然之前产生的所有情况都会被考虑到,不会使也不会使当前的决策使后面更优的情况被否决掉。
那么问题就转化成考虑已经产生若干个序列了,当前的这个元素是单独开一个序列还是接在某一个后面。这些序列只有最后一个元素会被用来和当前元素比较,那么我们就只考虑最后一个元素,那么如果当前所有序列的最后一个元素都大于当前元素,那么当前元素没办法接在任意一个后面,所以必须单开。如果有一些序列的最后元素小于当前元素,那么很明显这个元素有地方可以接,而且接上后能使它们的长度有所增加,会比单开好一点。那些小于它的元素都可以被使用,那么问题由来了,我们接在谁的后面,接上当前元素实际上相当于拉大了最后一位的值,那么后面如果有稍小一点的元素可能会被影响到,所以接在这些成立情况里面最大的一个后面比较好,因为剩下小一些的还可以用来去接后面的元素,元素越小价值越大,我们要尽可能地保留那些较小的元素,因为它们后面可以接的元素比那些较大的元素多一些。所以策略就出来了,我们将元素接到末尾元素小于它的序列中最大的那个后面,那么这个序列的末尾元素就会被修改,同时序列长度也会变大。我们来看下我们统计的这些不同长度的序列,会发现它们实际上是单增的关系,
如图,如果我们要将2元素插入,很显然插到序列1后面,此时序列1和序列3的长度就相同了,对于长度相同的序列,因为元素2小于元素3,那么肯定保留元素2结尾的序列产生的效益更大,所以我们虽然是接在序列1后面,但是更新是却更新的长度为len+1的序列的尾元素。(用序列长度作为下标来设置数组,对应的值存序列的尾元素的值。)然后从单增序列中找小于它的元素的最大值用二分即可。二分的时间复杂度是O(logn),在加上外面那层循环,时间复杂度就是O(nlogn)。
#include
using namespace std;
int a[100010],q[100010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int len=0;
q[0]=-2e9;//方便后面的二分,相当于二分边界值的处理,任何一个值都可以接在它后面变成长度为1的序列。
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l>1;//l=mid时需要加上1
if(q[mid]
1017. 怪盗基德的滑翔翼(活动 - AcWing)
思路:没有模型那么裸,那我们来简单分析一下,首先他可以选择任意一个节点开始,可以向左,也可以向右,但是选定后就不能改方向了,然后经过的点是单减的。乍一看跟最大上升子序列没什么关系,但是我们把最后的路径画出来
很显然,最后的结果要么是红色箭头,要么是绿色箭头,对于红色箭头,从左往右看,那么就是单增的序列,绿色箭头从右往左看也是单增的序列,然后我们需要求最大值,那么不就是正着找一遍上升序列的最大值,反着找一遍上升序列的最大值,然后求两者的最大值嘛。那么就很简单了。
#include
using namespace std;
int a[200],q[200];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int mx=0;
a[0]=-2e9;
int len=0;
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l>1;
if(q[mid]=1;i--)
{
int l=0,r=len;
while(l>1;
if(q[mid]
ps:这里的LIS是用贪心法实现的,其实动态规划也行,核心是这个模型,而非实现方法。
1014. 登山(活动 - AcWing)
思路:先来分析题意,找一条路径,使路径上的点最多,对于路径的要求是先按照编号上升的顺序来游览,然后在某一点后按照编号单减的顺序游览。我们把最终路径画出来
是不是和上一题很像,但是实际上还是有区别的,因为上一题只包含这两条边中的更长的那个,而这题要求的则是这两条边的和。不过思路很明显可以拿过来用,先正着找一遍所有点结尾的最大值,再反着找一遍所有点的最大值,然后将每一个点的这两个值加起来,从所有和中求一个最大值,就是我们想要的结果。
#include
using namespace std;
int a[1010],z[1010],d[1010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
z[i]=1;
for(int j=1;j=1;i--)
{
d[i]=1;
for(int j=n;j>i;j--)
{
if(a[j]
ps:这里有个细节要注意,正着算和反着算都会包含当前点,最后记得减掉。
482. 合唱队形(482. 合唱队形 - AcWing题库)
思路:乍一看也没哪里说要求最长上升子序列,那么我们就先来分析,这里问最少移出几个人,但是被移除的人之间关系不大,我们可以从留下的人中考虑(比较经典的思路,正着思考不同就反着想),会发现被留下人之间有点规律:
眼不眼熟,而且被移走的人最少,不就是留下的人最少,那就是上图这个序列最长,然后跟上一题就没什么区别了,就是记得这题求得是被移出的人的最小值,而非留下的人中的最大值。
#include
using namespace std;
int a[1010],z[1010],d[1010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
z[i]=1;
for(int j=1;j=1;i--)
{
d[i]=1;
for(int j=n;j>i;j--)
{
if(a[j]
1012. 友好城市(活动 - AcWing)
思路:这题不是乍一看,而是哪怕把图画出来都会感觉跟最长上升序列没什么关系,
看看,确实跟上升子序列扯不上什么关系,很容易想到图论那边去,但是图论写的话,一时也没有什么思路。那么我们不妨再仔细想一想,这里每个城市连的只有一条边,那么我们先挑出几个可以同时存在的桥看看可以同时存在的桥之间有什么关系。
其实还是有点隐蔽,但是已经明显很多了,如果下面的点在前面,上面对应的点也在前面,所以对于两个输入的两个坐标,我们如果按照其中一个进行排序,然后想要共存,另一个坐标就是单增关系,不然就会交叉。再结合题目要求的最多能建多少条桥,那么不就是最长上升子序列。然后就是套模板就能写出来了。
#include
using namespace std;
int f[10000];
int main()
{
int n;
scanf("%d",&n);
vector>p;
for(int i=1;i<=n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
p.push_back({x,y});
}
sort(p.begin(),p.end());
for(int i=0;i
ps:这个题最关键的入手点就是看看能被保留下来的边有什么关系,进而发现对应的单增关系,进而结合最多,联系到LIS。对于其他题目也可以从这种方向来入手,如果直观的没什么想法,那么不妨看看结果之间有没有什么规律,这里又可以细分,先看存在于结果中的东西有没有什么规律,如果不通就看看不存在结果中的东西有什么规律。
1016. 最大上升子序列和(活动 - AcWing)
思路:这题虽然不是直接求最长上升子序列长度,但实际也比较裸,就是找上升子序列和的最大值。 客观来讲,和LIS模型大差不差。
状态表示:f[i]表示以i结尾的上升子序列的和的最大值。
状态划分:我们计算状态可以就需要考虑当前的元素接在谁后面,那么就是遍历即可。
因为这道题要求的是和,而非最大长度,所以贪心方法就不太方便。
#include
using namespace std;
int f[1010],a[1010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i=1;i<=n;i++)
{
f[i]=a[i];
for(int j=1;j
1010. 拦截导弹(活动 - AcWing)
思路:我们首先来看这个系统,它通过发射炮弹来拦截,发射的炮弹是单减的,所以能拦截的炮弹也是单减的,那么就很显然 我们要从待拦截的序列中找出最长的单减序列,那么就是第一问要求的。
关于第二问,我们再抽象一点,相当于对原序列进行划分,划分出来的每个序列内相对顺序不能改变,而且都要是严格单减的,问最少能划分出来多少个。那么对于一步具体的操作就是考虑它是单独开一个系统还是接在某个系统后面。如果我们用动态规划来解,我们只能通过循环找出它接在某个系统后面而产生的某个属性(如以它为结尾的最大值、最小值、数量等),但是对于包含所有情况最少需要几个系统,动态规划是无法解决的。那么我们就来看贪心的思路。这里贪心和前面一样,在操作当前元素之前产生的不只有一个序列,所以它面临的不是放与不放问题,因为每个点都是要放的,它面临的是放在哪里的问题。
很显然如果当前已经产生的序列的末尾全都小于它,肯定是必须新开的,因为没有地方可以放。但是如果有一些序列的末尾大于它,那么就可以接在它们后面,这样产生的新系统更少。那么接在谁后面,显然,它接上后,相当于减少了末尾的值,那么最优的情况肯定是接在一个让末尾减少最少的情况后面。那么就是找出所有大于它的末尾中的最小值,更改这个值。这里和前面模型中提到的贪心做法实际上大差不差,我们就不证明什么末尾元素构成的序列是单调的了。那么实际上就出来了。
用贪心算法来思考这类问题时一定要注意我们想要的的那个元素的下标究竟是越大还是越小,虽然我们比较的是元素,但二分实际查找的是下标,不理清楚很容易出bug。
刚好复习下二分,我们将两个贪心的过程都具体写一下:
首先是找最长单减序列,我们可以倒序访问找单增,也可以正序访问找单减,另外需要注意这里是不大于,所以可以取等。单增好说,我们考虑下找非单增怎么写(也就是正着写,用贪心法):
单减就要插到大于等于它的最小的元素后面,插完长度边长,但尾元素变小,所以序列应该是单减的关系,找出大于等于目标元素的最大位置(因为单减),所以符合要求的时候,应该是左指针右移。考虑边界,全部小于它访问的是0位置,0位置置空,不用特判。
//序列单减
int len=0;
q[0]=-2e9;
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l>1;
if(q[mid]>=a[i]) l=mid;
else r=mid-1;
}
len=max(len,r+1);
q[r+1]=a[i];
}
然后再考虑找那个系统个数,我们这里就不用对长度进行更新了,所以只是需要维护一个单调序列即可,每次插入大于等于它的最大位置,len什么时候更新呢,与上面不同,上面的len表示的是序列长度,这里的len表示的是序列个数,所以,只有全部小于它的时候,才会更新len,那就是找不到的时候,显然应该维护单增序列,因为要通过下标表示需要新增,全部小于的时候就会查到位置len,但是len置空无影响,每次数据就接在序列后边,序列下标不表示长度,所以不用增加,直接修改即可。
len=0;//位置len其实相当于置空
q[0]=-2e9;
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l>1;
if(q[mid]>=a[i]) r=mid;
else l=mid+1;
}
len=max(len,r+1);
q[r]=a[i];
}
两个的区别就在于len的意义和数组下标的意义,第一个len表示区间最大长度,第二个len表示区间个数,所以判增的条件不一样,上一个只有长度变长,才需要增加,那么就是找到最后一个元素,最后一个元素应该是序列最长的,所以才会增加,那么就应该单减,而且0位置是置空的;但是第二个下标只表示是第几个区间,判增的条件就是所有的末尾都小于它,那么就是找到最后一个元素时,最后一个元素应该最大,所以维护单增序列,而且因为我们的序列序号是从0开始的,所以len位置是置空状态,刚好没影响,找到最后一个元素也即找到len,len置空,填数后确实该自增,否则找到的就是len-1位置,虽然是序列最后一个,但是加1仍为len。
这里可以抽象出动态维护多个序列,而且每个序列的单调性相同或者只需要考虑尾元素的做法。根据目的(求序列个数还是序列最大长度,个数就是全部不符合会找到最后一个位置,最大长度就是全部不符合的话找到第一个位置,因为下标表长度,需要重开,那么肯定找到0位置。这里所说的位置是置空的。)判断维护一个什么样的序列,然后只维护末尾元素即可。
回归到这个题,该分析的都差不多了,现给出完整代码:
#include
using namespace std;
int a[1010],f[1010],q[1010];
int main()
{
int x,n=0;
while(~scanf("%d",&x))
{
a[++n]=x;
}
//序列单减
int len=0;
q[0]=-2e9;
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l>1;
if(q[mid]>=a[i]) l=mid;
else r=mid-1;
}
len=max(len,r+1);
q[r+1]=a[i];
}
cout<>1;
if(q[mid]>=a[i]) r=mid;
else l=mid+1;
}
len=max(len,r+1);
q[r]=a[i];
}
cout<
187. 导弹防御系统(187. 导弹防御系统 - AcWing题库)
这道题看似也是拦导弹,但却很不一样。因为这里可以用的序列是严格单增或者严格单减的,也就是说,我们需要将原序列拆成若干个序列,每个序列中的相对位置不变,然后每个序列要么严格单增,要么严格单减。
这里很麻烦的地方在于无法确定当前元素到底是放在单增的序列中,还是放在单减的序列中。只能dfs暴搜。 而且状态一定要清干净。
另外还要考虑维护单增单减序列的数组的增减,因为这里存的是末尾元素,但下标只是表示第几个,所以我们可以发现对于单增序列新增序列就是所有的都大于它,那么从前往后遍历到最后一个位置退出。序列应该是单减的,一旦有小于它的,就立即接在那个元素后面。(贪心可知,这里最优)对于单减序列新增就是所有元素都小于它,那么数组单增,从前往后,一旦遍历到大于它的就立刻接(贪心可知最优)。
#include
using namespace std;
int a[100],u[100],d[100];
int ans;
int n;
void dfs(int k,int z,int j)
{
if(z+j>=ans) return;
if(k==n)//0-(n-1)都访问过了
{
ans=min(ans,z+j);
return;
}
//对于上升序列,接上后变大,找不到则全部大于等于,故而数组单减
int i=0;
while(i=d[i]) i++;
if(i==j)
{
d[i]=a[k];
dfs(k+1,z,j+1);
}
else
{
int t=d[i];
d[i]=a[k];
dfs(k+1,z,j);
d[i]=t;
}
}
int main()
{
while(~scanf("%d",&n))
{
if(!n) break;
for(int i=0;i
ps:这实际是一道贪心的题目,不过是由这边的题目引申出来的,所以放在这里。
272. 最长公共上升子序列(活动 - AcWing)
思路:虽然有一个情景,但实际上还是比较裸的题目。就是求最长公共上升子序列。最长上升子序列好求,但是这里涉及到公共,所以我们不能直接套模板。最长公共子序列(LCS)也是一类问题,我们先来看一下LCS问题再来讨论这题。
897. 最长公共子序列(活动 - AcWing)
思路:最长公共子序列求出一个子序列,既是A的子序列,又是B的子序列,同时是所有满足条件的序列中最长的。
先要考虑状态的表示,我们来看如果当前选了A中的第i个元素和B中的第j个元素,那么后面的判断需不需要考虑a[i]和b[j]的影响,显然是不用的,因为仅仅两个数组之间有关系,子序列内部是没有关系的,所以我们可以定义dp[i][j]表示访问到a的前i个元素和b的前j个元素的时候,此时产生的最长公共子序列长度。
然后再来考虑状态计算,这里就不看是从哪里转移了,就看当前这一步,a[i]和b[j]的选与不选,总共能产生四种情况:
都不选:dp[i][j]=dp[i-1][j-1]
这个很好说,就是a[1]-a[i-1],b[1]-b[j-1]即可
选a不选b:dp[i][j]=dp[i][j-1]
注意到,dp[i][j-1]实际包含了两部分,包含a[i]不含b[j]和不包含a[i]不包含b[j]的两种情况,并不一定能保证包含a[i],看似不够严谨,但我们的集合划分原则是一定不能遗漏,但是求最大值的时候可以重复,而且这里的值是赋给dp[i][j]的,那么既是比我们预期的多了一部分,但是它只要每超过dp[i][j]的范围,为什么不可以呢,反正四个值都是赋给dp[i][j]的,重复也无所谓,下面的思路同理。
选b不选a:dp[i][j]=dp[i-1][j]
同上。
都选:dp[i][j]=dp[i-1][j-1]+1
那就是从前面找到最大的,然后加上它俩即可。
我们用到的状态一定要是前面更新过的状态,所以这里不要钻牛角尖说不如直接写dp[i][j],dp[i][j]此时算未知的值呀。
那么状态表示和状态划分都想清楚了,这道题实际也就很简单了。 而且如果只求最大公共子序列的话,我们可以发现都不选的情况被涵盖在后面的两种只选一个的情况中,所以实际上第一种情况可以不写出来。
#include
using namespace std;
int dp[1010][1010];
char a[1010],b[1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
scanf("%s%s",a+1,b+1);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a[i]==b[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
}
}
printf("%d",dp[n][m]);
}
那我们回到最长公共上升子序列的问题来。
既要最长,又要公共。我们对比下两者状态表示的定义:
定义f[i]表示以元素i结尾的上升子序列的长度集合
定义dp[i][j]表示访问到a的前i个元素和b的前j个元素的时候,此时产生的最长公共子序列长度,
发现两者对于结尾的要求是不同的,一个需要将结尾元素确切的用到,一个又不需要。那么我们中和一下,定义dp[i][j]表示从前i个和前j个元素中选,包含b[j]的最长公共上升子序列的长度,值表示这些长度中的最大值。
然后来思考状态划分:
现在已经定下来了,一定包含b[j],那么就将公共自序列的四种状态压缩成了两种,现在的步骤就是看含不含a[i]:
不含a[i]:那么就是从a的前i-1个字母和b的前j个字母中选,以b[j]结尾的最长公共上升子序列,那么就是dp[i-1][j]。
含a[i]:那么a[i]需要等于b[j],然后我们来考虑从前i个和前j个中选,包含a[i]和b[j],那么就要看倒数第二位在哪里,由此划分状态。如果倒数第二位是b[k],那么就是从前i-1个和前k个中选,以b[k]为结尾的上升子序列的长度+1,即dp[i-1][k]+1
我们暴力来写一下:
#include
using namespace std;
int dp[3010][3010],a[3010],b[3010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=dp[i-1][j];
if(a[i]==b[j])
{
dp[i][j]=max(dp[i][j],1);
for(int k=1;k
三重循环,时间复杂度显然有点高。所以我们来考虑优化:
我们注意到第三重循环中的b[j]可以换成a[i],因为此时两者相等,那么,实际上第三重循环就不用嵌套在第二重循环内。
for(int i=1;i<=n;i++)
{int mx=1;
for(int k=1;k
{
if(b[k] }
for(int j=1;j<=n;j++)
{
dp[i][j]=dp[i-1][j];
if(a[i]==b[j])
{
dp[i][j]=max(dp[i][j],mx);
}
}
}
但是这里显然k的范围还要用j来限制,所以我们来考虑这层循环的实际意义,求的是满足小于a[i]的b[k]对应的f[i-1][k]的最大值,我们每次循环j的时候i是固定的,那么岂不是可以在第二层循环中同步计算这个值,也即:
for(int i=1;i<=n;i++)
{
int mx=1;
for(int j=1;j<=n;j++)
{
dp[i][j]=dp[i-1][j];
if(a[i]==b[j]) dp[i][j]=max(dp[i][j],mx);
if(b[j]
那么时间复杂度就降低了,完整代码:
#include
using namespace std;
int dp[3010][3010],a[3010],b[3010];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
int mx=1;
for(int j=1;j<=n;j++)
{
dp[i][j]=dp[i-1][j];
if(a[i]==b[j]) dp[i][j]=max(dp[i][j],mx);
if(b[j]