线性动态规划总结--最长上升子序列

线性动态规划总结–最长上升子序列

线性动态规划的相关问题主要有以下几个分类:

  1. 最长上升子序列(LIS)和最长下降子序列
  2. 最长字段和
  3. 最长公共子序列(LCS)

1.最长上升子序列

首先讲一下子序列和子串的区别:

假设给定“acdefsh"

  • 子序列:“adfs”,“cefh”,“ce”,字符串不需要连在一起,但是字符串之间的相对位置不能改变。
  • 子串:“acde",“defs”,“sh”,字符串必须连在一起

最长上升子序列,顾名思义,就是在一组序列中,最长的并且呈上升趋势的子序列。例如给定序列a1,a2,a3,a4,并且a1<a2<a3<a4,那么称这个序列为上升的。给定序列(a1=2,a2=4,a3=1,a4=5,a5=2),有上升子序列(a1,a2,a4),(a2,a4),(a3,a5),但是最长上升子序列是(a1,a2,a4)。
最长上升子序列可以有很多个,但是长度一定是固定的。
最长上升子序列相关的题目主要有三种做法,下面主要写一下其中两种:

  1. 使用dp动态规划
  2. 使用贪心+二分的思想

1.DP动态规划:

代码:

#include<bits/stdc++.h>
using namespace std;
int N[100];
int dp[100];
int M;
int ans=1;
int main()
{
 	cin>>M;
 	for(int i=1;i<=M;i++)
  		cin>>N[i];
  
	for(int i=1;i<=M;i++)
  		dp[i]=1;
  
 	for(int i=1;i<=M;i++)
 	{
  		for(int j=1;j<i;j++)
  		{
   			if(N[j]<N[i])
   			{
    				dp[i]=max(dp[j]+1,dp[i]);
   				 ans=max(ans,dp[i]);
   			}
  		}
	}
 	cout<<ans<<endl;
 
} 

假设给定一组长度N为5的序列(1,4,2,7,6),求出给定序列的最长上升子序列。

首先开一个一维数组dp[6],dp[i]表示在以第i个数为结尾的状态下,最长上升子序列是多少。例如:dp[2]=2表示在给定序列中以第二个数4为结尾时,最长上升子序列是2,因为4前面有个1,最后,找出dp[i]中最大的数,这个数就是这个序列的最长上升子序列。

状态转移方程:

dp[i]=max(dp[j]+1,dp[i])
//以第i个数为结尾时的最长上升子序列

那么就有以下的情况:

i=1:
dp[1]=1
i=2:
j=1:N[1]<N[2]
dp[2]=max(dp[1]+1,dp[2])=max(1+1,1)=2

i=3:
j=1:N[1]<N[3]:
dp[3]=max(dp[1]+1,dp[3])=max(1+1,1)=2。
j=2:N[2]>N[3]:
直接跳过。

i=4:
j=1:N[1]<N[4]:
dp[4]=max(dp[1]+1,dp[4])=max(1+1,1)=2
j=2:N[2]<N[4]:
dp[4]=max(dp[2]+1,dp[4])=max(2+1,2)=3
j=3:N[3]<N[4]:
dp[4]=max(dp[3]+1,dp[4])=max(2+1,3)=3

i=5:
j=1:N[1]<N[5]:
dp[5]=max(dp[1]+1,dp[5])=max(1+1,1)=2
j=2:N[2]<N[5]:
dp[5]=max(dp[2]+1,dp[5])=max(2+1,2)=3
j=3:N[3]<N[5]:
dp[5]=max(dp[3]+1,dp[5])=max(2+1,3)=3
j=4:N[4]>N[5]
直接跳过

2.贪心+二分

代码:

#include<bits/stdc++.h>
using namespace std;
int N[100];
int low[100];
int K;
int m_len;
void Find(int x,int l,int r)
{
	 int mid=0;
	 while(l<=r)
	 {
	  	mid=(l+r)/2;
	  	if(x>=low[mid])
	  	{
	   		l=mid+1;
	  	}
	  	else if(x<low[mid])
	  	{
	   		r=mid-1;
	  	}
	 }
	 low[mid]=x;
	 
}
int main()
{
 	cin>>K;
 	for(int i=1;i<=K;i++)
  		cin>>N[i];
 
 	for(int i=1;i<=K;i++)
 	{
  		if(N[i]>low[m_len])
   			low[++m_len]=N[i];
  		else 
   			Find(N[i],1,m_len);
 	}
 	cout<<m_len<<endl;
}

在这个方法中,我们使用一个辅助数组low[m_len]来帮忙。其实从上一个方法可以观察到,dp[j](以N[j]为尾数的最长上升子序列)是在所有满足N[t]<N[i] (1<=t<i ) 的dp[1]到dp[i-1]中最大的一个数。所以low[m_len]就记录了这样一种关系,m_len表示当前序列的最长上升子序列,low[i](0<=i<=m_len)表示在所有上升子序列为i的情况下,第i个数的最小值。例如,一个序列有两个长度为3的上升序列:(1,5,7)和(2,3,6),low[3]=6,因为6是这两个长度为3的上升子序列的第3个数的最小值。因此,就能够保证low[m_len]在所有长度为m_len的最长上升子序列的情况下,是所有第m_len个数的最小值。同时,low数组是一个单调上升的序列。

如果N[i+1]>low[m_len],low[++m_len]=N[i+1]。因为N[i+1]比当前最长上升子序列的最大值还大,所以m_len加上1,即最长上升子序列加1,长度为m_len的上升子序列的最大数也赋值了N[i+1]。

如果N[i+1]<=low[m_len],那么就找到第一个>=N[i+1]的数,然后替换掉它。例如,low[4]=(1,4,5,8), N[i+1]=3, N[i+1]<low[4], low[2]=4是第一个大于N[i+1]的数,所以low[2]=N[i+1]=3,意味着长度为在所有长度为2的上升子序列中,3是结尾最小的数。因为low数组是一个单调上升的序列,所以可以用二分来查找这个数。

假设有一组长度为N=7的序列:1 5 3 6 4 2 6求最长上升子序列。

i=1, m_len=0:
N[1]>low[m_len], low[++m_len]=N[1]=1
low[1]={1}

i=2,m_len=1:
N[2]>low[m_len],low[++m_len]=N[2]=5
low[2]={1,5}

i=3,m_len=2:
N[3]<=low[m_len], low[2]=N[3]=3 (因为low[2]是第一个比N[3]大的数)
low[2]={1,3}

i=4,m_len=2:
N[4]>low[m_len],low[++m_len]=N[4]=6
low[3]={1,3,6}

i=5,m_len=3:
N[5]<=low[m_len], low[3]=N[5]=4 (因为low[3]是第一个比N[5]大的数)
low[3]={1,3,4}

i=6,m_len=3:
N[6]<=low[m_len], low[2]=N[6]=2 (low[2]是第一个比N[6]大的数)
low[3]={1,2,4}

i=7,m_len=3:
N[7]>low[m_len], low[++m_len]=N[7]=6
low[4]={1,2,4,6}

最后输出m_len=4,最长上升子序列为4

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页