【题解】反转子序列 | Subsequence Reversal

题目

题目来源:[USACO17JAN] P,USACO 2017 January Contest, Platinum;20200502 模拟赛 T3。

测试地址:LG3607

题目描述

FJ 要给他的 \(N\) 头奶牛拍照,现在 \(N\) 头奶牛排成一条直线,第 \(i\) 头奶牛的身高为 \(a_i\)。在 FJ 的审美里,递增的子序列是最好看的。现在他有一次机会,挑选任意一个子序列,将整个子序列翻转。

比如奶牛最初的顺序是:\(\left<1,6,2,3,4,3,5,3,4\right>\)

FJ 挑选了一个子序列:\(\left<1,\underline{6},2,3,4,3,\underline{5},\underline{3},\underline{4}\right>\),可以得到:\(\left<1,\underline{4},2,3,4,3,\underline{3},\underline{5},\underline{6}\right>\)

在只能反转一次任意子序列的情况下,请找到不下降子序列的最大可能长度。

输入格式

第一行输入数字 \(N\)

接下来 \(N\) 行,按顺序输入每头奶牛的身高。

输出格式

输出反转一次任意子序列后所得到的不下降子序列的最大可能长度。

评测限制 and 数据范围

评测时间限制 \(1000\ \textrm{ms}\),空间限制 \(128\ \textrm{MiB}\)

对于所有数据,\(1\le N\le 50\)。保证 \(1\le a_i\le 50\)

分析

题目大意是说,给你一个数列,可以反转一个子序列,使得最长不降子序列最大。

注意到反转的是子序列而不是子串,联系其不连续性,我们就可以将其视为若干对交换并两两包含,并考虑 DP。

定义

\(f_{i,j}\) 为最后一对交换为 \(a_i\)\(a_j\) 时在 \([i,j]\) 内最长不降子序列长度。但是这个定义无法确定下一项/上一项,所以无法转移。

如果我们加上左右区间端点(也可以称为「值域」),就可以很好地转移。也就是定义 \(f_{i,j,l,r}\) 为最后一对交换为 \(a_i\)\(a_j\) 时在 \([i,j]\) 内、值均在 \([l,r]\) 内时的最长不降子序列长度。

转移

(为方便讲述,下定义 \([\textrm{Pred}]\) 为当 \(\textrm{Pred}\) 为真时,\([\textrm{Pred}]\)\(1\),反之为 \(0\)。)

首先,单纯增大值域是可以转移的,就是说 \(f_{i,j,l,r} = \max\{f_{i,j,l+1,r},f_{i,j,l,r-1}\}\)

同时,考虑到要不断扩张不降子序列(以及不是所有的数都在子序列里),所以 \(f_{i,j,l,r} = \max\{f{i+1,j,l,r}+[a_i=l],f{i,j-1,l,r}+[a_j=r]\}\)。(因为是不降序列,所以不用扩张值域)

最后,还要加上反转操作,所以 \(f_{i,j,l,r} = \max\{f_{i-1,j+1,l,r}+[a_i=r]+[a_j=l]\}\)

边界

还有一点要注意的是 DP 的边界问题。

很显然,\(f_{i,i,a_i,a_i}=1\),其余为 \(0\)。最终要求的是 \(f_{1,n,1,\max\limits_{\small{1\le i\le N}}{a_j}}\)

但可能是因为实现问题,这样的算法在求解类似 \(\left<1,2,3,4,5\right>\) 时会挂,所以初始设置时要所有 \(f_{i,i,1,\max\limits_{\small{1\le i\le N}}{a_j}}\) 都为 \(1\)

这样,我们就可以愉快地 Coding 了,复杂度 \(\Theta(N^4)\sim 50^4=6.25\times 10^6\),还是 hold 住的。

Code

除此以外,就是纯粹的代码了,还是很简单的。

#include <cstdio>
using namespace std;

const int max_n = 50;

int dp[max_n][max_n][max_n+1][max_n+1] = {}, a[max_n];

void upd(int& a, int b) { a = ((a > b)? a:b); }

int main()
{
	int n;
	
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
		scanf("%d", a + i);
	
	for (int i = 0; i < n; i++)
		for (int j = 1; j <= a[i]; j++)
			for (int k = a[i]; k <= max_n; k++)
				dp[i][i][j][k] = 1;
	
	for (int l1 = 2; l1 <= n; l1++)
		for (int l = 0, r = l1 - 1; r < n; l++, r++)
			for (int l2 = 1; l2 <= max_n; l2++)
				for (int lp = 1, rp = l2; rp <= max_n; lp++, rp++)
				{
					if (lp != max_n)
						upd(dp[l][r][lp][rp], dp[l][r][lp+1][rp]);
					upd(dp[l][r][lp][rp], dp[l][r][lp][rp-1]);
					
					upd(dp[l][r][lp][rp], dp[l+1][r][lp][rp] + (lp == a[l]));
					upd(dp[l][r][lp][rp], dp[l][r-1][lp][rp] + (rp == a[r]));
					
					upd(dp[l][r][lp][rp], dp[l+1][r-1][lp][rp] + (lp == a[r]) + (rp == a[l]));
				}
	
	printf("%d\n", dp[0][n-1][1][max_n]);
	
	return 0;
}

后记

这道题的指向性很明显,但是还是一道很不错的 DP 练手题。

当然,我们也可以从这道题中学到「加一维」的思想,也就是说如果当前的 DP 状态无法很好地容纳条件,那么就给 DP 数组加一维。这种思想在以后的 DP 训练或者比赛中是很重要的。

posted @ 2020-05-03 15:03  5ab  阅读(250)  评论(0编辑  收藏  举报