又是一道比较新的模板题吧,即使是在Codeforces上小C还是贴了出来。
Description
给定一个长度为n的序列a1~an,每个元素代表一种颜色。m次操作,每次操作为两种中的一种:
1 p x:将第p个位置上的颜色修改为x;
2 l r:询问[l,r]区间,求该区间内的每种颜色的“最大出现位置-最小出现位置”之和。
Input
第一行两个正整数n、m;
第二行n个整数,表示a1~an;
接下来m行,每行表示一个如题所示的操作。
Output
对于每个操作2,输出题目所求的答案。
Sample Input
7 6
1 2 3 1 3 2 1
2 3 7
2 1 3
1 7 2
1 3 2
2 1 6
2 5 7
Sample Output
5
0
7
1
HINT
1 ≤ n,m ≤ 100 000,1≤ ai ≤ n;
1 ≤ p,x ≤ n,1 ≤ l ≤ r ≤ n。
Solution
应该说入手这道题还是很容易的,不管后面是怎么做,我们首先可以判定它是一道数据结构题。
我们考虑对于每个元素,我们维护上一个出现它的颜色的位置。
这样似乎就成为了我们很熟悉的矩形询问一类的问题。我们类比一下询问区间的颜色种数怎么做:
第一维代表区间下标,第二维代表上一次出现该颜色的位置,要维护的信息是该位置出现的次数(其实只有0和1),目的是求和。
同理这一题似乎同样可以这么做:
第一维代表区间下标,第二维代表上一次出现该颜色的位置,要维护的信息是 区间下标与上一次出现的位置的差 ,目的是求和。
这样似乎就很完美,我们可以直接树套树……然后并不能很爽地通过该题,因为炸空间了。
那这可咋办呀,我们就可以用到我们神奇的分治算法——cdq分治!
cdq算法的主要思想就是将操作区间分成两半,计算前一半操作对后一半询问的影响。
这样就相当于将在线的修改去掉,将询问改为离线。
这也就要求询问具有可合并性,如果操作之间会互相影响,cdq就不管用了。
例如操作是加法而询问是取max,这样的询问是不满足可合并性的。
对于这道题,每个操作对于答案的影响是独立的,且每次修改颜色都会改变至多6次我们所维护的信息:
设pre[x]为上一次出现该color[x]的位置,suc[x]为下一次出现color[x]的位置。而我们只要维护pre[x]。
假设修改pre[x],改之后的pre[x]为npre[x],suc[x]同理。
要修改的所有信息为:pre[x],pre[suc[x]],pre[nsuc[x]]。
对于每个信息在二维平面上的操作是一次单点减和一次单点加,所以总共是3*2=6次。
完全转化成离线操作后,就只有询问矩形和了,把询问排序用一个普通线段树都是可以做的。
每次操作出现在logm个分治区间里,单个操作的复杂度是logn,所以总时间复杂度O(mlogmlogn)。
#include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <set> #define ll long long #define MM 264005 #define MN 500005 using namespace std; struct meg{int ki,pos,lf,rf,val,aps;}px[MN]; struct node{int g,x,y;}b[MM]; set <int> se[MM]; ll t[MM],ans[MM]; int c[MM][4],las[MM],pre[MM],col[MM]; int MQ,n,m,pxin; inline int read() { int n=0,f=1; char c=getchar(); while (c<'0' || c>'9') {if(c=='-')f=-1; c=getchar();} while (c>='0' && c<='9') {n=n*10+c-'0'; c=getchar();} return n*f; } inline void getadd(int x,int z) {for (x+=MQ;x;x>>=1) t[x]+=z;} inline ll getsum(int x,int y) { register ll lt=0; for (x+=MQ,y+=MQ;x<=y;x>>=1,y>>=1) { if ( x&1) lt+=t[x++]; if (~y&1) lt+=t[y--]; } return lt; } bool cmp(const meg& a,const meg& b) {return a.pos<b.pos || a.pos==b.pos && a.ki<b.ki;} void solve() { sort(px+1,px+pxin+1,cmp); register int i; for (i=1;i<=pxin;++i) if (px[i].ki==0) {if (px[i].lf) getadd(px[i].lf,px[i].val);} else ans[px[i].aps]+=getsum(px[i].lf,px[i].rf)*px[i].val; for (i=1;i<=pxin;++i) if (px[i].ki==0&&px[i].lf) getadd(px[i].lf,-px[i].val); } void work(int L,int R,int gs) { if (!gs||L==R) return; int i,qet=0,mid=L+R>>1; pxin=0; for (i=L;i<=mid;++i) if (b[i].g==1) { px[++pxin]=(meg){0,b[i].x,c[i][0],0,c[i][0]-b[i].x,0}; px[++pxin]=(meg){0,b[i].x,c[i][1],0,b[i].x-c[i][1],0}; px[++pxin]=(meg){0,c[i][2],b[i].x,0,b[i].x-c[i][2],0}; px[++pxin]=(meg){0,c[i][2],c[i][0],0,c[i][2]-c[i][0],0}; px[++pxin]=(meg){0,c[i][3],c[i][1],0,c[i][1]-c[i][3],0}; px[++pxin]=(meg){0,c[i][3],b[i].x,0,c[i][3]-b[i].x,0}; } for (i=mid+1;i<=R;++i) if (b[i].g==2) { px[++pxin]=(meg){1,b[i].x-1,b[i].x,b[i].y,-1,i}; px[++pxin]=(meg){1,b[i].y ,b[i].x,b[i].y, 1,i}; ++qet; } solve(); work(L,mid,gs-qet); work(mid+1,R,qet); } int main() { register int i,x,qet=0; n=read(); m=read(); pxin=0; for (MQ=1;MQ<n;MQ<<=1); --MQ; for (i=1;i<=n;++i) { col[i]=x=read(); las[i]=pre[x]; pre[x]=i; se[x].insert(i); px[++pxin]=(meg){0,i,las[i],0,i-las[i],0}; } for (i=1;i<=m;++i) { b[i].g=read(); b[i].x=read(); b[i].y=read(); if (b[i].g==1) { set<int> ::iterator k; k=se[col[b[i].x]].lower_bound(b[i].x); if (k!=se[col[b[i].x]].begin()) --k,c[i][0]=*k,++k; else c[i][0]=0; if ((++k)!=se[col[b[i].x]].end()) c[i][2]=*k; else c[i][2]=0; se[col[b[i].x]].erase(--k); col[b[i].x]=b[i].y; k=se[col[b[i].x]].lower_bound(b[i].x); if (k!=se[col[b[i].x]].end()) c[i][3]=*k; else c[i][3]=0; if (k!=se[col[b[i].x]].begin()) c[i][1]=*(--k); else c[i][1]=0; se[col[b[i].x]].insert(b[i].x); } else { px[++pxin]=(meg){1,b[i].x-1,b[i].x,b[i].y,-1,i}; px[++pxin]=(meg){1,b[i].y ,b[i].x,b[i].y, 1,i}; ++qet; } } solve(); work(1,m,qet); for (i=1;i<=m;++i) if (b[i].g==2) printf("%I64d ",ans[i]); }
Last Word
感觉这题会让人觉得恶心的只有set的插入删除操作了。
相比树套树,只需要用到普通线段树还是比较赏心悦目的。