【题解】反转子序列 | 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 训练或者比赛中是很重要的。
本文来自博客园,作者 5ab,转载请注明链接哦 qwq
博客迁移啦,来看看新博客吧 -> https://5ab-juruo.oier.space/