多校A层冲刺NOIP2024模拟赛06

多校A层冲刺NOIP2024模拟赛06

\(T1\) A. 小 Z 的手套(gloves) \(100pts/100pts\)

  • 容易发现将选出的左右手套各升序排序后,同一个位置上的两只手套的尺码差距一定在答案的候选集合里,画个数轴分讨一下就证完了。

  • 部分分

    • \(20 \%\) :因为 \(n=m\) 所以不用管谁选谁不选的问题,故 \(\sum\limits_{i=1}^{n}|l_{i}-r_{i}|\)
    • 另外 \(50 \%\)
      • \(n>m\) 为例。
      • \(f_{i,j}\) 表示前 \(i\) 只左手手套中选了 \(j\) 只左手手套的最小的最大值,状态转移方程为 \(f_{i,j}=\min(f_{i-1,j},\max(f_{i-1,j-1},|l_{i}-r_{j}|))\) ,边界为 \(f_{0,0}=0\)
      • 最终有 \(f_{n,m}\) 即为所求。
    点击查看代码
    ll l[100010],r[100010],f[2][100010];
    int main()
    {
        freopen("gloves.in","r",stdin);
        freopen("gloves.out","w",stdout);
        ll n,m,ans=0,i,j;
        scanf("%lld%lld",&n,&m);
        for(i=1;i<=n;i++)
        {
            scanf("%lld",&l[i]);
        }
        for(i=1;i<=m;i++)
        {
            scanf("%lld",&r[i]);
        }
        sort(l+1,l+1+n);
        sort(r+1,r+1+m);
        memset(f,0x3f,sizeof(f));
        f[0][0]=0;
        if(n>m)
        {        
            for(i=1;i<=n;i++)
            {
                for(j=0;j<=min(i,m);j++)
                {
                    f[i&1][j]=f[(i-1)&1][j];
                    if(j-1>=0)
                    {
                        f[i&1][j]=min(f[i&1][j],max(f[(i-1)&1][j-1],abs(l[i]-r[j])));
                    }
                }
            }
            printf("%lld\n",f[n&1][m]);
        }
        if(n==m)
        {
            for(i=1;i<=n;i++)
            {
                ans=max(ans,abs(l[i]-r[i]));
            }
            printf("%lld\n",ans);
        }
        if(n<m)
        {
            for(i=1;i<=m;i++)
            {
                for(j=0;j<=min(i,n);j++)
                {
                    f[i&1][j]=f[(i-1)&1][j];
                    if(j-1>=0)
                    {
                        f[i&1][j]=min(f[i&1][j],max(f[(i-1)&1][j-1],abs(r[i]-l[j])));
                    }
                }
            }
            printf("%lld\n",f[m&1][n]);
        }
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    
  • 正解

    • 仍以 \(n>m\) 为例。
    • 观察到答案具有单调性,考虑二分答案。假设当前二分出的答案为 \(mid\) ,在第 \(i\) 只右手手套能够匹配的范围内选尽量小的,画数轴即可证明。
    • 至于一只左手手套仅能匹配一只右手手套,使用双指针或 multiset 维护即可,后者比前者多一个 \(\log\)
    点击查看代码
    int x[100010],y[100010];
    multiset<int>a,b;
    multiset<int>::iterator it;
    vector<int>s;
    bool check(int mid,int n,int m)
    {
        bool flag=1;
        s.clear();
        if(n>m)
        {
            for(int i=1;i<=m;i++)
            {
                it=a.lower_bound(y[i]-mid);
                if(it!=a.end()&&*it<=y[i]+mid)
                {
                    s.push_back(*it);
                    a.erase(it);
                }
                else
                {
                    flag=false;
                    break;
                }
            }
            for(int i=0;i<s.size();i++)
            {
                a.insert(s[i]);
            }
            return flag;
        }
        else
        {
            for(int i=1;i<=n;i++)
            {
                it=b.lower_bound(x[i]-mid);
                if(it!=b.end()&&*it<=x[i]+mid)
                {
                    s.push_back(*it);
                    b.erase(it);
                }
                else
                {
                    flag=false;
                    break;
                }
            }
            for(int i=0;i<s.size();i++)
            {
                b.insert(s[i]);
            }
            return flag;
        }
    }
    int main()
    {
        freopen("gloves.in","r",stdin);
        freopen("gloves.out","w",stdout);
        int n,m,ans=0,l=0,r=1e9,mid,i;
        scanf("%d%d",&n,&m);
        for(i=1;i<=n;i++)
        {
            scanf("%d",&x[i]);
            a.insert(x[i]);
        }
        for(i=1;i<=m;i++)
        {
            scanf("%d",&y[i]);
            b.insert(y[i]);
        }
        sort(x+1,x+1+n);
        sort(y+1,y+1+m);
        while(l<=r)
        {
            mid=(l+r)/2;
            if(check(mid,n,m)==true)
            {
                r=mid-1;
                ans=mid;
            }
            else
            {
                l=mid+1;
            }
        }
        cout<<ans<<endl;
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    

\(T2\) B. 小 Z 的字符串(string) \(30pts/30pts\)

  • 部分分

    点击查看代码
    int cnt[3],tmp[3],a[410],ans=0x7f7f7f7f;
    char s[410];
    vector<int>id[3],e;
    void dfs(int pos,int n,int pre)
    {
        if(pos==n+1)
        {
            int sum=0;
            for(int i=1;i<=n;i++)
            {
                for(int j=i+1;j<=n;j++)
                {
                    sum+=(a[j]<a[i]);
                }
            }
            ans=min(ans,sum);
        }
        else
        {
            for(int i=0;i<=2;i++)
            {
                if(i!=pre&&tmp[i]<=cnt[i]-1)
                {
                    tmp[i]++;
                    a[id[i][tmp[i]-1]]=pos;
                    dfs(pos+1,n,i);
                    a[id[i][tmp[i]-1]]=-1;
                    tmp[i]--;
                }
            }
        }
    }
    int main()
    {
        freopen("string.in","r",stdin);
        freopen("string.out","w",stdout);
        int n,flag=1,i;
        cin>>(s+1);
        n=strlen(s+1);
        for(i=1;i<=n;i++)
        {
            cnt[s[i]-'0']++;
            id[s[i]-'0'].push_back(i);
            flag&=(s[i]!=s[i-1]);
        }
        if(flag==1)
        {
            cout<<0<<endl;
        }
        else
        {
            if(cnt[0]>n/2+1||cnt[1]>n/2+1||cnt[2]>n/2+1)
            {
                cout<<-1<<endl;
            }
            else
            {
                dfs(1,n,3);
                cout<<ans<<endl;
            }
        }
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    

  • 正解

    • \(f_{i,j,k,h,0/1/2}\) 表示最终前 \(i\) 个位置上有 \(j\)\(0\)\(k\)\(1\)\(h\)\(2\) 且第 \(i\) 位为 \(0/1/2\) 的最少移动次数。
    • 类似 luogu P3531 [POI2012] LIT-Letters ,我们发现相同的数字间交换一定不优,即相同数字之间的相对位置不变。
    • 压缩 \(j+k+h=i\) 加滚动数组后时空复杂度为 \(O(n^{3})\)
    • 最终有 \(\frac{1}{2}\min\limits_{i=0}^{2}\{ f_{n,cnt_{0},cnt_{1},cnt_{2},i} \}\) 即为所求。
      • \(\frac{1}{2}\) 的系数是因为交换两个相邻位置的过程这两个数中一个向前移动,一个向后移动,用移动次数 \(\times \frac{1}{2}\) 才能得到操作次数。

      • 挂一下官方题解的理解方式。

    点击查看代码
    int cnt[3],f[2][210][210][3];
    char s[410];
    vector<int>pos[3];
    int main()
    {
        freopen("string.in","r",stdin);
        freopen("string.out","w",stdout);
        int n,ans=0x7f7f7f7f,i,j,k;
        cin>>(s+1);
        n=strlen(s+1);
        for(i=1;i<=n;i++)
        {
            cnt[s[i]-'0']++;
            pos[s[i]-'0'].push_back(i);
        }
        if(cnt[0]>(n+1)/2||cnt[1]>(n+1)/2||cnt[2]>(n+1)/2)
        {
            cout<<-1<<endl;
        }
        else
        {
            memset(f,0x3f,sizeof(f));
            for(i=0;i<=2;i++)
            {
                f[0][0][0][i]=0;
            }
            for(i=1;i<=n;i++)
            {
                for(j=0;j<=min(i,cnt[0]);j++)
                {
                    for(k=0;k<=min(i,cnt[1]);k++)
                    {
                        if(j-1>=0)
                        {
                            f[i&1][j][k][0]=min(f[(i-1)&1][j-1][k][1],f[(i-1)&1][j-1][k][2])+abs(pos[0][j-1]-i);
                        }
                        if(k-1>=0)
                        {
                            f[i&1][j][k][1]=min(f[(i-1)&1][j][k-1][0],f[(i-1)&1][j][k-1][2])+abs(pos[1][k-1]-i);
                        }
                        if(i-j-k<=min(i,cnt[2])&&i-j-k-1>=0)
                        {
                            f[i&1][j][k][2]=min(f[(i-1)&1][j][k][0],f[(i-1)&1][j][k][1])+abs(pos[2][i-j-k-1]-i);
                        }
                    }
                }
            }
            for(i=0;i<=2;i++)
            {
                ans=min(ans,f[n&1][cnt[0]][cnt[1]][i]);
            }
            cout<<ans/2<<endl;
        }
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    

\(T3\) C. 一个真实的故事(truth) \(50pts/50pts\)

  • 弱化版: luogu P1638 逛画展 | CF1354B Ternary String

  • 原题: luogu P7230 [COCI2015-2016#3] NEKAMELEONI | SP25818 NEKAMELEONI - NEKAMELEONI

  • 部分分

    • \(20 \%\) :枚举左右端点 \(O(n^{2})\) 处理询问。

      点击查看代码 ```cpp int a[50010],cnt[50]; int main() { // freopen("in.in","r",stdin); // freopen("truth.out","w",stdout); int n,k,m,pd,ans,sum,i,j; scanf("%d%d%d",&n,&k,&m); for(i=1;i<=n;i++) { scanf("%d",&a[i]); } for(j=1;j<=m;j++) { scanf("%d",&pd); if(pd==1) { scanf("%d",&i); scanf("%d",&a[i]); } else { ans=0x7f7f7f7f; sum=0; for(int l=1;l<=n;l++) { memset(cnt,0,sizeof(cnt)); sum=0; for(int r=l;r<=n;r++) { cnt[a[r]]++; sum+=(cnt[a[r]]==1); if(sum==k) { ans=min(ans,r-l+1); break; } } } if(ans==0x7f7f7f7f) { ans=-1; } printf("%d\n",ans); } } fclose(stdin); fclose(stdout); return 0; } ```
    • \(50 \%\) :双指针 \(O(n)\) 处理单组询问。

      点击查看代码
      int a[50010],cnt[50];
      int main()
      {
          freopen("truth.in","r",stdin);
          freopen("truth.out","w",stdout);
          int n,k,m,pd,ans,sum,pos,l,r,i,j;
          scanf("%d%d%d",&n,&k,&m);
          for(i=1;i<=n;i++)
          {
              scanf("%d",&a[i]);
          }
          for(i=1;i<=m;i++)
          {
              scanf("%d",&pd);
              if(pd==1)
              {
                  scanf("%d",&pos);
                  scanf("%d",&a[pos]);
              }
              else
              {
                  sum=0;
                  ans=0x7f7f7f7f;
                  memset(cnt,0,sizeof(cnt));
                  for(l=r=1;r<=n;r++)
                  {   
                      cnt[a[r]]++;
                      sum+=(cnt[a[r]]==1);
                      while(l<=r&&cnt[a[l]]>1)
                      {
                          cnt[a[l]]--;
                          sum-=(cnt[a[l]]==0);
                          l++;
                      }
                      if(sum==k)
                      {
                          ans=min(ans,r-l+1);
                      }
                  }
                  if(ans==0x7f7f7f7f)
                  {
                      ans=-1;
                  }
                  printf("%d\n",ans);
              }
          }
          fclose(stdin);
          fclose(stdout);
          return 0;
      }
      
  • 正解

    • 观察到 \(c \le 50\) ,同 暑假集训CSP提高模拟22 T3 P266. 军队 ,考虑线段树维护前后缀元素最早/晚出现位置,然后 pushup 合并两个区间的信息,求解答案时双指针维护即可。
    • 时间复杂度为 \(O((n+m)k \log n \log k)\) ,写个常数小的写法。
    点击查看代码
    int a[100010],cnt[60];
    pair<int,int>tmp[110];
    struct SMT
    {
        struct SegmentTree
        {
            int ans,lpos[60],rpos[60];
        }tree[400010];
        int lson(int x)
        {
            return x*2;
        }
        int rson(int x)
        {
            return x*2+1;
        }
        void pushup(int rt,int k)
        {
            int sum=0,num=0;
            tree[rt].ans=0x3f3f3f3f;
            memset(cnt,0,sizeof(cnt));
            for(int i=1;i<=k;i++)
            {
                if(tree[lson(rt)].rpos[i]!=0x3f3f3f3f)
                {
                    num++;
                    tmp[num]=make_pair(tree[lson(rt)].rpos[i],i);
                }
                if(tree[rson(rt)].lpos[i]!=0x3f3f3f3f)
                {
                    num++;
                    tmp[num]=make_pair(tree[rson(rt)].lpos[i],i);
                }
            }
            sort(tmp+1,tmp+1+num);
            for(int l=1,r=1;r<=num;r++)
            {
                cnt[tmp[r].second]++;
                sum+=(cnt[tmp[r].second]==1);
                while(l<=r&&cnt[tmp[l].second]>1)
                {
                    cnt[tmp[l].second]--;
                    sum-=(cnt[tmp[l].second]==0);
                    l++;
                }
                if(sum==k)
                {
                    tree[rt].ans=min(tree[rt].ans,tmp[r].first-tmp[l].first+1);
                }
            }
            tree[rt].ans=min(tree[rt].ans,min(tree[lson(rt)].ans,tree[rson(rt)].ans));
            for(int i=1;i<=k;i++)
            {
                tree[rt].lpos[i]=min(tree[lson(rt)].lpos[i],tree[rson(rt)].lpos[i]);
                if(tree[rson(rt)].rpos[i]==0x3f3f3f3f)
                {
                    tree[rt].rpos[i]=tree[lson(rt)].rpos[i];
                }
                else
                {
                    tree[rt].rpos[i]=tree[rson(rt)].rpos[i];
                }
            }
        }
        void build(int rt,int l,int r,int k)
        {
            if(l==r)
            {
                for(int i=1;i<=k;i++)
                {
                    tree[rt].lpos[i]=tree[rt].rpos[i]=0x3f3f3f3f;
                }
                tree[rt].lpos[a[l]]=tree[rt].rpos[a[l]]=l;
                tree[rt].ans=(k==1)?1:0x3f3f3f3f;
                return;
            }
            int mid=(l+r)/2;
            build(lson(rt),l,mid,k);
            build(rson(rt),mid+1,r,k);
            pushup(rt,k);
        }
        void update(int rt,int l,int r,int pos,int val,int k)
        {
            if(l==r)
            {
                tree[rt].lpos[a[l]]=tree[rt].rpos[a[l]]=0x3f3f3f3f;
                a[l]=val;
                tree[rt].lpos[a[l]]=tree[rt].rpos[a[l]]=l;
                tree[rt].ans=(k==1)?1:0x3f3f3f3f;
                return;
            }
            int mid=(l+r)/2;
            if(pos<=mid)
            {
                update(lson(rt),l,mid,pos,val,k);
            }
            else
            {
                update(rson(rt),mid+1,r,pos,val,k);
            }
            pushup(rt,k);
        }
    }T;
    int main()
    {
        freopen("truth.in","r",stdin);
        freopen("truth.out","w",stdout);
        int n,k,m,pd,pos,val,i;
        scanf("%d%d%d",&n,&k,&m);
        for(i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
        }
        T.build(1,1,n,k);
        for(i=1;i<=m;i++)
        {
            scanf("%d",&pd);
            if(pd==1)
            {
                scanf("%d%d",&pos,&val);
                T.update(1,1,n,pos,val,k);
            }
            else
            {
                printf("%d\n",((T.tree[1].ans==0x3f3f3f3f)?-1:T.tree[1].ans));
            }
        }
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    

\(T4\) D. 异或区间(xor) \(20pts/20pts\)

  • 弱化版: luogu P4755 Beautiful Pair

  • 部分分

    • \(20pts\) :模拟。
    点击查看代码
    int a[100010];
    int main()
    {
        freopen("xor.in","r",stdin);
        freopen("xor.out","w",stdout);
        int n,sum,maxx,i,j;
        ll ans=0;
        scanf("%d",&n);
        for(i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
        }
        for(i=1;i<=n;i++)
        {
            sum=maxx=0;
            for(j=i;j<=n;j++)
            {
                sum^=a[j];
                maxx=max(maxx,a[j]);
                ans+=(sum<=maxx);
            }
        }
        printf("%lld\n",ans);
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    
  • 正解

    • 考虑单调栈处理出 \(a_{i}\) 作为最大值的区间 \([l_{i},r_{i}]\) ,注意左闭右开或左开右闭,不能重复统计。
    • 对于满足 \(l_{i} \le x \le i \le y \le r_{i}\)\(x,y\) 若有 \(sum_{y} \bigoplus sum_{x-1} \le a_{i}\)
    • 一个比较假的做法是枚举左端点 \(x\) ,统计 \(y \in i \sim r_{i}\) 中满足 \(sum_{y} \bigoplus sum_{x-1} \le a_{i}\) 的数量,可持久化 \(01Trie\) 即可处理,做法同 CF665E Beautiful Subarrays
      • 存在的问题是单次枚举的时间复杂度为 \(O(i-l_{i})\) ,很容易卡到 \(O(n)\)
    • 考虑启发式合并,选择左右区间中较小的一个进行枚举,有点类似笛卡尔树上分治/合并的写法,时间复杂度为 \(O(n \log n \log V)\)
    点击查看代码
    int a[100010],l[100010],r[100010],sum[100010];
    stack<int>s;
    struct PDS_Trie
    {
        int root[100010],rt_sum=0;
        struct Trie
        {
            int cnt,ch[2];
        }tree[100010<<5];
        int build()
        {
            rt_sum++;
            return rt_sum;
        }
        void insert(int pre,int &rt,int s)
        {
            rt=build();
            int p=rt,q=pre;
            tree[p].cnt=tree[q].cnt+1;
            for(int i=30;i>=0;i--)
            {
                for(int j=0;j<=1;j++)
                {
                    tree[p].ch[j]=tree[q].ch[j];
                }
                tree[p].ch[(s>>i)&1]=build();
                p=tree[p].ch[(s>>i)&1];
                q=tree[q].ch[(s>>i)&1];
                tree[p].cnt=tree[q].cnt+1;
            }
        }
        ll query(int rt1,int rt2,int s,int k)
        {
            ll ans=0;
            for(int i=30;i>=0;i--)
            {
                if((k>>i)&1)
                {
                    ans+=tree[tree[rt2].ch[(s>>i)&1]].cnt-tree[tree[rt1].ch[(s>>i)&1]].cnt;
                    rt1=tree[rt1].ch[((s>>i)&1)^1]; 
                    rt2=tree[rt2].ch[((s>>i)&1)^1];
                }
                else
                {
                    rt1=tree[rt1].ch[(s>>i)&1];
                    rt2=tree[rt2].ch[(s>>i)&1];
                }
                //本来是需要在两棵树各走到叶子节点时就应该 break 的,但可以直接减掉,遂没写
            }
            return ans+tree[rt2].cnt-tree[rt1].cnt;
        }
    }T;
    int main()
    {  
        freopen("xor.in","r",stdin);
        freopen("xor.out","w",stdout);
        int n,pos=1,i,j;
        ll ans=0;
        cin>>n;
        T.insert(T.root[0],T.root[1],0);
        for(i=1;i<=n;i++)
        {
            cin>>a[i];
            sum[i]=sum[i-1]^a[i];
            pos++;
            T.insert(T.root[pos-1],T.root[pos],sum[i]);
            while(s.empty()==0&&a[s.top()]<a[i])
            {
                s.pop();
            }
            l[i]=(s.empty()==0)?s.top()+1:1;
            s.push(i);
        }
        while(s.empty()==0)
        {
            s.pop();
        }
        for(i=n;i>=1;i--)
        {
            while(s.empty()==0&&a[s.top()]<=a[i])
            {
                s.pop();
            }
            r[i]=(s.empty()==0)?s.top()-1:n;
            s.push(i);
        }
        for(i=1;i<=n;i++)
        {
            if(i-l[i]+1<r[i]-i+1)
            {
                for(j=l[i];j<=i;j++)
                {
                    ans+=T.query(T.root[i-1+1],T.root[r[i]+1],sum[j-1],a[i]);
                }
            }
            else
            {
                for(j=i;j<=r[i];j++)
                {
                    ans+=T.query(T.root[l[i]-1-1+1],T.root[i-1+1],sum[j],a[i]);
                }
            }
        }
        cout<<ans<<endl;
        fclose(stdin);
        fclose(stdout);
        return 0;
    }
    
    • 也可以把笛卡尔树建出来,然后在上面进行分治/合并,这样的话就需要写 \(01Trie\) 的合并看,大同小异。
    点击查看官方题解代码

总结

  • 下发题面后觉得 \(T3\) 线段树维护合并两个区间最可做,但碍于码量较大就先把其他题看了一遍。 \(T1\) 想了近 \(40 \min\) 才想出 \(70pts\) ,而且大部分时间都在想几个比较显然的结论; \(T4\) 误认为是 luogu P10853 【MX-X2-T2】「Cfz Round 4」Xor Counting ,一直在想怎么按位开桶做。中间写暴力部分分的暂且不讲,在最后 \(40 \min\) 才想出 \(T1\) 正解并在仅剩 \(20 \min\) 时成功口胡出 \(T4\) 异或不等式直接进行移项后使用主席树维护二维覆盖的类似正解,临近结束时码完了都没意识到异或不等式直接进行移项是假的,而且单调栈维护左右区间时有重复的,所以没调完。
posted @ 2024-10-13 09:04  hzoi_Shadow  阅读(29)  评论(2编辑  收藏  举报
扩大
缩小