Luogu P9055 [集训队互测 2021] 数列重排 题解 [ 紫 ] [ 构造 ] [ 数学 ]

数列重排:差点就场切的神仙构造,最后一步想假了,导致我模拟赛荣获 25+5+0 的好成绩!

这题部分分很有启发性,跟着一步一步打基本能想到正解的构造,但也有可能想偏部分分的意思,想假策略。

构造

先看 Subtask 4 的构造,不难发现,当 k=0 时所有重排都能让所有区间的 mex0,而当 k=1 时,01 交替放置,多出的那个放第一位,一定是最优的。

再看 Subtask 5 的构造,显然我们可以先让每个数里的 X 个先组成循环节,比如 012301230123。而对于多出 1 的那些,我们调整循环节内部的顺序,使得多出来接在后面的部分是循环节的一段前缀,比如多出了 1,3,则构造为 13201320132013。因为这样能保证每 m 个就一定可以形成一个 mexm 的区间。

最后看 Subtask 6 的构造,对于多出来没有用的数字,我们可以列出一个无比丑陋的二次函数式子,然后就能证明这些多出的数字分成两半,分别放在两边是最优的。就过了这个子任务。

然后我就以为正解就是在 Subtask 5 的基础上加上 Subtask 6 就做完了,荣获 25pts 的高分!

实际上 Subtask 6 的策略是片面的,当两边插的过多的时候,两边带来的贡献可能比插到中间的贡献少,这一点同样可以从 Subtask 4 的构造中得出。

那么我们先考虑插到中间,一个显然的结论是插到循环节中间不如插到两个循环节之间的部分,因此我们来计算插到两个循环节之间(设这个位置为 p,之前已经在这个位置插了 c 个,插之前序列的长度为 l)的贡献:

  • 当一个区间的左右端点都不在 p 时,显然对区间的贡献没有影响。
  • 当一个区间的左右端点至少有一个在 p 上时,这些区间一共有 l+1 个,减去其中不合法的,一共有 l+12×(k1)c1 个有贡献。其中 2×(k1) 减去的是那些不完整的循环节,c+1 是包括自身在内的插入了的位置。总共减去的贡献是 2×(k1)+c+1

插到两边也是同理,贡献是 l+1(k1)c1,减去的总贡献是 (k1)+c+1

那么显然先插两边是更优的,为了得到临界值,令中间插的 c=0,列出方程:

l+1(k1)c1<l+12×(k1)01

c<2k1

因此,我们得出策略:先在两边插 2k2 个,然后我们轮流进行中间插和两边插,这样一定能让贡献最大化。

实现

首先定义函数 cal(l,r)=(l+r)(rl+1)2,含义是计算 lr 的等差数列的和。

pre 为小于 k 的数的个数,suf 为大于等于 k 的数的个数。

对于一个 f(k),我们进行如下过程求解:

  • k=0,则 f(k)=cal(1,n),直接结束程序。
  • 先计算所有区间的个数 f(k)cal(1,n)
  • 然后减去插入前就不合法的区间(也就是本来 mex<k 的区间),f(k)f(k)cal(pre(k1)+1,pre)
  • 然后先插 min(2k2,suf) 个在两边,注意两边要各一半,两边的个数记为 lx,rx,则 f(k)f(k)cal(k,k+lx1)cal(k,k+rx1)
  • 最后将剩下的均匀插入中间和两边,假设中间的位置有 j1 个(详见代码),则一共可插入的位置有 j+1 个。设 lst=sufmin(2k2,suf),z=lstj+1,y=lstmod(j+1)z 的含义是完整插入的轮数,y 是最后一轮多插的个数。则 f(k)f(k)cal(2k1,2k1+k1)×(j+1)y×(2k1+z)

由此计算即可。

时间复杂度 O(m)

注意不要爆空间。

代码

#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
const ll mod=998244353;
int pw[10000005],m,l,r,x,f[10000005],suf[10000005],n,a[10000005],ans=0;
ll cal(ll l,ll r)
{
    return ((l+r)*(r-l+1)/2)%mod;
}
int main()
{
    //freopen("sample.in","r",stdin);
    //freopen("sample.out","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    pw[0]=1;
    for(int i=1;i<=10000000;i++)pw[i]=(233ll*pw[i-1])%mod;
    cin>>m>>l>>r>>x;
    for(int i=0;i<m;i++)
    {
        char c;
        cin>>c;
        suf[i]=a[i]=x+c-'0';
        n+=suf[i];
    }
    for(int i=m-1;i>=0;i--)
    {
        suf[i]+=suf[i+1];
    }    
    f[0]=cal(1,n);
    for(int i=1,j=a[0];i<=m;i++)
    {
        //计算所有区间
        ll res=cal(1,n),sufsm=suf[i],presm=n-sufsm;
        //减去插入前不合法区间
        ll sm1=cal(presm-i+2,presm);
        res=((res-sm1)%mod+mod)%mod;
        //减去插入左右两端后不合法区间
        ll canp=min(2ll*(i-1),sufsm);
        ll lx=canp/2,rx=canp-lx;
        ll sm2=cal(i,i+lx-1);
        ll sm3=cal(i,i+rx-1);
        res=((res-sm2)%mod+mod)%mod;
        res=((res-sm3)%mod+mod)%mod;
        //减去中间和两端交替插入后不合法区间
        ll lst=sufsm-canp;
        ll z=lst/(j+1),y=lst%(j+1);
        ll sm4=1ll*cal(2*i-1,2*i-1+z-1)*(j+1);
        ll sm5=1ll*(2*i-1+z)*y;
        res=((res-sm4)%mod+mod)%mod;
        res=((res-sm5)%mod+mod)%mod;
        f[i]=res%mod;
        j=min(j,a[i]);
    }
    for(int i=l;i<=r;i++)ans^=((1ll*pw[i]*f[i])%mod);
    cout<<ans;
    return 0;
}
posted @   KS_Fszha  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示