题目链接
题目大意
题意:有一个长度为N的字符串(只包含小写字母)。
现有Q个操作,操作1是把第x位的字符改成y,操作2是查询[l,r]内去重后有多少个字符。
前记
这个题目个人认为很有研究的必要。我将用三个方法来写这题
线段树+二进制
首先看到单点操作和区间查询,很容易想到线段树操作。但是一直没想出,后面看别人代码明白可以用二进制操作。
把a−z用0−25表示,再把他们用二进制位表示,就可以用或运算和线段树单点修改区间查询解决这道问题,时间复杂度O(nlogn)
知识
__builtin_popcount()可以计算int类型的数据二进制有多少个1
代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=5e5+5;
int n,t,opt,tree[maxn<<2],pos,x,y;
char s[maxn],ch;
void build(int node,int l,int r){
if(l==r){
tree[node]=1<<(s[l]-'a');
return ;
}
int mid=(l+r)>>1;
build(node<<1,l,mid);
build(node<<1|1,mid+1,r);
tree[node]=tree[node<<1]|tree[node<<1|1];
}
void change(int node,int l,int r,int pos){
if(l==r&&l==pos){
tree[node]=1<<(ch-'a');
return ;
}
int mid=(l+r)>>1;
if(mid>=pos) change(node<<1,l,mid,pos);
else change(node<<1|1,mid+1,r,pos);
tree[node]=tree[node<<1]|tree[node<<1|1];
}
int query(int node,int L,int R,int l,int r){
if(L<=l&&R>=r){
return tree[node];
}
int mid=(l+r)/2,ans=0;
if(mid>=L) ans=ans|query(node<<1,L,R,l,mid);
if(mid<R) ans=ans|query(node<<1|1,L,R,mid+1,r);
return ans;
}
int main(){
scanf("%d %s %d",&n,s+1,&t);
build(1,1,n);
while(t--){
scanf("%d",&opt);
if(opt==1){
scanf("%d %c",&pos,&ch);
if(s[pos]!=ch){
change(1,1,n,pos);
s[pos]=ch;//改变
}
}else{
scanf("%d %d",&x,&y);
printf("%d\n",__builtin_popcount(query(1,x,y,1,n)));
}
}
return 0;
}
set+二分
修改s[i]显然比较简单,问题是解决l到r之间有多少种字母,这里显然要用二分查找降低复杂度,二分查找又需要有序的数据,所以需要用到
set。以前觉得set没什么用,现在感觉set比优先队列好很多。可以插入删除随便哪一点的元素。
知识
lower_bound(key_value) ,返回第一个大于等于key_value的定位器
upper_bound(key_value),返回最后一个大于key_value的定位器(也就是指针
二分使用方法和普通的数组不同
set.lower_bound(x) 返回大于等于x的第一个迭代器(指针)
还有这种迭代器不能加减,没有定义运算符
也就是如果在查找[x,y]有多少个元素不能set.upper_bound(y)-set.lower_bound(x)
因为没有定义这个运算符
只能找大于等于他的最小元素
新学的tip:
set中先加入较大的数防止越界(重要
代码
#include<cstdio>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=5e5+5;
char s[maxn],ch;
set<int> pos[26];
int n,t,opt,x,y;
int main(){
scanf("%d %s %d",&n,s+1,&t);
for(int i=0;i<=25;i++){
pos[i].insert(n+1);//防止二分为空
}
for(int i=1;i<=n;i++){//放入相应的set
pos[s[i]-'a'].insert(i);
}
while(t--){
scanf("%d",&opt);
if(opt==1){
scanf("%d %c",&x,&ch);
if(s[x]!=ch){
pos[s[x]-'a'].erase(x);//删除
pos[ch-'a'].insert(x);//加入
s[x]=ch;
}
}else{
scanf("%d %d",&x,&y);
int ans=0;
for(int i=0;i<=25;i++){
if(*pos[i].lower_bound(x)<=y){//如果大于等于x的最小值小于等于y则有这个字符
ans++;
}
}
printf("%d\n",ans);
}
}
return 0;
}
二维树状数组
其实思考思考会发现造26个树状数组就直接ok了。显然是最容易且最好理解的
代码
#include<cstdio>
using namespace std;
const int maxn=5e5+5;
char ch,s[maxn];
int n,bit[maxn][30],opt,l,r,t;
int lowbit(int x){
return x&(-x);
}
void update(int pos,int kind,int cnt){//位置,种类,增减
while(pos<=n){
bit[pos][kind]+=cnt;
pos=pos+lowbit(pos);
}
}
int query(int x,int i){
int ans=0;
while(x>0){
ans+=bit[x][i];
x=x-lowbit(x);
}
return ans;
}
int main(){
scanf("%d %s %d",&n,s+1,&t);
for(int i=1;i<=n;i++){
update(i,s[i]-'a',1);//加1
}
while(t--){
scanf("%d",&opt);
if(opt==1){
scanf("%d %c",&l,&ch);
if(s[l]!=ch){
update(l,s[l]-'a',-1);//减1
update(l,ch-'a',1);//加1
s[l]=ch;//修改不要忘了
}
}else{
scanf("%d %d",&l,&r);
int ans=0;
for(int i=0;i<=25;i++){//查询26个树状数组
if(query(r,i)-query(l-1,i)){
ans++;
}
}
printf("%d\n",ans);
}
}
return 0;
}