AIGC标识 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 完全可行。


七、代码实现中的关键细节与调试要点

  1. findl 中 tg 的优先级:
    祖先 tag 一定覆盖当前节点 tag,否则区间赋值语义被破坏。
    正确写法:
    int cur_tag = tg ? tg : tr[pos].tag;
    if(cur_tag) return cur_tag >= v ? l : n+1;

  2. lastt[x] 的语义:
    应记录“旧颜色最后一次出现的位置”,而不是“修改操作发生的位置”,
    否则删除事件的边界会错位。建议在修改时写 lastt[x] = i-1。

  3. events 数组大小:
    4 * N 对 m <= 1e5 足够,但若 m 更大需要开 4 * m。

  4. 哨兵值:
    用 INT_MAX/2 级别的值更安全,避免 v - r + 1 下溢。

  5. 重复挂载最终颜色的删除事件:
    只保留一个循环即可,否则浪费空间和时间。


八、另一种通用做法(供参考)

若不想写可回滚线段树,也可以用线段树合并:

  • 每个节点维护一个 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;
}
posted @ 2026-10-07 16:09  xmd11  阅读(4)  评论(0)    收藏  举报