学习笔记:子序列模型

最长上升/不上升/下降/不下降子序列

概念

上升序列,指一个序列中每一个数都是递增的。最长上升子序列,就是一个序列中找出最长的一个每一个数都递增的子序列,其余的同理。比如: 12   9   31   42   23   19   21   73 12 9 31 42 23 19 21 73 12 9 31 42 23 19 21 73一个上升子序列: 12   31   73 12 31 73 12 31 73最长上升子序列: 12   19   21   73 12 19 21 73 12 19 21 73这个也是: 9   31   42   73 9 31 42 73 9 31 42 73所以,最长上升子序列不是唯一的。这个最长上升子序列的长度是 4 4 4。特殊的,空序列也是上升/不上升/下降/不下降子序列。

方法

这里以上升子序列为例。可以思考,当一个序列结束的一个数小于该数,则该数可以加在这个序列上。于是,方案就出来了:设 f i f_i fi为以 i i i结束的最长上升子序列,那么 f i = max ⁡ ( f j , j < i , a [ j ] < a [ i ] ) + 1 f_i=\max(f_j,jfi=max(fj,j<i,a[j]<a[i])+1。其余的同理。

例题

AcWing 1017

这个题可以发现,如果从左到右就是一个最长上升子序列问题。那么从右到左呢?因为这个题数据不大,可以从右往左再做一遍。当然,也不难发现,如果从右往左是上升子序列,那么从左往右看就是下降子序列。于是,本题就能解决了。

#include
using namespace std;
const int NN=104;
int a[NN],f1[NN],f2[NN];
int main()
{
	int k;
	scanf("%d",&k);
	while(k--)
	{
		int n,ans=0;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			f1[i]=f2[i]=1;
			for(int j=1;j<=i-1;j++)
			{
				if(a[j]>a[i])
					f1[i]=max(f1[i],f1[j]+1);
				if(a[j]<a[i])
					f2[i]=max(f2[i],f2[j]+1);
			}
			ans=max(ans,max(f1[i],f2[i]));
		}
		printf("%d\n",ans);
	}
	return 0;
}

AcWing 1014

这个题发现不太对,居然是先上升后下降的样子。但是我们可以把它拆成两个部分:上升部分和下降部分。所以我们可以枚举一个分界点,前面上升后面下降。这样就是以 i i i结束的最长上升和下降子序列问题,直接相加就行了。注意,中间点 i i i被计算了两次,要减一。但是上一题处理下降子序列的简便方式在这里并不适用,因为那个存的是 i i i开始的最长下降子序列,这个题是要求 i i i结束的,所以只能从右往左再算一遍。

#include
using namespace std;
const int NN=1004;
int a[NN],f1[NN],f2[NN];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		f1[i]=1;
		for(int j=1;j<=i-1;j++)
			if(a[j]<a[i])
				f1[i]=max(f1[i],f1[j]+1);
	}
	for(int i=n;i>=1;i--)
	{
		f2[i]=1;
		for(int j=i+1;j<=n;j++)
			if(a[j]<a[i])
				f2[i]=max(f2[i],f2[j]+1);
	}
	for(int i=1;i<=n;i++)
		ans=max(f1[i]+f2[i]-1,ans);
	printf("%d\n",ans);
	return 0;
}

AcWing 482

这个题和上一题没有任何区别。出队的越少留下的就越多,用 n n n减去最长的一个上升又下降的子序列即可。

#include
using namespace std;
const int NN=1004;
int a[NN],f1[NN],f2[NN];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		f1[i]=1;
		for(int j=1;j<=i-1;j++)
			if(a[j]<a[i])
				f1[i]=max(f1[i],f1[j]+1);
	}
	for(int i=n;i>=1;i--)
	{
		f2[i]=1;
		for(int j=i+1;j<=n;j++)
			if(a[j]<a[i])
				f2[i]=max(f2[i],f2[j]+1);
	}
	for(int i=1;i<=n;i++)
		ans=max(f1[i]+f2[i]-1,ans);
	printf("%d\n",n-ans);
	return 0;
}

AcWing 1012

这个题目感觉和最长上升子序列没有关系,但是我们发现,两个相交当且仅当 u i > u j u_i>u_j ui>uj v i < v j v_ivi<vj或者符号反着。所以,我们给 u u u从小到大排序,则选出的 v v v一定是上升的。所以,就变成了求由 v v v组成的最长上升子序列问题。

#include
using namespace std;
const int NN=5004;
pair<int,int>a[NN];
int f[NN];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&a[i].first,&a[i].second);
	sort(a+1,a+1+n);
    int ans=0;
	for(int i=1;i<=n;i++)
	{
		f[i]=1;
		for(int j=1;j<i;j++)
			if(a[j].second<a[i].second)
			    f[i]=max(f[i],f[j]+1);
	    ans=max(ans,f[i]);
	}
	printf("%d",ans);
	return 0;
}

