题目
题目来源:[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 ight>)。
FJ 挑选了一个子序列:(left<1,underline{6},2,3,4,3,underline{5},underline{3},underline{4} ight>),可以得到:(left<1,underline{4},2,3,4,3,underline{3},underline{5},underline{6} ight>)。
在只能反转一次任意子序列的情况下,请找到不下降子序列的最大可能长度。
输入格式
第一行输入数字 (N);
接下来 (N) 行,按顺序输入每头奶牛的身高。
输出格式
输出反转一次任意子序列后所得到的不下降子序列的最大可能长度。
评测限制 and 数据范围
评测时间限制 (1000 extrm{ms}),空间限制 (128 extrm{MiB})。
对于所有数据,(1le Nle 50)。保证 (1le a_ile 50)。
分析
题目大意是说,给你一个数列,可以反转一个子序列,使得最长不降子序列最大。
注意到反转的是子序列而不是子串,联系其不连续性,我们就可以将其视为若干对交换并两两包含,并考虑 DP。
定义
令 (f_{i,j}) 为最后一对交换为 (a_i) 和 (a_j) 时在 ([i,j]) 内最长不降子序列长度。但是这个定义无法确定下一项/上一项,所以无法转移。
如果我们加上左右区间端点(也可以称为「值域」),就可以很好地转移。也就是定义 (f_{i,j,l,r}) 为最后一对交换为 (a_i) 和 (a_j) 时在 ([i,j]) 内、值均在 ([l,r]) 内时的最长不降子序列长度。
转移
(为方便讲述,下定义 ([ extrm{Pred}]) 为当 ( extrm{Pred}) 为真时,([ extrm{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,maxlimits_{small{1le ile N}}{a_j}})。
但可能是因为实现问题,这样的算法在求解类似 (left<1,2,3,4,5 ight>) 时会挂,所以初始设置时要所有 (f_{i,i,1,maxlimits_{small{1le ile N}}{a_j}}) 都为 (1)。
这样,我们就可以愉快地 Coding 了,复杂度 (Theta(N^4)sim 50^4=6.25 imes 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
", dp[0][n-1][1][max_n]);
return 0;
}
后记
这道题的指向性很明显,但是还是一道很不错的 DP 练手题。
当然,我们也可以从这道题中学到「加一维」的思想,也就是说如果当前的 DP 状态无法很好地容纳条件,那么就给 DP 数组加一维。这种思想在以后的 DP 训练或者比赛中是很重要的。