洛谷 P7971 [KSN2021] Colouring Balls 题解

人生第一道蓝交互。

观察数据范围,可以考虑数据点分治,分别解决各个 Subtask。

Subtask 1 & 2

由于颜色块连续,当 query(l,r)==1 时必有 \([l,r]\) 的小球颜色全部相同。

于是双指针扫一遍即可,\(l\) 为当前颜色段起点,\(r\) 一直向右扫到 query(l,r)>1

cpp 复制代码
namespace Subtask12{
    void solve(){
        int l=1,r=2,cnt=1;
        while(l<=n){
            while(r<=n&&query(l,r)==1) ++r;
            rep(i,l,r) ans[i]=cnt;
            ++cnt,l=r;
        }
        cout<<"! ";
        rept(i,1,n) cout<<ans[i]<<' ';
        cout<<endl;
    }
}

Subtask 3 & 4

发现颜色数量不会超过 \(4\) 种。这里只需要考虑 \(4\) 种颜色的情况。

可以维护每种颜色最后出现的位置,按照最后出现的顺序排序,然后分讨即可,细节比较多。

cpp 复制代码
namespace Subtask34{
    void solve(){
        int lst[5]={},ord[5];
        // lst[i]:颜色i上一次出现的位置
        rept(i,1,4) ord[i]=i;
        rept(i,1,n){
            sort(ord+1,ord+5,[lst](int x,int y)->bool{return lst[x]>lst[y];});
            int a=ord[1],b=ord[2],c=ord[3],d=ord[4];
            if(!lst[a]) ans[i]=1;
            else if(!lst[b]){
                if(query(lst[a],i)==1) ans[i]=a;
                else ans[i]=b;
            }else if(!lst[c]){
                if(query(lst[b],i)==3) ans[i]=c;
                else if(query(lst[a],i)==1) ans[i]=a;
                else ans[i]=b;
            }else{
                // 可以通过二分减少询问次数
                if(query(lst[b],i)==2){
                    if(query(lst[a],i)==1) ans[i]=a;
                    else ans[i]=b;
                }else{
                    if(query(lst[c],i)==3) ans[i]=c;
                    else ans[i]=d;
                }
            }
            lst[ans[i]]=i;
        }
        cout<<"! ";
        rept(i,1,n) cout<<ans[i]<<' ';
        cout<<endl;
    }
}

Subtask 5

先判断总颜色数量。

若为 \(N\),做法显然。

若为 \(N-1\),则有且仅有一对同色的球。

我们注意到如果 query(l,r)==r-l,则这对球一定在 \([l,r]\) 中。

因此从 \([1,N]\) 开始逐渐收紧区间就能找出同色球的位置。

cpp 复制代码
namespace Subtask5{
    void solve(){
        int k=query(1,n);
        if(k==n){
            cout<<'!';
            rept(i,1,n) cout<<' '<<i;
            cout<<endl;
            return;
        }
        int l=1,r=n,cnt=1;
        while(l<=r&&query(l,r)==r-l) ++l;
        --l;
        while(l<=r&&query(l,r)==r-l) --r;
        ++r;
        cout<<"! ";
        rept(i,1,n){
            if(i==l||i==r) cout<<n<<' ';
            else cout<<cnt++<<' ';
        }
        cout<<endl;
    }
}

Subtask 6 & 7

注意到,如果 query(l,r)==query(l+1,r),则 \([l+1,r]\) 中一定有与 \(l\) 同色的球。

有了这个,我们就能二分出每个球右边第一个与它同色的球的位置 \(nxt_i\),进而求出每个球的颜色。

cpp 复制代码
namespace Subtask67{
    int nxt[N];
    // nxt[i]:i右边第一个与i同色的球位置
    void solve(){
        int cnt=1,p;
        rept(i,1,n){
            int l=i+1,r=n+1,mid;
            while(l<r){
                mid=l+r>>1;
                if(query(i,mid)==query(i+1,mid)) r=mid;
                else l=mid+1;
            }
            nxt[i]=l;
        }
        rept(i,1,n){
            if(!ans[i]){
                for(int j=i;j<=n;j=nxt[j]) ans[j]=cnt;
                ++cnt;
            }
        }
        cout<<"! ";
        rept(i,1,n) cout<<ans[i]<<' ';
        cout<<endl;
    }
}

然而这样的询问次数大约是 \(2N\log_2 N\approx 20000\),过不了 Subtask 7。需要想办法去掉一次 query

发现 query(i,mid) 不好省掉,考虑省掉 query(i+1,mid)

仔细观察 query(i+1,mid),发现这个东西等于 \(mid-i-\sum_{k=i+1}^{mid}[nxt_k\le mid]\)。

于是从 \(N\) 到 \(1\) 倒着计算 \(nxt_i\),用树状数组维护和式里面的部分即可。

询问次数 \(N\log_2 N\),时间复杂度 \(O(N \log^2 N)\),目前最优解。

cpp 复制代码
namespace Subtask67{
    int nxt[N],s[N];
    void add(int p,int x){
        while(p<=n){
            s[p]+=x;
            p+=p&-p;
        }
    }
    int ask(int p){
        int res=0;
        while(p){
            res+=s[p];
            p&=p-1;
        }
        return res;
    }
    void solve(){
        pert(i,n,1){
            int l=i+1,r=n+1,mid;
            while(l<r){
                mid=l+r>>1;
                if(query(i,mid)==mid-i-(ask(mid)-ask(i))) r=mid;
                else l=mid+1;
            }
            nxt[i]=l;
            if(nxt[i]<=n) add(nxt[i],1);
        }
        int cnt=1;
        rept(i,1,n){
            if(!ans[i]){
                for(int j=i;j<=n;j=nxt[j]) ans[j]=cnt;
                ++cnt;
            }
        }
        cout<<"! ";
        rept(i,1,n) cout<<ans[i]<<' ';
        cout<<endl;
    }
}

完整代码