AcWing 1016

这个题从原来的求最长上升子序列变成了求和最大的上升子序列。之前新加一个数 a i a_i ai子序列的长度长度加 1 1 1,现在和却增加了 a i a_i ai,所以状态转移方程就变成了 max ⁡ ( f j , a j < a i , j < i ) + a i \max(f_j,a_jmax(fj,aj<ai,j<i)+ai,只用自己(边界条件)也是 f i = a i f_i=a_i fi=ai

#include
using namespace std;
const int NN=1004;
int a[NN],f[NN];
int main()
{
	int n,ans=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
	    scanf("%d",&a[i]);
		f[i]=a[i];
		for(int j=1;j<i;j++)
			if(a[j]<a[i])
			    f[i]=max(f[i],f[j]+a[i]);
		ans=max(ans,f[i]);
	}
	printf("%d",ans);
	return 0;
}

AcWing 1010

这个题第一问就是一个典型的最长不上升子序列,直接 d p dp dp解决。那么第二问呢?可以记录每一个防御系统最后一次处理的高度,如果没有能处理新的的防御系统那么就新加一个。但是如果有选用哪个呢?选最小的,因为其他的还可以处理更高的,所以就用一个"最差的"就行了。

#include
using namespace std;
const int NN=1004;
int a[NN],f[NN],h[NN];
int main()
{
	int i=0,ans=0,num=0;
	h[0]=1e9;
	while(scanf("%d",&a[++i])!=EOF)
	{
	    f[i]=1;
		for(int j=1;j<i;j++)
			if(a[j]>=a[i])
				f[i]=max(f[i],f[j]+1);
		ans=max(ans,f[i]);
		int k=0;
		for(int j=1;j<=num;j++)
			if(h[j]>=a[i]&&h[j]<h[k])
				k=j;
		if(!k)
		{
			num++;
			k=num;
		}
		h[k]=a[i];
	}
	printf("%d\n%d",ans,num);
	return 0;
}

AcWing 187

这个题目可以考虑上一题那道题的贪心算法。但是这个题目可以选择上升或者下降,贪心无法在新加一个时确定是上升还是下降。于是我们再分析,发现题目中 n n n很小,不难想到可以暴搜。每次两种决策,在下降的选一个或者新加,在上升的选一个或者新加。这样分成了两种,就可以分别贪心了。注意,如果当前用的个数大于答案就可以退出了,因为最小的一定会是这个方案。用 u u u表示上升的序列的个数, d d d表示下降, t t t表示处理第几个。

#include
using namespace std;
const int NN=54;
int a[NN],up[NN],down[NN],n,ans;
void dfs(int u,int d,int t)
{
    if(u+d>=ans)
        return;
    if(t==n)
    {
        ans=u+d;
        return;
    }
    int k=u+1;
    for(int i=1;i<=u;i++)
        if(up[i]<a[t])
        {
            k=i;
            break;
        }
    int temp=up[k];
    up[k]=a[t];
    dfs(max(u,k),d,t+1);
    up[k]=temp;
    k=d+1;
    for(int i=1;i<=d;i++)
        if(down[i]>a[t])
        {
            k=i;
            break;
        }
    temp=down[k];
    down[k]=a[t];
    dfs(u,max(d,k),t+1);
    down[k]=temp;
}
int main()
{
    while(scanf("%d",&n)!=EOF&&n)
    {
        ans=1e9;
        for(int i=0;i<n;i++)
            scanf("%d",&a[i]);
        dfs(0,0,0);
        printf("%d\n",ans);
    }
    return 0;
}

最长公共子序列

概念

两个序列,分别选出一个子序列,要求这两个子序列相等,这就是公共子序列。最长公共子序列就是最长的一个。比如: 1   6   2   4   5   8   3   7 1 6 2 4 5 8 3 7 1 6 2 4 5 8 3 7 2   6   3   5   8   7   1   4 2 6 3 5 8 7 1 4 2 6 3 5 8 7 1 4一个公共子序列: 6   3   7 6 3 7 6 3 一个最长公共子序列: 2   5   8   7 2 5 8 7 2 5 8 7这个也是: 6   5   8   7 6 5 8 7 6 5 8 7可见,最长公共子序列也不是唯一的。

方法

