題目鏈接
題目大意
題意:有一個長度爲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;
}