题目
「POI2011 R2 Day1」差值 Difference
分析
考虑枚举两个字符分别作为子序列的出现次数最多和最少的字符。一个性质是,这两个字符到底是不是次数最多或最少的字符并不重要,我们只需要统计最大差值,就能自动避免不符合要求的情况(因为不符合要求的情况一定比大差值小)。
简单来说,枚举字符
a
a
a,
b
b
b和区间
[
l
,
r
]
[l, r]
[l,r],计算
[
l
,
r
]
[l, r]
[l,r]中
a
a
a和
b
b
b的个数
c
n
t
a
cnt_a
cnta和
c
n
t
b
cnt_b
cntb,
a
n
s
=
max
{
c
n
t
a
−
c
n
t
b
}
ans = \max\{cnt_a - cnt_b\}
ans=max{cnta−cntb}。
显然这样是
O
(
2
6
2
n
2
)
O(26^2n^2)
O(262n2)的,需要优化。枚举了
a
,
b
a, b
a,b过后,若把所有
a
a
a视作
1
1
1、
b
b
b视作
−
1
-1
−1、其他字符视作
0
0
0,那么问题变为求最大子段和,记录前面的DP最大值即可,
O
(
2
6
2
n
)
O(26^2n)
O(262n)。但是,最大的问题在于要求子段中
a
,
b
a, b
a,b的数量都要大于
0
0
0,所以这种方法比较难以解决。
考虑换一种方式,先枚举 a a a,然后同时对所有 b b b做DP,定义 d p [ b ] [ i ] dp[b][i] dp[b][i]表示 [ 1 , i ] [1, i] [1,i]的所有子段中, c n t a − c n t b cnt_a - cnt_b cnta−cntb的最大值,为了确保这当中的 a , b a, b a,b数量均大于 0 0 0,考虑记录两个东西:
- d p [ b ] [ i ] [ 0 ] dp[b][i][0] dp[b][i][0]: [ 1 , i ] [1, i] [1,i]的所有子段中, c n t a − c n t b cnt_a - cnt_b cnta−cntb的最大值,要求 c n t b ≠ 0 cnt_b \neq 0 cntb=0;
- d p [ b ] [ i ] [ 1 ] dp[b][i][1] dp[b][i][1]: [ 1 , i ] [1, i] [1,i]的所有子段中, c n t a − c n t b cnt_a - cnt_b cnta−cntb的最大值, c n t b cnt_b cntb可以为 0 0 0。
注意 c n t a cnt_a cnta没有必要做规定,因为如果 c n t a cnt_a cnta为 0 0 0,算出来的必定是非正数,而 a n s ans ans的初值肯定是 0 0 0,不会更新答案。
然后对当前字符分两种情况转移:
- s i = a s_i = a si=a,所有 d p [ c ] [ i ] [ 0 / 1 ] dp[c][i][0/1] dp[c][i][0/1]的值 + 1 +1 +1,这样肯定更优;
-
s
i
≠
a
s_i \neq a
si=a,那么考虑更新
d
p
[
s
i
]
[
i
]
[
0
/
1
]
dp[s_i][i][0/1]
dp[si][i][0/1]的值:
- d p [ s i ] [ i ] [ 0 ] = d p [ s i ] [ i − 1 ] [ 1 ] − 1 dp[s_i][i][0] = dp[s_i][i - 1][1] - 1 dp[si][i][0]=dp[si][i−1][1]−1,将 s j s_j sj加入最优解,这样还是最优解,因为 d p [ s i ] [ i − 1 ] [ 1 ] dp[s_i][i - 1][1] dp[si][i−1][1]是所有情况的最优解;
- d p [ s i ] [ i ] [ 1 ] = max { d p [ s i ] [ i − 1 ] [ 1 ] − 1 , 0 } dp[s_i][i][1] = \max\{dp[s_i][i - 1][1] - 1, 0\} dp[si][i][1]=max{dp[si][i−1][1]−1,0},如果小于 0 0 0了,就什么都不要了(因为 c n t b cnt_b cntb可以为 0 0 0),重新计数。
注意一下初值: d p [ c ] [ 0 ] = − ∞ , d p [ c ] [ 1 ] = 0 dp[c][0] = -\infty, dp[c][1] = 0 dp[c][0]=−∞,dp[c][1]=0。然后时刻更新答案即可。
时间复杂度 O ( 2 6 2 n ) O(26^2n) O(262n),但实际上会小一些,因为只有上面的情况一会多一个 26 26 26。
代码
可以滚动一下数组。
#include <algorithm>
#include <cstdio>
const int MAXN = 1000000;
const int INF = 0x3f3f3f3f;
int N, Dp[30][2]; // 0 必须出现过 / 1 可以没出现过
char S[MAXN + 5];
int main() {
scanf("%d%s", &N, S + 1);
int Ans = 0;
for (int x = 0; x < 26; x++) {
for (int y = 0; y < 26; y++)
Dp[y][0] = -INF, Dp[y][1] = 0;
for (int i = 1; i <= N; i++) {
if (S[i] == x + 'a') {
for (int y = 0; y < 26; y++) {
Dp[y][0]++, Dp[y][1]++;
Ans = std::max(Ans, Dp[y][0]);
}
}
else {
int y = S[i] - 'a';
Dp[y][0] = Dp[y][1] - 1; // 将 y 补到后面
Dp[y][1] = std::max(0, Dp[y][1] - 1); // 可以不要当前的 y, 重新计数
Ans = std::max(Ans, Dp[y][0]);
}
}
}
printf("%d", Ans);
return 0;
}