首先,拿到这个题很容易把这个状态表示出来: f i , j f_{i,j} fi,j表示 s 1 1... i s1_{1...i} s11...i s 2 1... j s2_{1...j} s21...j的最长公共子序列长度。于是,我们就会出现几种情况:

  1. 都不用: f i , j = f i − 1 , j − 1 f_{i,j}=f_{i-1,j-1} fi,j=fi1,j1
  2. 不用 s 1 i s1_i s1i f i , j = f i − 1 , j f_{i,j}=f_{i-1,j} fi,j=fi1,j
  3. 不用 s 2 j s2_j s2j f i , j = f i , j − 1 f_{i,j}=f_{i,j-1} fi,j=fi,j1
  4. 都用:这种情况只有两个相等才可以。所以,就是都不用的长度加一。 f i , j = f i − 1 , j − 1 + 1 f_{i,j}=f_{i-1,j-1}+1 fi,j=fi1,j1+1

然后我们根据这个来写代码是能得到正确答案的。但是仔细思考会发现一个事:有一些状态之间是包含了的。比如第一条,其实就被第二条和第三条包含了。因为之前 f i − 1 , j f_{i-1,j} fi1,j在之前用转移三,则迭代成了 f i − 1 , j − 1 f_{i-1,j-1} fi1,j1,第三条同理。所以,我们可以去掉第一条。那么再考虑,发现第四条肯定是最优的,因为 f i − 1 , j f_{i-1,j} fi1,j可以从 f i − 2 , j f_{i-2,j} fi2,j f i − 1 , j − 1 f_{i-1,j-1} fi1,j1以及 f i − 2 , j − 1 + 1 f_{i-2,j-1}+1 fi2,j1+1迭代,其中第二个绝对不可能比转移四优,第三个其实少一个字符参加计算肯定也不会更有。第一个只要一出现 j − 1 j-1 j1就一定不是最优,那么只能一直 f i − k , j f_{i-k,j} fik,j地迭代,所以最后会迭代到 f 0 , j f_{0,j} f0,j,为 0 0 0,肯定不是最优。所以,当两个相等直接计算第四个状态转移方程即可。

例题

AcWing 272

这个居然还加了一个上升的条件,那么怎么办呢?我们可以思考一下,这不就是最长上升子序列和最长公共子序列的结合体吗?所以我们可以结合一下,首先,最长上升子序列的状态是 f i f_i fi表示以 i i i结束的最长上升子序列,最长公共子序列的状态是 f i , j f_{i,j} fi,j表示 s 1 1... i s1_{1...i} s11...i s 2 1... j s2_{1...j} s21...j的最长公共子序列。所以,我们可以设一个状态 f i , j f_{i,j} fi,j表示用 s 1 1... i s1_{1...i} s11...i且以 s 2 j s2_j s2j结束的一个最长公共上升子序列。因为公共子序列是一样的,所以只用一个表示以某个结束即可。思考状态转移, f i , j f_{i,j} fi,j首先可以转移成不用 s 2 i s2_i s2i,则 f i , j = f i − 1 , j f_{i,j}=f_{i-1,j} fi,j=fi1,j,但是第二个要求以 s 2 j s2_j s2j结束,所以不能不用 s 2 j s2_j s2j。于是就思考,在两个相等的时候怎么转移。其实就是这个公共子序列上一个是什么,然后再补一个新加入的我们。

  1. 只有 s 2 j s2_j s2j,长度为 1 1 1
  2. 上一个是 s 2 1 s2_1 s21 f i , j = f i − 1 , 1 + 1 f_{i,j}=f_{i-1,1}+1 fi,j=fi1,1+1,要求 s 2 1 < s 2 j s2_1s21<s2j
  3. 以此类推,直到 s 2 j − 1 s2_{j-1} s2j1

所以,就可以三层循环,计算每一个 f i , j f_{i,j} fi,j。但是这个题 n n n最大是 3000 3000 3000,时间复杂度根本受不了。不难发现如果内层循环计算 s 2 s2 s2和枚举每一个 j j j,所以可以边循环边统计。因为 s 2 s2 s2放在内层循环,则数是实时变化的,但都要求等于 s 1 i s1_i s1i,所以改成和 s 1 i s1_i s1i比较是否上升即可。最后依次计算,如果两个相等就是可以进行第二条状态转移的,现在直接和统计的 max ⁡ \max max比较即可。注意要先转移完了再更新 max ⁡ \max max

#include
using namespace std;
const int NN=3004;
int a[NN],b[NN],f[NN][NN];
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 maxx=1;
        for(int j=1;j<=n;j++)
        {
            f[i][j]=f[i-1][j];
            if(a[i]==b[j])
                f[i][j]=max(f[i][j],maxx);
            if(a[i]>b[j])
                maxx=max(maxx,f[i-1][j]+1);
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)
        res=max(res,f[n][i]);
    printf("%d",res);
    return 0;
}

你可能感兴趣的:(学习笔记(提高篇),动态规划,c++,最长上升子序列,最长公共子序列,dp)