@bzoj - 4384@ [POI2015] Trzy wieże
@description@
给定一个长度为 n 的仅包含'B'、'C'、'S'三种字符的字符串,请找到最长的一段连续子串,使得这一段要么只有一种字符,要么有多种字符,但是没有任意两种字符出现次数相同。
input
第一行包含一个正整数 n(1<=n<=1000000),表示字符串的长度。
第二行一个长度为 n 的字符串。
output
包含一行一个正整数,即最长的满足条件的子串的长度。
sample input
9
CBBSSBCSC
sample output
6
@solution@
在这篇博客内有一个很神仙的结论:
最优解 [l, r] 一定满足 \(l \in [1, 3]\) 或 \(r \in [n-2, n]\)。
至于证明,那位博主略去了。。。
我来尝试证明一下吧:
性质 <1>: 如果 [l, r] 满足没有任意两种字符出现次数相同,则一定能找到合法区间 [l', r'] 满足 \(l' \in [l-3,l], r' \in [r,r+3]\),且 r - l + 1< r' - l' + 1。
使用反证法。不失一般性,假设 [l, r] 内三种字符的出现次数分别为 (a, b, c) 且满足 a < b < c。
分类讨论 l-1 是什么字符:
(1) [l-1, r] 对应 (a, b, c+1),始终满足 a < b < c + 1。
(2) [l-1, r] 对应 (a, b+1, c),若不满足 a < b + 1 < c,则有 b + 1 = c 成立。
(2) [l-1, r] 对应 (a+1, b, c),若不满足 a + 1 < b < c,则有 a + 1 = b 成立。
类似地对 r+1 进行分类,可以得到一个初步的结论:如果性质不成立,那么 (a, b, c) 至少要满足 a + 2 = b + 1 = c。
剩下的证明因为我很懒再加上我也不是专业的采用 dfs 枚举所有可能。用于验证的程序如下:
#include<cstdio>
int a[6], s[3];
void dfs(int x) {
if( x == 6 ) {
for(int i=0;i<=3;i++)
for(int j=2;j<=5;j++) {
if( i <= j ) {
s[0] = -1, s[1] = 0, s[2] = 1;
for(int k=i;k<=j;k++)
s[a[k]]++;
if( s[0] != s[1] && s[0] != s[2] && s[1] != s[2] )
return ;
}
}
puts("error");
return ;
}
a[x] = 0; dfs(x + 1);
a[x] = 1; dfs(x + 1);
a[x] = 2; dfs(x + 1);
}
int main() {
dfs(0);
}
性质 <2>:如果 [l, r] 满足只有一种字符且 r - l + 1 > 1,则一定能找到合法区间 [l', r'] 满足 l - 1 = l' 或 r + 1 = r'。
对于区间 [l-1, r],它要么是形如 (0, 0, r - l + 2),要么是形如 (0, 1, r - l + 1)。
对于区间 [l, r+1] 同理。
有了这两个性质就可以证明我们的结论了。
假如最优解 [l, r] 不满足结论,则有 l > 3, r < n - 2。由性质 <1> 或性质 <2> 可知可以通过移动它的左右端点使得区间长度变大。故它一定不是最优解。
有一些不严谨的地方,就是如果 r - l + 1 = 1 的时候。这种情况特殊讨论一下也成立。
时间复杂度 O(n)。
@accepted code@
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 1000000 + 5;
inline int fun(char ch) {
if( ch == 'B' ) return 0;
if( ch == 'C' ) return 1;
if( ch == 'S' ) return 2;
}
int n, sum[3][MAXN];
int f(int x, int l, int r) {
return sum[x][r] - sum[x][l - 1];
}
bool check(int l, int r) {
if( f(0, l, r) != f(1, l, r) && f(0, l, r) != f(2, l, r) && f(1, l, r) != f(2, l, r) ) return true;
if( (!f(0, l, r) && !f(1, l, r)) || (!f(0, l, r) && !f(2, l, r)) || (!f(1, l, r) && !f(2, l, r)) ) return true;
return false;
}
char s[MAXN];
int main() {
int n, ans = 1;
scanf("%d%s", &n, s + 1);
for(int i=1;i<=n;i++) {
sum[0][i] = sum[0][i-1];
sum[1][i] = sum[1][i-1];
sum[2][i] = sum[2][i-1];
sum[fun(s[i])][i]++;
}
for(int i=1;i<=n;i++) {
if( 1 <= i && check(1, i) ) ans = max(ans, i - 0);
if( 2 <= i && check(2, i) ) ans = max(ans, i - 1);
if( 3 <= i && check(3, i) ) ans = max(ans, i - 2);
if( i <= n - 0 && check(i, n - 0) ) ans = max(ans, n - i + 1);
if( i <= n - 1 && check(i, n - 1) ) ans = max(ans, n - i);
if( i <= n - 2 && check(i, n - 2) ) ans = max(ans, n - i - 1);
}
printf("%d\n", ans);
}
@details@
当然这种神仙结论我肯定是想不到的。
所以,我选用的是另外一种方法:
只含一种字符的区间显然可以随便怎么搞。
定义 sum[i] 表示字符 i 的前缀和。如果区间 [l + 1, r] 合法,则一定有:
sum[0][r] - sum[0][l] ≠ sum[1][r] - sum[1][l]
sum[0][r] - sum[0][l] ≠ sum[2][r] - sum[2][l]
sum[1][r] - sum[1][l] ≠ sum[2][r] - sum[2][l]
变一下形:
sum[0][r] - sum[1][r] ≠ sum[0][l] - sum[1][l]
sum[0][r] - sum[2][r] ≠ sum[0][l] - sum[2][l]
sum[1][r] - sum[2][r] ≠ sum[1][l] - sum[2][l]
枚举 r,相当于找到一个最小的 l 满足上述条件。
先判断它是不是三个前缀和都不一样,如果是就有 l = 0。
接下来,对于某一个 l,不满足上述条件的 r 其实很少。我们大力分类讨论即可。
时间复杂度还是 O(n) 的,但是代码复杂度……