题解:CF1349E Slime and Hats

· · 题解

0.前言:

题解区唯二的两篇题解,一篇是官方题解的翻译过分复杂,一篇是 wt 巨佬的讲的过分简略,导致我研究了半个早上+一整个晚上还是和机房同学共同讨论才会做。我太菜了qwq。 同时应该是人生第一道 *3500,遂写篇题解纪念一下。

注意:此题解以文字讲解为主,更习惯看推一坨式子的可移步官方题解。

1.分析Ⅰ(正向做法):

首先我们考虑知道每个人的帽子颜色,能否推出每个人在什么时候离开。

首先容易发现,如果没有黑帽子的人,那么所有人会在时刻 1 离开。

其次,如果有黑帽子的人,并且最后一个人看到前面都是白帽子,那么他会在时刻 1 离开。如果他没有离开,那么倒数第二个人就知道他自己加上他前面的人当中有黑帽子。如果他发现他前面没有白帽子,那么他就会在时刻 2 离开。

同理,第 i 个时刻会有倒数第 i 个人知道了自己加上前面的人有没有黑帽子,同时知道自己前面有没有黑帽子。那么直到最前面一个黑帽子的人,他就会发现此时自己只可能是黑帽子,他就会离开。

我们发现这个“倒数第几个”每次这么形容太麻烦了,于是我们将序列翻转,即令最前面的人编号为 n,这样我们得到结论:

黑帽子中编号最大的人 i 会在时刻 i 离开,下一时刻,编号为 (i,n] 的人会知道自己都是白帽子,因此也会离开,与此同时这个问题变成了一个区间范围 [1,i-1] 的子问题,重复上述过程。最后,当最后一个黑帽子离开时,下一时刻,剩余的人会得知只剩下白帽子,他们会一起离开。

2.分析 Ⅱ(预处理出块):

现在我们开始考虑告诉了你部分人的离开时间 t_i,如何求出每个人头顶的帽子颜色。

我们借用一下 wt 巨佬的图:

注意:以下所有描述规定前或右表示翻转后的编号大,后或左表示翻转后的编号小,与上图统一。

解释一下图片(感觉没必要解释):横轴是每个人的编号,注意我们已经翻转了序列,最右边的点是第一个离开的黑帽子,下一时刻他前面的白帽子,也就是最右边的线段,一起离开。

所以除去最左边的那一段,剩下的一定是多个形如“最左边一个单点,右边一段比他高一个单位的线段”这样的东西。

但是由于信息被挖掉了一些,导致图并不完全,我们的任务其实就是补全这个图,也就知道哪些点是黑哪些点是白了。

图不完全,我们尝试将这些散信息合并合并,得到一些我们可以用的信息帮助解题。于是,我们通过认(xue)真(xi)思(ti)考(jie)知道,首先将不确定离开时间的点去掉,将剩下的散点合并为 3 种类型:

  1. 单独的一个点;
  2. 一条线段;
  3. 一条线段并且左边有一个比其低一个单位的点。

下面称散点合并成的东西为

首先最开始每个点都认为是类型 0 的块,如果他与他前面一个的离开时间一样,那么他们应该属于同一批离开的白帽子,为类型 1 的块。如果他比他前面一个离开时间小 1 个单位,那么他应该是黑帽子,离开后他前面的一批白帽子离开,为类型 2 的块。

对于类型 0,我们既可以认为是类型 1 的一条线段,也可以认为是类型 2 的一个黑帽子只是右边的线段长度为 0。这样我们就可以认为去掉了类型 0

3.分析 Ⅲ(赋颜色):

对于类型 2,他已经拥有了一个黑点,对于类型 1,我们还需要在这条线段的左边给他找一个黑点。对于每个块的内部,其实点的时间就算没有给你,也已经是固定的,应该很好理解,不过多解释。那么我们还需要对块与块之间的没有确定时间的点赋予帽子颜色,以凑上两个块之间的时间差。

定义一些数组、变量。

我们记录第 i 个块的左端点 l,右端点 r,块类型 flag,以及块当中时间最小的点的时间 t(也就是对类型 2 的块,t 是那个黑帽子离开的时间。)

同时设 B_i 表示让第 i 个块走掉的那个黑帽子合法的最靠右的位置。显然对于每个类型 2 的块,有 B_i=b_i \rightarrow l,对于类型 1 的块,有 B_i\lt b_i \rightarrow 1

