题目描述
给定一个字符串,提供m个访问区间,求每次访问中,访问区间内满足区间内字符重排可形成回文序列的子区间个数。
输入格式
第一行输入两个数据,分别为n、m;第二行输入长为n的仅由小写字母构成的字符串;接下来输入m组查询数据,每组数据包含两个数据,分别是访问区间的左端点和右端点。
输出格式
输出m行数据,每一行为对应查询区间满足条件的子区间个数。
输入样例#1
6 6
zzqzzq
1 6
2 4
3 4
2 3
4 5
1 1
输出样例#1
16
4
2
2
3
1
解题思路
一个区间内的字符串经过重排可以构成回文序列的充要条件是:所有字母均有偶数个
或者仅有一个字母为奇数个其余均有偶数个
。因此,可以使用一个26位的二进制数来表示每个字母的奇偶状态,该二进制数从低位到高位分别表示a-z的奇偶性,0表示有偶数个,1表示有奇数个。当为回文序列时,该二进制数必满足为0
或为2^k
。
当给定一个访问区间时,不但需要判断这个区间是否满足重排形成回文,还需要判断子区间是否满足重排形成回文。因此如果直接遍历给定区间的所有子区间并计算子区间各字母奇偶状态,必定导致复杂度上升。这时我们考虑到莫队的left和right区间指针移动的特性,由于每次只能移动一个单位,因此指针移动的过程中就完成了对所有子区间的遍历,这样便将复杂度降低到了 O ( n n ) O(n\sqrt n) O(nn)。
莫队的指针移动是如何完成对子区间的遍历的呢?当指定访问区间由 [ l e f t , t i g h t ] [left,tight] [left,tight]变为 [ l e f t , r i g h t + 1 ] [left,right+1] [left,right+1]时,由于 [ l e f t , t i g h t ] [left,tight] [left,tight]区间内的子区间情况已经遍历过,实际上只需判断新添加的 r i g h t + 1 right+1 right+1位置的数据与 [ l e f t , t i g h t ] [left,tight] [left,tight]中各个点组合成的子区间是否满足条件。
下考虑如何快速得到各区间的字母奇偶状态。使用 s u m [ i ] sum[i] sum[i]来存储区间 [ 1 , i ] [1,i] [1,i]之间所有字母的奇偶状态;因此想要获得区间 [ x , y ] [x,y] [x,y]的字母奇偶状态,只需要计算 s u m [ y ] x o r s u m [ x − 1 ] sum[y]\ xor\ sum[x-1] sum[y] xor sum[x−1]即可。因此在输入字符串的过程中,即可直接计算出 s u m [ ] sum[] sum[]数组的值,具体如下:
for(register int i = 1; i <= n; i++){
sum[i] = sum[i - 1] ^ (1 << (getchar() - 'a'));
}
如何记录操作区间内各状态的个数?我们使用 c n t [ i ] cnt[i] cnt[i]数组来存储莫队left指针和right指针指向区间内i状态下的区间个数。举例:对于字符串zppp
来说,区间 [ 1 , 2 ] [1,2] [1,2]的状态和区间 [ 1 , 4 ] [1,4] [1,4]的状态相同,因此满足该状态的个数 c n t [ ( 1 < < 25 ) + ( 1 < < 15 ) ] = 2 cnt[(1<<25)+(1<<15)]=2 cnt[(1<<25)+(1<<15)]=2。而由于总共有26个字母,每种字母对应2种状态——奇数个或偶数个,因此总共可能的状态有 2 26 2^{26} 226个,即 c n t [ ] cnt[] cnt[]数组的大小应大于等于 2 26 2^{26} 226。
如何在区间扩充或缩减时更新子区间个数?设 x x x为区间 [ l e f t , r i g h t ] [left,right] [left,right]内某一点,当区间由 [ l e f t , r i g h t ] [left,right] [left,right]扩充为 [ l e f t , r i g h t + 1 ] [left,right+1] [left,right+1]时,如果区间 [ x , r i g h t + 1 ] [x,right+1] [x,right+1]内各字母状态对应的二进制数a必满足为0或为 2 k 2^k 2k。由于 c n t [ a ] cnt[a] cnt[a]记录的是区间 [ l e f t , r i g h t ] [left,right] [left,right]内满足状态a的子区间个数,因此若 s u m [ r i g h t + 1 ] sum[right+1] sum[right+1]的状态也为a,则意味着区间 [ 1 , l e f t − 1 ] [1,left-1] [1,left−1]之间的字母状态没有影响,且 [ x + 1 , r i g h t ] [x+1,right] [x+1,right]区间内的字母状态也没有影响,即 [ x , r i g h t + 1 ] [x,right+1] [x,right+1]区间字母状态满足异或和为0,为回文序列。而区间 [ l e f t , r i g h t ] [left,right] [left,right]内所有满足与right+1异或为0的点的个数为 c n t [ s u m [ r i g h t + 1 ] ] cnt[sum[right+1]] cnt[sum[right+1]]。换句话说 c n t [ s u m [ r i g h t + 1 ] ] cnt[sum[right+1]] cnt[sum[right+1]]记录了区间内所有满足 s u m [ x − 1 ] x o r s u m [ r i g h t + 1 ] = = 0 sum[x-1]\ xor\ sum[right+1]==0 sum[x−1] xor sum[right+1]==0的子区间的个数。同理我们可以得出满足为 2 k 2^{k} 2k形式的区间必满足 s u m [ x − 1 ] x o r s u m [ r i g h t + 1 ] = = 2 k sum[x-1]\ xor\ sum[right+1]==2^k sum[x−1] xor sum[right+1]==2k,其中 0 ≤ k ≤ 25 0\leq k\leq25 0≤k≤25,即满足 s u m [ x − 1 ] x o r s u m [ r i g h t + 1 ] = = 1 < < k sum[x-1]\ xor\ sum[right+1]==1<<k sum[x−1] xor sum[right+1]==1<<k,即满足 s u m [ x − 1 ] x o r s u m [ r i g h t + 1 ] x o r ( 1 < < k ) = = 0 sum[x-1]\ xor\ sum[right+1]\ xor\ (1<<k)==0 sum[x−1] xor sum[right+1] xor (1<<k)==0,即区间个数由 c n t [ s u m [ r i g h t + 1 ] x o r ( 1 < < k ) ] cnt[sum[right+1]\ xor\ (1<<k)] cnt[sum[right+1] xor (1<<k)]记录。据此构建莫队关键的更新函数如下:
inline void add(register int pos){
res += cnt[sum[pos]]; //res加上区间内满足sum[j]^sum[pos]==0的个数
cnt[sum[pos]]++;
for(register int i = 0; i < 26; i++) res += cnt[sum[pos] ^ (1 << i)]; //加上区间内满足sum[j]^sum[pos]==2^k的个数
}
inline void del(register int pos){
cnt[sum[pos]]--;
res -= cnt[sum[pos]];
for(register int i = 0; i < 26; i++) res -= cnt[sum[pos] ^ (1 << i)];
}
随后直接利用莫队对查询区间进行排序后遍历即可,如下:
int left = 1, right = 0; //左右端点指针
for(register int i = 0; i < m; i++){
register int l = visits[i].left - 1, r = visits[i].right; //求区间[left,right]需要计算的是sum[left-1]^sum[right],因此l赋值时需减1
while(left < l) del(left++);
while(left > l) add(--left);
while(right < r) add(++right);
while(right > r) del(right--);
out[visits[i].id] = res;
}
由于该题数据量较大,因此使用快读。快读在计算数据值时也存在技巧。当为字符'0'-'9'
之间时,需要进行ans = ans * 10 + ch - '0'
的计算,但考虑到乘法运算较慢,又因为 10 = 8 + 2 = 2 3 + 2 10=8+2=2^3+2 10=8+2=23+2,因此将ans * 10
改写为位运算(ans << 3) + (ans << 1)
;ch - '0'
由于字符'0'
ASCII码的特殊性,因此可改写为ch ^ 48
;综上可改为ans = (ans << 3) + (ans << 1) + (ch ^ 48)
。快读函数如下:
inline int read(){
//快读
register char ch = getchar();
while(ch < '0' || ch > '9') ch = getchar();
register int ans = 0;
while(ch >= '0' && ch <= '9'){
ans = (ans << 3) + (ans << 1) + (ch ^ 48);
ch = getchar();
}
return ans;
}
由于该题卡常苛刻,因此使用 s o r t ( ) sort() sort()函数时采用一般的莫队排序方法会遭遇卡常,此时采用分块排序的方法,将块按照 b a s e base base大小分,在 b a s e base base区间内的直接按 r i g h t right right从小到大排序;在 b a s e base base区间外的按 l e f t left left从小到大排序。同时由于构造 s o r t ( ) sort() sort()函数所需的 c m p ( ) cmp() cmp()函数,会导致访问时间的增加,因此直接在结构体声明中重定义<
即可。如下:
struct visit{
int id, left, right;
bool operator < (visit vst)const{
return left / base == vst.left / base ? right < vst.right : left < vst.left;
}
}visits[MAX];
base = 3 * sqrt(n);
sort(visits, visits + m);
代码
由于该题卡常严重,使用#pragma GCC optimize(2)
仍会卡两个测试点,因此不需要手动代码添加编译优化,而直接点击O2优化即可AC。
#include <stdio.h>
#include <math.h>
#include <algorithm>
using namespace std;
#define MAX 60005
int n, m, base;
int sum[MAX], cnt[(1 << 26) + 5], out[MAX];
//sum是前缀异或和;由于有26个字母,每个字母有奇偶两种状态,因此cnt用来存储当前区间内某种状态下对应的区间个数
struct visit{
int id, left, right;
bool operator < (visit vst)const{
return left / base == vst.left / base ? right < vst.right : left < vst.left;
}
}visits[MAX];
int res = 0;
inline int read(){
//快读
register char ch = getchar();
while(ch < '0' || ch > '9') ch = getchar();
register int ans = 0;
while(ch >= '0' && ch <= '9'){
ans = (ans << 3) + (ans << 1) + (ch ^ 48);
ch = getchar();
}
return ans;
}
inline void add(register int pos){
res += cnt[sum[pos]]; //res加上区间内与当前状态一致的区间个数,即区间内满足sum[j]^sum[pos]==0的个数
cnt[sum[pos]]++;
for(register int i = 0; i < 26; i++) res += cnt[sum[pos] ^ (1 << i)]; //加上区间内满足sum[j]^sum[pos]==2^k的个数
}
inline void del(register int pos){
cnt[sum[pos]]--;
res -= cnt[sum[pos]];
for(register int i = 0; i < 26; i++) res -= cnt[sum[pos] ^ (1 << i)];
}
int main(){
//输入
n = read(), m = read();
base = 3 * sqrt(n);
register char ch = getchar();
while(ch < 'a' || ch > 'z') ch = getchar();
sum[1] = 1 << (ch - 'a');
for(register int i = 2; i <= n; i++){
sum[i] = sum[i - 1] ^ (1 << (getchar() - 'a'));
}
for(register int i = 0; i < m; i++){
visits[i].id = i;
visits[i].left = read();
visits[i].right = read();
}
sort(visits, visits + m); //莫队排序
int left = 1, right = 0;
for(register int i = 0; i < m; i++){
register int l = visits[i].left - 1, r = visits[i].right;
while(left < l) del(left++);
while(left > l) add(--left);
while(right < r) add(++right);
while(right > r) del(right--);
out[visits[i].id] = res;
}
for(register int i = 0; i < m; i++) printf("%d\n", out[i]);
return 0;
}