题目大意
对于一个01字符串,如果将这个字符串0和1取反后,再将整个串反过来和原串一样,就称作“反对称”字符串。比如00001111和010101就是反对称的,1001就不是。
现在给出一个长度为N的01字符串,求它有多少个子串是反对称的。 N ≤ 5 × 1 0 5 N \le 5 \times 10^5 N≤5×105
思路
-
考虑如何表示反对称,将一个01串按位取反后,如果和原串回文,则该串是反对称的。
例如 01 01 01取反后是 10 10 10, 0110 0110 0110是回文的,所以 10 10 10是反对称的。 -
接着考虑如何找回文,可以用哈希, O ( n ) O(n) O(n)处理, O ( 1 ) O(1) O(1)查询。
但是题目问的是子串数量,如果枚举长度和起点,那么时间复杂度是 O ( n 2 ) O(n^2) O(n2)级别的,会超时。 -
可以用中心扩展法,以每个字符为中心,左右扩展,而且扩展中心字符的左右求回文串的长度具有单调性(因为回文串的子串一定也是回文的),所以可以用二分,优化到 O ( n l o g 2 n ) O(n\ log_2n) O(n log2n)。
代码
注意一些细节:
- 本题的回文串一定是偶数,如果是奇数,那么取反后一定不符合要求。
- 二分判断回文,以 s [ x ] s[x] s[x]为中心, m i d mid mid为回文半径,判断 s s s的 [ x − m i d + 1 , x ] [x - mid + 1, x] [x−mid+1,x]和 t t t的 [ x + 1 , x + m i d ] [x + 1, x +mid] [x+1,x+mid]是否回文( t t t 为 s s s取反后的字符串)。
- 二分找到最大回文半径 l l l后,则以 s [ x ] s[x] s[x]为中心的回文串的子串数量为 l l l,累加求和即可。
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
const int Maxn = 5e5;
int n, PP = 131, ans;
string s, t;
ull P[Maxn + 3], f[Maxn + 3], g[Maxn + 3];
ull get_hash(int l, int r) {
return f[r] - f[l - 1] * P[r - l + 1];
}
ull get_hash1(int l, int r) {
return g[l] - g[r + 1] * P[r - l + 1];
}
void calc(int x) {
int l = 0, r = min(x, n - x);
while (l < r) {
int mid = (l + r + 1) >> 1;
if (get_hash(x - mid + 1, x) == get_hash1(x + 1, x + mid)) l = mid;
else r = mid - 1;
}
ans += l;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> s;
s.insert(0, " ");
t = s;
for (int i = 1; i <= n; ++i)
if (s[i] == '1') t[i] = '0';
else t[i] = '1';
P[0] = 1;
for (int i = 1; i <= n; ++i) P[i] = P[i - 1] * PP;
for (int i = 1; i <= n; ++i) f[i] = f[i - 1] * PP + s[i];
for (int i = n; i >= 1; --i) g[i] = g[i + 1] * PP + t[i];
for (int i = 1; i <= n; ++i) calc(i);
cout << ans << '\n';
return 0;
}