[Ynoi2016] 镜中的昆虫
[Ynoi2016] 镜中的昆虫
题目描述
维护一个长为 \(n\) 的序列 \(a_i\),有 \(m\) 次操作。
-
将区间 \([l,r]\) 的值修改为 \(x\)。
-
询问区间 \([l,r]\) 出现了多少种不同的数,也就是说同一个数出现多次只算一个。
输入格式
第一行两个整数 \(n,m\)。
第二行 \(n\) 个整数表示 \(a_i\)。
后面 \(m\) 行每行为 \(1\ l\ r\ x\) 或者 \(2\ l\ r\) ,分别表示修改和询问。
输出格式
对于每个询问,输出一个数表示答案。
样例 #1
样例输入 #1
5 5
1 2 3 4 5
2 1 5
1 2 3 4
2 1 5
2 3 3
2 2 4
样例输出 #1
5
3
1
1
提示
\(1\leq n , m \leq 10^5\),\(1\leq a_i\leq 10^9\)。
思路
套路
一
查询一个区间有多少种不同的数,我们可以对每个数求一下他的前驱,就是指在他前面离他最近相同的数的下标(如果没有前驱则为0),记为 \(pre_i\) ,那么一个区间中合法的点 \(i\) 一定是 \(pre_i<L\) ,这样题目就变为求一个区间内有多少个点的 \(pre\) 是小于 \(L\) 的,就变为了一个二维偏序问题,可以 \(O(nlogn)\) 去做这个事。
相同套路的题:[国家集训队] 数颜色 / 维护队列
二
对于只有区间推平操作的题我们可以考虑用珂朵莉树去模拟这个操作,考虑时间复杂度,每次 \(assign\) 操作最多会多生成一个点,其余的点会被区间推平,并且全局下来最多总共会推平 \(n\) 个点,所以推平的时间复杂度为 \(O(n+m)\) ,均摊 \(O(1)\) ,这个时候你如果想对这些被推平的点做一些事也是可做的。
正解
首先对于询问来说我们肯定是上套路,记一个 \(pre\) 数组求前驱,但是有区间修改操作不是那么好做。
但是单点修改我们是会的,就例如上面那道题,我们可以再记一个 \(nxt\) 后继数组,再多一维时间变为三维偏序问题,就可以用 cdq 等做法去解决。
\(1-side\ \ t_i≤t_j\)
$2-side\ \ pre_i<L_j $
\(3-side\ \ L_i≤pos_j≤R_i\)
以 cdq 举例 我们可以一维排序,二维 cdq,三维 BIT。这样就可有解决单点修改的区间查询。
题目只有一个修改操作,那就是区间推平,直接上套路用珂朵莉树去模拟,我们不妨把推平后的区间看做一个点,用上面的结论,发现区间推平全局下来也就最多等于有 \(O(n+m)\) 个单点修改,直接做就行。
本题的时间复杂度瓶颈在求解三维偏序所以总时间复杂度为 \(O(nlog^2n)\)。
\(\huge code\)
const int maxn=1e5+10;
int a[maxn],all[maxn<<1],cnt;
unordered_map<int,int>ump;
struct OPT
{
int op,l,r,v;
}q[maxn];
struct VAL
{
bool op;
int pre,pos,v,t;
// pre=l
// pos=r
// v=id
bool operator < (const VAL &A)const
{
return pre<A.pre;
}
// op-1 change
// op-2 query
}c[maxn*16];
int tot;
int ans[maxn],idx;
set<int>col[maxn<<1];
int pre[maxn],nxt[maxn],n,m;
void add(int pos,int cl,int t)
{
auto it=col[cl].insert(pos).fi,tmp=it;
if(it!=col[cl].begin())
{
--tmp;
nxt[*tmp]=pos;
pre[pos]=*tmp;
}
else pre[pos]=0;
tmp=it;++tmp;
if(tmp!=col[cl].end())
{
// <==
c[++tot]={0,pre[*tmp],*tmp,-1,t};
pre[*tmp]=pos;
nxt[pos]=*tmp;
// <==
c[++tot]={0,pre[*tmp],*tmp,1,t};
}
else nxt[pos]=0;
// <==
c[++tot]={0,pre[pos],pos,1,t};
}
void del(int pos,int cl,int &t)
{
// <==
c[++tot]={0,pre[pos],pos,-1,t};
auto it=col[cl].find(pos),tmp=it;
tmp=it;++tmp;
if(tmp!=col[cl].end())
{
// <==
c[++tot]={0,pre[*tmp],*tmp,-1,t};
pre[*tmp]=pre[*it];
nxt[pre[*it]]=*tmp;
// <==
c[++tot]={0,pre[*tmp],*tmp,1,t};
}
else nxt[pre[*it]]=0;
col[cl].erase(it);
}
struct node
{
int l,r,v;
bool operator < (const node &A)const
{
return l<A.l;
}
};
set<node>odt;
int find(int &x)
{
auto it=--odt.upper_bound({x,0,0});
return it->r;
}
auto split(int x,int &t)
{
if(x>n)return odt.end();
auto it=--odt.upper_bound({x,0,0});
if(it->l==x)return it;
int l=it->l,r=it->r,v=it->v;
add(x-1,v,t);
odt.erase(it);
odt.insert({l,x-1,v});
return odt.insert({x,r,v}).fi;
}
void assign(int &l,int &r,int &v,int &t)
{
auto R=split(r+1,t),L=split(l,t);
for(auto it=L;it!=R;++it)
del(it->r,it->v,t);
add(r,v,t);
odt.erase(L,R);
odt.insert({l,r,v});
}
struct BIT
{
int tr[maxn+110],ed;
void change(int x,int v)
{
for(;x<=ed;x+=x&-x)
tr[x]+=v;
}
int query(int x)
{
int ans=0;
for(;x;x-=x&-x)
ans+=tr[x];
return ans;
}
int query(int &l,int &r)
{
return query(r)-query(l-1);
}
}bit;
void cdq(int l,int &r)
{
if(l==r)return;
int mid=l+r>>1;
cdq(l,mid);
cdq(mid+1,r);
int L=l,R;
for(int i=mid+1;i<=r;++i)
{
while(L<=mid and c[L].pre<c[i].pre)
{
if(c[L].op==0)bit.change(c[L].pos,c[L].v);
++L;
}
if(c[i].op==1)
ans[c[i].v]+=bit.query(c[i].pre,c[i].pos);
}
for(int i=l;i<L;++i)
if(c[i].op==0)
bit.change(c[i].pos,-c[i].v);
stable_sort(c+l,c+r+1);
}
signed main()
{
n=read(),m=read();
bit.ed=n+10;
for(int i=1;i<=n;++i)
{
a[i]=read();
all[++cnt]=a[i];
}
for(int i=1;i<=m;++i)
{
q[i].op=read(),q[i].l=read(),q[i].r=read();
if(q[i].op==1)q[i].v=read(),all[++cnt]=q[i].v;
}
sort(all+1,all+1+cnt);
cnt=unique(all+1,all+1+cnt)-(all+1);
for(int i=1;i<=cnt;++i)ump[all[i]]=i;
for(int i=1;i<=n;++i)a[i]=ump[a[i]];
for(int i=1;i<=m;++i)q[i].v=ump[q[i].v];
for(int i=1;i<=n;++i)
{
odt.insert({i,i,a[i]});
add(i,a[i],0);
}
for(int i=1;i<=m;++i)
if(q[i].op==2)
{
q[i].l=find(q[i].l),q[i].r=find(q[i].r);
c[++tot]={1,q[i].l,q[i].r,++idx,i};
}
else
assign(q[i].l,q[i].r,q[i].v,i);
cdq(1,tot);
for(int i=1;i<=idx;++i)
__print(ans[i]);
return 0;
}
细节
一
因为把一个推平后的区间看做了一个点,我们可以钦定这个区间的贡献的位置,比如钦定这个区间的右端点能产生贡献,这时我们得把询问的 \(l,r\) 也平移到他们各自所在推平区间的右端点,这样才不会算少贡献。
二
注意在 \(split\) 操作中分裂一个区间时,由于贡献在后面,所以得把前面那个点的贡献加上。
三
本题有点卡空间,所以好像只有 cdq 才能过,但是你如果用手写归并排序开一个双倍空间也会 MLE,所以可以直接在 cdq 再排一次序,时间复杂也是对的。
四
建议把前驱后继的修改用一个函数封装一下不然写的头疼,也不易检查。
后记
本人第一道黑,大爱 ds ,感觉也就 ds 的黑我能做了,中间重构了一次,死因没把前驱后继的修改用一个函数套起来,根本检查不出来哪儿锅了。

浙公网安备 33010602011771号