再设 f_{i,0/1} 表示只考虑第 i 的块及它右边的块,当前这个块的左端点为 0/1B_i 的合法最大值是多好,若无合法值则为 -1。显然,对于类型 1 的块,有 f_{i,1}=-1;对于类型 2 的块,有 f_{i,0}=-1;对于类型 0 的块,两种可能都合法。

为什么我们希望 B_i 越大越好呢?因为我们要用块与块之间的自由点来凑上两个块之间的时间差。那么前面的块用到的 B_i 越靠后,留给中间的自由点就越多,能为凑上时间差提供更好的可能。

如何凑时间差呢?首先要求出时间差。

假设现在在第 i 个块,我们可以从 f_{i+1,0}f_{i+1,1} 转移来。注意,设第二维状态为 jj\in \{0,1\},则第 i 块的结束时间为 (b_i\rightarrow t)-(j\oplus 1) 解释:考虑若第二维状态为 0 表示这个块为类型 1,那么 b_i\rightarrow t 记录的是白帽子离开的时间,因为这是一条线段,只有白帽子,但结束时间应该是黑帽子离开的时间,所以要减 1,反之亦然。

如果这个块为类型 2,我们已经确定了其 B_i;如果这个块为类型 1,我们就从大到小枚举可能的 B_i;如果是类型 0,就两种都考虑即可。然后求出时间差,判断是否可以凑出时间差即可。第 i+1 的块结束时间为 (b_{i+1}\rightarrow t)-(j\oplus 1),并且第 i 的块开始时间的前一个时刻为 (b_i\rightarrow t)-B_i,于是得到时间差为 (b_i\rightarrow t)-B_i-(b_{i+1}\rightarrow t)+(j\oplus 1)

得到时间差后,我们考虑有哪些数可以来凑这个差。显然自由点编号 \in [(b_i\rightarrow r)+1,B_i-1]。同时我们知道一个黑帽子如果编号为 i,贡献的时间就是 i。所以问题变为了给定 d,问能否在 [L,R] 种选择不同的一些数使得它们的和为 d

给出结论,如果选择 num 个数,那么可以凑出 [L+(L+1)+\ldots+(L+num-1),R+(R-1)+\ldots+(R-num+1)] 区间内的数。可以感性理解一下下界和上界,并可以通过每次对最大的数 +1 或对最小的数 -1 达到目标。二分 num 即可。

接下来考虑构造解,也就是构造中间的自由点哪些是黑帽子,这个是容易的。二分出 num 后先将所有点放在最左边,每次将左边最靠右的点移到最右边,每次判断即可。

注意,由于 dp 有两种状态,因此对每种合法的状态都应该构造一组解,而解的转移是整个数组转移,不好搞。所以我们只记录到每一个块的构造的解,并记录从哪里转移的即可。

4.Corner CaseⅠ(最左边的块)

我们知道,最左边的块长的并不一样,如果我们按照前面的做法去做却做不出合法解的时候,我们就需要考虑这种情况。并且一定只可能是这种情况,因为保证有解。

考虑最后一个块,显然它应当是类型 1,并且在这条线段中间有被挖掉的点,它可能是那个黑帽子。枚举被挖掉的点,判断是黑帽子是否合法,一样求出时间差看能否凑到即可。

5.Corner Case Ⅱ(边界哨兵)

初始化 f-1

对于最右边的块,我们可以认为有一个 0 号块,它的 B_0=n+1(b_0\leftarrow t=0)。于是得到 f_{0,1}=n+1

对于最左边的块,我们可以认为其可以在 0 号位置有一个黑帽子,因为是 0 号位置,所以在实现的时候是不会占用时间的。那么我们就在 [-1,-1] 加上一个块即可。

