题意:给一串由n个数字组成的字符串,选择其中一个区间进行翻转,要求翻转后该字符串的最长非降子序列长度最长,输出这个最长非降子序列的长度以及翻转的区间的左右端点。
题解:由于n的大小为1e5,如果直接枚举a中的翻转位置的话,那么复杂度肯定不行,但是这里有一种十分巧妙的做法。首先如果是求一个只有数字的串中的最长上升子序列长度的话,那就是这个串与 "0123456789" 这个串的最长公共子序列。而要求最长非降的话,那就是两个串可以重复匹配,遇到相同的位置答案依然加1。有了这个预备知识,我们就可以做这道题了,因为题目上给了字符串中的元素是0~9中的,所以如果翻转后可以是升序的话,那翻转前就是存在降序了,所以我们设原字符串为a,b字符串为:"0123456789",不断枚举b数组中的所有区间被翻转的情况,然后找a数组与b数组的最长公共子序列就是答案了。为什么?因为b没翻转前就是一个递增的序列,翻转之后就有了一个性质:只要再反转一次就能变成递增的序列。但是需要注意的是题目要求的是非递减,所以当翻转b的时候要额外保留翻转的两端点在原位置。由于还要求输出翻转的左右端点,所以额外开两个二维数组分别保存左端点和右端点。
附上代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 100005;
const int maxm = 22;
int n, a[maxn], dp[maxn][maxm], b[maxm];
int ans, ansl, ansr, l, r, cnt;
int al[maxn][maxm], ar[maxn][maxm];
char s[maxn];
int solve()
{
for(int i = 0; i <= cnt; i++)
dp[0][i] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= cnt; j++)
{
dp[i][j] = dp[i - 1][j];
al[i][j] = al[i - 1][j];//al记录翻转的左端点
ar[i][j] = ar[i - 1][j];//al记录翻转的左端点
if(a[i] == b[j])
{
dp[i][j] = dp[i - 1][j] + 1;
if(l == j && !al[i][j])//如果当前的j就是b开始翻转的左端点,更新记录
al[i][j] = i;
if(r == j)//当前的j是b翻转的右端点,记录更新
ar[i][j] = i;
}
if(dp[i][j - 1] > dp[i][j])//如果答案有更新就要更新答案
{
dp[i][j] = dp[i][j - 1];
al[i][j] = al[i][j - 1];
ar[i][j] = ar[i][j - 1];
}
}
}
return dp[n][cnt];
}
int main()
{
int t;
scanf("%d", &t);
while(t--)
{
scanf("%d%s", &n, s + 1);
for(int i = 1; i <= n; i++)
a[i] = s[i] - '0';
cnt = 0;
for(int i = 0; i <= 9; i++)
b[++cnt] = i;
ansl = ansr = l = r = 1;
ans = solve();
for(int i = 0; i <= 9; i++) //枚举翻转b数组的每一段
{
for(int j = i + 1; j <= 9; j++)
{
cnt = 0;
for(int k = 0; k <= i; k++)
b[++cnt] = k;
l = cnt+1;//左端点
for(int k = j; k >= i; k--)//只翻转i~j区间的数
b[++cnt] = k;
r = cnt;//右端点
for(int k = j; k <= 9; k++)
b[++cnt] = k;
/* for(int i=1; i<=cnt; i++)
printf("%d ",b[i]);
printf("\n");*/
int tmp = solve();
if(ans < tmp && al[n][cnt] && ar[n][cnt])//不断更新答案
{
ans = tmp;
ansl = al[n][cnt], ansr = ar[n][cnt];
}
}
}
printf("%d %d %d\n", ans, ansl, ansr);
}
return 0;
}