P7230题解
P7230 [COCI 2015/2016 #3] NEKAMELEONI 题解
一、题目简述
维护一个长度为 n 的数组,元素取值 1..k。支持两种操作:
- 单点修改:把位置 p 的值改为 v
- 查询:求最短的连续子数组长度,使其包含 1..k 所有数字。若不存在输出 -1。
数据范围(原题):n, m <= 1e5,k <= 50。
拓展思考:若 k 也达到 1e5,该怎么做?
二、核心转化:f[i] 的定义
设 f[i] 表示:从位置 i 开始,能够覆盖所有 1..k 颜色的最短区间的右端点。
若不存在这样的区间,则 f[i] = +∞。
那么答案就是:
ans = min_{1 <= i <= n} ( f[i] - i + 1 )
如何计算 f[i]?
对固定颜色 c,记 nxt_c(i) 为颜色 c 在位置 i 之后第一次出现的位置(包含 i 本身)。
若没有出现则 nxt_c(i) = +∞。
则:
f[i] = max_{c = 1..k} nxt_c(i)
也就是说,从 i 开始,必须等到最晚出现的那个颜色也出现,区间才能覆盖所有颜色。
三、单点修改的影响
把位置 p 的颜色从 c 改成 c',会影响哪些 f[i]?
对颜色 c(删除 p):
设 p 在颜色 c 的出现集合中的前驱为 x,后继为 y。
那么对于 i ∈ [x+1, p],原来 nxt_c(i) = p,现在变成 y。
所以 f[i] 在区间 [x+1, p] 上需要由 p 变为 y。
对颜色 c'(插入 p):
设 p 在颜色 c' 的出现集合中的前驱为 x',后继为 y'。
那么对于 i ∈ [x'+1, p],原来 nxt_{c'}(i) = y',现在变成 p。
所以 f[i] 在区间 [x'+1, p] 上需要由 y' 变为 p。
注意:每次修改只影响 O(1) 个区间,这是关键。
四、当 k 很大(如 k >= 1e5)时的思路
原题 k <= 50,可以用数组 st[60] 存每个颜色的 multiset。
当 k 达到 1e5 时,只需把按颜色索引的数组改成哈希表即可:
- 用 unordered_map<int, set
> st; 或者 map<int, set > st; - 只对出现过的颜色初始化哨兵,不必遍历 1..k;
- g[i] 仍然记录位置 i 历史上出现过的所有颜色,总大小 O(n + m)。
算法框架完全不变,复杂度与 k 无关。
五、离线 + 时间线段树分治 + 可回滚线段树
这是代码采用的方案,天然支持大 k。
5.1 把“颜色存活时间”变成事件
对每个位置 p,它在时刻 i 被改成新颜色,旧颜色 c 的“存活时间”是 [L, R]。
这个删除事件需要挂到时间线段树上,覆盖 [1, L-1] ∪ [R+1, m](补集)。
5.2 可回滚线段树维护 f
线段树每个节点维护:
- mx:区间内 f[i] 的最大值(用于 findl 定位)
- mn:区间内 f[i] - i + 1 的最小值(即答案候选)
- tag:区间赋值懒标记
支持两个操作:
- updf(l, r, v):把 [l, r] 内的 f[i] 全部赋值为 v
- findl(v):找到最左的 f[i] >= v 的位置
5.3 DFS 时间线段树
进入节点时执行所有删除事件(更新 st 与线段树),
离开时回滚(rb 栈 + 恢复 st)。
六、复杂度分析
- 时间线段树分治:O((n + m) log m) 个事件
- 每个事件:set 操作 O(log n) + 线段树区间赋值 O(log n) 均摊
- 总时间复杂度:O((n + m) log^2 (n + m))
- 空间复杂度:O(n + m)
与 k 无关,所以 k >= 1e5 完全可行。
七、代码实现中的关键细节与调试要点
-
findl 中 tg 的优先级:
祖先 tag 一定覆盖当前节点 tag,否则区间赋值语义被破坏。
正确写法:
int cur_tag = tg ? tg : tr[pos].tag;
if(cur_tag) return cur_tag >= v ? l : n+1; -
lastt[x] 的语义:
应记录“旧颜色最后一次出现的位置”,而不是“修改操作发生的位置”,
否则删除事件的边界会错位。建议在修改时写 lastt[x] = i-1。 -
events 数组大小:
4 * N 对 m <= 1e5 足够,但若 m 更大需要开 4 * m。 -
哨兵值:
用 INT_MAX/2 级别的值更安全,避免 v - r + 1 下溢。 -
重复挂载最终颜色的删除事件:
只保留一个循环即可,否则浪费空间和时间。
八、另一种通用做法(供参考)
若不想写可回滚线段树,也可以用线段树合并:
- 每个节点维护一个 map:颜色 -> 该颜色在区间内第一次/最后一次出现位置
- 合并时只需合并两个 map,并顺便更新答案
- 复杂度 O((n + m) log^2 (n + m)),但常数较大,容易 TLE。
九、一句话总结
本题的“最短包含 1..k 所有颜色的子数组”问题,可以转化为:
对每个起点 i 求 f[i] = max_c nxt_c(i),
答案即 min(f[i] - i + 1)。
当 k 很大时,只需把按颜色索引的数组改成 map,
其余框架(离线时间线段树分治 + 可回滚线段树)完全不变,
复杂度 O((n + m) log^2 (n + m)) 与 k 无关。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,k,m;
bool vis[N];
int a[N],ans[N],f[N];
vector<int>g[N];//位置 i 历史上出现过的所有颜色
multiset<int>st[60];//存储颜色 c 的所有历史出现位置
struct tree{
int mx,mn,tag;//f[i]max,f[i]-i+1min,区间
}tr[1<<22];
struct rollback{
int pos,mxv,mnv,tagv;
};
struct OP{
int yp;
int x,y;
}ops[N];
vector<rollback>rb;
// ---------- 可回滚线段树 ----------
inline void reset(int pos,int l,int r,int v){
rb.push_back({pos,tr[pos].mx,tr[pos].mn,tr[pos].tag});
tr[pos].mx=v;
tr[pos].tag=v;
tr[pos].mn=v-r+1;//i越大,v-i+1越小,所以最小值在r
}
inline void down(int pos,int l,int r){
if(tr[pos].tag){
int mid=(l+r)>>1;
reset(pos<<1,l,mid,tr[pos].tag);
reset(pos<<1|1,mid+1,r,tr[pos].tag);
rb.push_back({pos,tr[pos].mx,tr[pos].mn,tr[pos].tag});
tr[pos].tag=0;
}
}
inline void up(int pos){
rb.push_back({pos,tr[pos].mx,tr[pos].mn,tr[pos].tag});
tr[pos].mx=max(tr[pos<<1].mx,tr[pos<<1|1].mx);
tr[pos].mn=min(tr[pos<<1].mn,tr[pos<<1|1].mn);
}
void upd(int pos,int l,int r,int ql,int qr,int v){
if(ql<=l&&r<=qr){
reset(pos,l,r,v);
return ;
}
down(pos,l,r);
int mid=(l+r)>>1;
if(ql<=mid)upd(pos<<1,l,mid,ql,qr,v);
if(qr>mid)upd(pos<<1|1,mid+1,r,ql,qr,v);
up(pos);
}
// 查询最左的i使得f[i]>=v,不修改线段树
int findl(int pos,int l,int r,int v,int tg){
if(tr[pos].tag){
return tr[pos].tag>=v?l:n+1;
}
if(tg){
return tg>=v?l:n+1;
}
if(l==r){
return tr[pos].mx>=v?l:n+1;
}
int mid=(l+r)>>1;
if(tr[pos<<1].mx>=v){
return findl(pos<<1,l,mid,v,0);
} else {
return findl(pos<<1|1,mid+1,r,v,0);
}
}
void updf(int l,int r,int v){
l=max(l,1);
if(l>r)return ;
int pos=findl(1,1,n,v,0);
r=min(r,pos-1);
if(l>r)return ;
upd(1,1,n,l,r,v);
}
// ---------- 时间线段树分治 ----------
vector<pair<int,int>>events[4*N];//每个时间节点挂 (位置, 颜色) 删除事件
void adde(int pos,int l,int r,int ql,int qr,int p,int c){
if(ql>qr)return ;
if(ql<=l&&r<=qr){
events[pos].push_back({p,c});
return;
}
int mid=(l+r)>>1;
if(ql<=mid)adde(pos<<1,l,mid,ql,qr,p,c);
if(qr>mid)adde(pos<<1|1,mid+1,r,ql,qr,p,c);
}
void solve(int pos,int l,int r){
int rbsz=rb.size();
vector<pair<int,int>>deleted;
for(auto[p,c]:events[pos]){
auto it=st[c].find(p);
if (it==st[c].end())continue;
st[c].erase(it);
deleted.push_back({p,c});
if(st[c].find(p)!=st[c].end()){
continue;
}
auto it2=st[c].lower_bound(p);
int y=*it2;
int x=*prev(it2);
updf(x+1,p,y);
}
if(l==r){
ans[l]=tr[1].mn;
}
else {
int mid=(l+r)>>1;
solve(pos<<1,l,mid);
solve(pos<<1|1,mid+1,r);
}
while(rb.size()>rbsz){
rollback rec=rb.back();
rb.pop_back();
tr[rec.pos].mx=rec.mxv;
tr[rec.pos].mn=rec.mnv;
tr[rec.pos].tag=rec.tagv;
}
for(auto[p,c]:deleted){
st[c].insert(p);
}
}
void build(int pos,int l,int r){
if(l==r){
tr[pos].mx=f[l];
tr[pos].mn=f[l]-l+1;
tr[pos].tag=0;
return;
}
int mid=(l+r)>>1;
build(pos<<1,l,mid);
build(pos<<1|1,mid+1,r);
tr[pos].mx=max(tr[pos<<1].mx,tr[pos<<1|1].mx);
tr[pos].mn=min(tr[pos<<1].mn,tr[pos<<1|1].mn);
tr[pos].tag=0;
}
int lastt[N],lastv[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k>>m;
for(int i=1;i<=k+2;i++){
st[i].insert(-1e9);
st[i].insert(1e9);
}
for(int i=1;i<=n;i++){
cin>>a[i];
st[a[i]].insert(i);
g[i].push_back(a[i]);
}
int op,x,y;
for(int i=1;i<=m;i++){
cin>>op;
if(op==1){
cin>>x>>y;
ops[i]={1,x,y};
}
else{
vis[i]=1;
ops[i]={2,0,0};
}
}
for(int i=1;i<=n;i++) {
lastt[i]=0;
lastv[i]=a[i];
}
for(int i=1;i<=m;i++){
if(ops[i].yp==1){
int x=ops[i].x,y=ops[i].y;
int c=lastv[x];
if(c==y)continue;
int L=lastt[x]+1;
int R=i-1;
if(L<=R){
if(L>1)adde(1,1,m,1,L-1,x,c);
if(R<m)adde(1,1,m,R+1,m,x,c);
}
st[y].insert(x);
g[x].push_back(y);
lastt[x]=i;
lastv[x]=y;
}
}
for(int x=1;x<=n;x++){
int c=lastv[x];
int L=lastt[x]+1;
int R=m;
if(L<=R){
if(L>1) adde(1,1,m,1,L-1,x,c);
}
}
f[1]=0;
for(int c=1;c<=k;c++) {
auto it=st[c].lower_bound(1);
f[1]=max(f[1],*it);
}
for(int i=2;i<=n;i++) {
f[i]=f[i-1];
for(int c:g[i-1]){
auto it=st[c].lower_bound(i);
f[i]=max(f[i],*it);
}
}
build(1,1,n);
solve(1,1,m);
for(int i=1;i<=m;i++){
if(vis[i]){
if(ans[i]>n)cout<<-1<<'\n';
else cout<<ans[i]<<'\n';
}
}
return 0;
}

浙公网安备 33010602011771号