6.Code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,m,nn,f[200005][2],lst[200005][2],anss[200005];
bool vis[200005];
vector<int> ans[200005][2];
struct node{
    int x;
    ll t;
}a[200005];
struct seg{
    ll l,r,flag,t;
}b[200005];
bool cmp(node x,node y){
    return x.x<y.x;
}
ll check(ll tar,int L,int R){
    if(tar<=0){//小特判
        if(!tar&&R-L+1>=0) return 0;
        return -1;
    }
    ll l=0,r=R-L+1,mid;
    while(l<=r){
        mid=l+r>>1;
        if((L+L+mid-1)*mid/2<=tar) l=mid+1;
        else r=mid-1;
    }
    l--;
    return (R+R-l+1)*l/2>=tar? l:-1;
}
void work(int x,int flag,ll l,ll r,ll tar,ll num){
    if(!num) return;
    ll sum=(l+l+num-1)*num/2;
    for(int i=l+num-1,j=r;sum<=tar;i--,j--){
        ll sum2=sum-i+j;
        if(sum2<tar){
            sum=sum2;
        }
        else{
            for(int k=l;k<i;k++){
                ans[x][flag].push_back(k);
            }
            ans[x][flag].push_back(j-(sum2-tar));
            for(int k=j+1;k<=r;k++){
                ans[x][flag].push_back(k);
            }
            break;
        }
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        ll x;
        scanf("%lld",&x);
        if(!x) continue;
        vis[n-i+1]=1;
        a[++nn]=(node){n-i+1,x};
    }
    if(!nn){//小特判
        for(int i=1;i<=n;i++){
            putchar('0');
        }
        return 0;
    }
    sort(a+1,a+nn+1,cmp);
    b[++m]=(seg){a[nn].x,a[nn].x,0,a[nn].t};
    for(int i=nn-1;i>=1;i--){
        if(a[i].t==a[i+1].t){
            b[m].l=a[i].x;
            b[m].flag=1;
        }
        else if(a[i].t==a[i+1].t-1){
            b[m].l=a[i].x;
            b[m].flag=2;
            b[m].t--;
        }
        else{
            b[++m]={a[i].x,a[i].x,0,a[i].t};
        }
    }
    memset(f,-1,sizeof(f));
    f[0][1]=n+1;
    b[m+1]=(seg){-1,-1,0,0};
    for(int i=1;i<=m;i++){
        for(int j=0;j<2;j++){
            if(f[i-1][j]==-1) continue;
            if(b[i].flag==0||b[i].flag==1){
                ll d=(b[i].t-1)-(b[i-1].t+j-1);//j^1=1-j
                for(int k=min(b[i].l-1,d);k>b[i+1].r;k--){
                    ll ok=check(d-k,b[i].r+1,f[i-1][j]-1);
                    if(~ok){
                        if(f[i][0]>=k) continue;//注意要最大
                        f[i][0]=k;
                        ans[i][0].clear();//清空上一次的答案
                        ans[i][0].push_back(k);
                        work(i,0,b[i].r+1,f[i-1][j]-1,d-k,ok);
                        lst[i][0]=j;
                        break;
                    }
                }
            }
            if(b[i].flag==0||b[i].flag==2){
                ll d=(b[i].t-b[i].l)-(b[i-1].t+j-1);
                ll ok=check(d,b[i].r+1,f[i-1][j]-1);
                if(~ok){
                    if(f[i][1]>=b[i].l) continue;
                    f[i][1]=b[i].l;
                    ans[i][1].clear();
                    ans[i][1].push_back(b[i].l);
                    work(i,1,b[i].r+1,f[i-1][j]-1,d,ok);
                    lst[i][1]=j;
                }
            }
        }
    }
    if(!(~f[m][0]||~f[m][1])){
        for(int j=0;j<2;j++){
            if(f[m-1][j]==-1) continue;
            ll d=(b[m].t-1)-(b[m-1].t+j-1);
            for(int k=min(b[m].r,d);k>=b[m].l;k--){
                if(vis[k]) continue;
                ll ok=check(d-k,b[m].r+1,f[m-1][j]-1);
                if(~ok){
                    if(f[m][0]>=k) continue;
                    f[m][0]=k;
                    ans[m][0].clear();
                    ans[m][0].push_back(k);
                    work(m,0,b[m].r+1,f[m-1][j]-1,d-k,ok);
                    lst[m][0]=j;
                    break;
                }
            }
        }
    }
    int nw=1;
    if(~f[m][0]) nw=0;
    for(int i=m;i>=1;i--){
        for(int y:ans[i][nw]){
            anss[y]=1;
        }
        nw=lst[i][nw];
    }
    for(int i=n;i>=1;i--){
        putchar(anss[i]+'0');
    }
    putchar('\n');
    return 0;
}

略微尸山勿喷qwq。