简述题意
给定一个长度为 n n n 的字符串 ,从中选出一个子串 ,使得字串中出现次数最多的字母与出现次数最少(不能为 0 0 0)的字母的出现次数相差最大,请求出这个最大值。
暴力
暴力枚举区间的左右端点以及该区间内出现次数最多的字符和出现次数最少的字符(注意是枚举,而不是求得),然后将指定次数最多的字符的位置标记为 1 1 1,出现次数最少的字符的位置标记为 − 1 -1 −1,其余标记为 0 0 0,然后对该区间进行最大子段和求解,即为答案
枚举区间需要 n 2 n^2 n2,枚举字符需要 2 6 2 26^2 262,总时间复杂度为 O ( 2 6 2 × n 2 ) O(26^2 \times n^2) O(262×n2)。
然后,发现子段和可以用 d p dp dp 优化,但是 d p dp dp 无法保证枚举出来的两个字符被选取,所以很难直接维护。
正解
我们既然要求一个区间的最长子段和,那么就很容易联想到 dp,但一般的 d p dp dp 无法保证枚举的两个字符(即出现次数最多和最少的字符)被选取,所以我们要在正常的 d p dp dp 上进行改动。
- Hint1 \text{Hint1} Hint1:不妨分两种情况进行考虑,一个是不取出现次数最少的字符(换句话说,最少的出现次数为 0 0 0),这里就涉及到了一个细节操作,那就是我们不需要考虑出现次数最多的字符是否被取,因为如果最多出现次数为 0 0 0,所得的答案一定是非正数,无意义。
- Hint2 \text{Hint2} Hint2:我们只需枚举出现次数最多的字符,接着枚举字符串中的每个元素,然后依次更新 d p dp dp 的值即可。
- Hint3 \text{Hint3} Hint3:我们不妨定义一个三维 d p dp dp, d p i , b , 0 dp_{i,b,0} dpi,b,0( b b b 为字符)表示从 [ 1 , i ] [1,i] [1,i] 中寻找一个子串,使得其满足条件:
1、出现次数最多的字符为枚举出来的 a a a 。
2、出现次数最少的字符为当前字符 b b b 。
3、字符 b b b 必须被选取,即最少出现次数不能为 0 0 0 。
4、最多的出现次数减最少的出现次数最大。
同理定义 d p i , b , 1 dp_{i,b,1} dpi,b,1 表示从 [ 1 , i ] [1,i] [1,i] 中寻找一个子串,使得其满足条件:
1、出现次数最多的字符为枚举出来的 a a a。
2、出现次数最少的字符为当前字符 b b b。
3、字符 b b b可以不被选取,即最少出现次数可以为 0 0 0。
4、最多的出现次数减最少的出现次数最大。
- Hint4 \text{Hint4} Hint4:初始化,我们应把所有 d p i , b , 0 dp_{i,b,0} dpi,b,0 置为极小值,把 d p i , b , 1 dp_{i,b,1} dpi,b,1 置为 0 0 0。
- Hint5 \text{Hint5} Hint5:以上准备工作都有了以后,我们就可以很巧妙的把是否选取枚举出来的这两个字符的问题巧妙地解决掉了。
状态转移方程式
- 考虑当前枚举到的 s [ i ] s[i] s[i] 与出现次数最多的字符是否相等,因为我们无需将这个 s [ i ] s[i] s[i] 既当作出现次数最多的字符,又当作出现次数最少的字符,另一个原因,我们需分开转移状态
- 当
s
[
i
]
=
a
s[i]=a
s[i]=a 时,我们可以把
d
p
i
,
c
,
0
/
1
dp_{i,c,0/1}
dpi,c,0/1 都进行加
1
1
1 操作(
c
c
c 为全部小写字母),这其中涉及到两个细节
- 我们为何直接进行加 1 1 1 操作,而不比 max \max max?原因是其实原方程长这样 d p i , c , 0 / 1 = m a x ( d p i , c , 0 / 1 , d p i , c , 0 / 1 + 1 ) dp_{i,c,0/1}=max(dp_{i,c,0/1}, dp_{i,c,0/1}+1) dpi,c,0/1=max(dpi,c,0/1,dpi,c,0/1+1)。
- 我们为何没有考虑
[
1
,
i
]
[1,i]
[1,i] 中是否出现了字符
c
c
c 便对
d
p
i
,
c
,
0
dp_{i,c,0}
dpi,c,0 进行了操作,
这个问题卡了笔者许久,其实我们本应当是要考虑的,但我们可以注意到我们 d p i , c , 0 dp_{i,c,0} dpi,c,0 的初值是极小值,所以加 1 1 1 操作在极小值面前不足一提,若 [ 1 , i ] [1,i] [1,i] 中没有出现了字符 c c c,那么 d p i , c , 0 dp_{i,c,0} dpi,c,0 里存放的一定是最小值, + 1 +1 +1 操作无意义
- 当
s
[
i
]
s[i]
s[i]
≠
\not=
=
a
a
a 时,我们就要对
d
p
i
,
s
i
,
0
/
1
dp_{i,s_i,0/1}
dpi,si,0/1 进行更新
- 考虑更新 d p i , s i , 0 dp_{i,s_i,0} dpi,si,0,那么我们已经知道在 [ 1 , i ] [1,i] [1,i] 中一定有 s i s_i si 这个字符(最小出现次数一定不为 0 0 0),所以可以通过 d p i − 1 , s i , 0 / 1 dp_{i-1,s_i,0/1} dpi−1,si,0/1 转移过来,即 d p i , s [ i ] , 0 = max { d p i − 1 , s i , 0 − 1 , d p i − 1 , s i , 1 − 1 } dp_{i,s[i],0}= \max\{dp_{i-1,s_i,0}-1 , dp_{i-1,s_i,1}-1\} dpi,s[i],0=max{dpi−1,si,0−1,dpi−1,si,1−1},因为是被减数增大(最大次数减最小次数),所以是减一。
- 考虑更新 d p i , s i , 1 dp_{i,s_i,1} dpi,si,1,依旧可以从 d p i − 1 , s i , 0 / 1 dp_{i-1,s_i,0/1} dpi−1,si,0/1,而就是我们无需从 d p i − 1 , s i , 0 dp_{i-1,s_i,0} dpi−1,si,0 转移过来,因为对于任意 d p i , c , 1 dp_{i,c,1} dpi,c,1 都有 d p i , c , 1 ≥ d p i , c , 0 dp_{i,c,1} \geq dp_{i,c,0} dpi,c,1≥dpi,c,0。换句话说, d p i , c , 0 dp_{i,c,0} dpi,c,0 的情况是包含在 d p i , c , 1 dp_{i,c,1} dpi,c,1 里的,故有 d p i , s i , 1 = max { d p i − 1 , s i , 1 − 1 , 0 } dp_{i,s_i,1} = \max\{dp_{i-1,s_i,1}-1,0\} dpi,si,1=max{dpi−1,si,1−1,0},至于为何要与 0 0 0 比 m a x max max 是因为我们可以不取当前的 s [ i ] s[i] s[i],所以可以为 0 0 0。
滚动数组
显然,对于每个 d p dp dp 状态都是从上一个 d p dp dp 转移过来的,所以我们可以缩维,将 i i i 那一维略去,然后每次枚举出现次数最多的字符时进行初始化,边更新值边比 max \max max 即可,最后得到一个二维 d p c , 0 / 1 dp_{c,0/1} dpc,0/1。
代码
细节 ⋅ DP 细节·\text{DP} 细节⋅DP
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 5;
int n , dp[MAXN][2] , ans;//缩维
char s[MAXN];
int main() {
scanf("%d %s", &n , s + 1);//输入
for (int k = 0 ; k < 26 ; k ++) {
for (int j = 0 ; j < 26 ; j ++) dp[j][0] = -0x3f3f3f3f , dp[j][1] = 0;//对于每次操作,给予初始值
for (int i = 1 ; i <= n ; i ++) {
if (s[i] == k + 'a') {//分两类情况讨论,当前为s[i]出现次数最多的情况
for (int j = 0 ; j < 26 ; j ++) {//依次更新每个字符的情况
dp[j][0] ++, dp[j][1] ++;//最优操作一定是+1,且1-i中没有j的情况下+1在极小值面前无意义
ans = max(ans , dp[j][0]);//边操作,边更新答案
}
} else {
dp[s[i] - 'a'][0] = dp[s[i] - 'a'][1] - 1;//1-i中一定存在s[i],大胆更新
dp[s[i] - 'a'][1] = max(dp[s[i] - 'a'][1] - 1 , 0);//注意与0比max,存在不去的情况
ans = max(ans , dp[s[i] - 'a'][0]);//同理
}
}
}
printf("%d", ans);//输出
return 0;
}