20261019 模拟赛 总结

前言

本来以为今天没人上 200 的

太难了……果然 mmt 最开始说题目不友好是真……太不友好了哈哈哈。

真的是 noip T1 T2 拼起来的吗?不像呀,这么难。

mmt 骗我们说 T4 可能确实简单一些,结果搞到最后正解这么难……不过 T4 的部分分确实是最好拿的诶(思考 ing

反正我是区。

基本信息

  • 时间:2026 年 10 月 19 日
  • 时长:4h
  • 题目数量:4 题
  • 得分:100+10+0+55=165pts
  • 排名:rk2

我一定要区起来。

赛时/赛后情况

T1

  • 估分:100pts
  • 实际:100pts
  • 时间段:约 8:10~8:30
  • 耗时:约 20min

最开始看到这个 T1 实在被吓到了。然后思考了一下,发现直接排个序求出前面有多少个数就可以了,还是挺简单的 ovo。注意把自己的那部分减去。

感觉玩明白了这里的期望的统计是很简单的吧,没什么难度,代码也很好写。

不过为什么还有用树状数组做的??没这么难吧 qwq。

T2

  • 估分:10pts
  • 实际:10pts
  • 时间段:约 8:30~11:40(和 T3/T4 一起做的)
  • 耗时:约 40min

在 T2 其实没花那么那么多的时间,但也耗时挺久了。

前面看到这个 \(-1\) 再 \(\div 3\) 其实想到了冰雹猜想,但是具体的忘了,而且我了解到冰雹猜想还是在水 B 站的时候(应该是看毕导看到的吧,记性不好),所以也记得很模糊。

而且更搞笑的是我连倒过来做都没想到。如果想到了倒过来做,就算没有特判负数的那些情况应该也还是有不少分的吧 ovo。

所以我只写了一个很傻子的 dfs 骗了一点点分。TAT


首先我们需要把整个过程倒过来,也就是从 \(1\) 到 \(n\) 变成 \(n\) 到 \(1\)。

那么对于正数的情况,不难发现这就是一个冰雹猜想:如果 \(n\) 是偶数就 \(\div 2\),如果 \(n\) 是奇数就 \(\times 3\) 再 \(+1\),这样操作下去绝对会回到 \(1\)。直接暴力做就行了。

但是如果它是负数,可能会卡到 \(-1\) 或 \(-5\) 或 \(-17\) 的 bug,如果你还根据上面的方案做,会直接掉进死循环。这个时候我们就用 \(+d\) 操作把它拯救回正数就可以啦。

写起来还是很简单的,我赛时可能主要是卡在第一步了吧 /hsh。

T3

  • 估分:20pts
  • 实际:0pts
  • 时间段:约 8:40~11:00(和 T2/T4 一起做的)
  • 耗时:约 60min

最开始一直在玩 \(O(n^2)\) 的分,结果最后调炸了。不过最开始的思路和正解思路也有一定的偏差,因为我是直接暴力处理的,你需要考虑是否遗留、是否用 \(u\) 自身做衔接等等,然后我就炸了,非常破防。

但是感觉链的分是好做的啊……所以就很快写了一个 dp,应该是 \(O(n \log n)\) 的(瓶颈在于中间的二分,可以用双指针优化不过我懒了),估分 20。


?我怎么保龄了?我分呢?(发疯 ing

细节实现错误……都怪权值为 \(0\) 的点……还有一个范围写错了 \(j\) 应该是 \(<i\) 的我给设成了 \(\le i\)……(发狂 ing

我的 \(20\) 分啊!

不过正解还是比较有难度的。

首先需要发现这个点权只有 \(0,1,2\) 肯定不太对劲啊,这个肯定有问题,\(0\) 对答案没贡献,\(1\) 和 \(2\) 引导我们思考奇偶性相关的东西。

然后有一个赛时根本没有风险想出来的需要证明的结论,就是当你存在一个子树它的点权和 \(\ge k\) 且和 \(k\) 同奇偶,是一定能够从子树里提取一个连通块点权和为 \(k\) 的。这个过程可以通过删叶子证明,不过需要特殊讨论一下链的情况。总之就是能证的,但是这谁能想出来谁又能证出来啊。

那么就可以通过 dp + 贪心处理这个问题,只要你这个子树里面能贪就贪到底这样。

只能说代码很短思维很难呀。

T4

  • 估分:50pts
  • 实际:55pts
  • 时间段:约 9:30~11:00(和 T2/T4 一起做的)
  • 耗时:约 70min

比较的区。

大家居然没有人均 50 吗,挺惊讶的,我本来以为这个题的 50 是人均分呀,因为这个和 T1 的难度没什么区别 > <。

我一看到这个题我就感觉诶这不比 T2 T3 可做多了吗!然后导致投入了很多时间到这道根本做不出来的题目里面 w。

大概就是你可以对每个阶乘拆出其包含的每个质因数的个数,那么就可以直接用一个桶存下来。就算你不知道那个什么什么定理,也可以很轻松地推出来一个阶乘里面含有多少个这个质因数这一类的式子,所以那个定理很傻。

得到这个之后,我们就可以暴力处理 \(b\) 了。事实上我赛时是犯了一些糖的,因为这个 \(b\) 可以直接倒序枚举,而不必枚举当前处理到第几个数然后二分之类。不过没什么区别。当你给出一个 \(d\) 后,可以用类似的方法求出它需要的每种质因数的个数,然后除一下看看 \(\min\) 是多少,就可以求出 \(c\)。这就是一组答案。然后把这组答案影响的质因数个数从原来的桶里面减掉就可以了呀。

我感觉这个思路全程都很顺吧,没有任何卡壳或者绕弯的地方,一眼出,这个是真简单吧 qwq。


感叹一下正解太强大了,这种偏移量的方法……好吧也不能说是不会呀,偏移量我 24 年就会了,但是这个方法确实还是比较强,比较巧妙的,也比较绕,搞了将近两个小时才看懂。

首先不能用我最开始那个做法,因为你需要维护每次二分的时候这个 \(b\) 的情况,做不到。只能倒序枚举 \(b\),还比我最开始那个做法少一个老哥。

那么我们要维护的就是一个当前这个质因数的数量 \(t_i\) 和这个质因数在这个 \(b\) 阶乘的情况下需要的个数 \(bp_i\)。维护 \(\min \lfloor \frac{t_i}{bp_i} \rfloor\),这个就是我们要的 \(c\) 了对吧!

所以现在就是有两个核心的问题,第一个是怎么更新 \(t\),第二个是怎么更新 \(bp\)。

直接更新 \(t\) 肯定不行的,因为每次减少的是 \(bp_i \times c\),并不是定值减少。但是 \(c\) 是个定值,\(bp_i\) 又是根据下标的,可以直接维护一个全局 \(tag\) 表示当前减去了多少。那么我们就可以将 \(t_i\) 表示为 \(bas_i - bp_i \times tag\),这个 \(bas_i\) 暂且看作最开始的值。这样就可以做到 \(O(1)\) 修改了,是不是很强大?

然后再看这个 \(bp\) 怎么更新。因为从 \(b\) 切换到 \(b-1\) 就是少了一个 \(b\) 而已,可以直接质因数分解(最开始跑个线性筛求出每个数最小的质因数,可以加速这个分解过程),分解出来的每个质数 \(i\) 做修改 \(bp_i\)。但是修改了之后有一个问题,我们认为 \(t_i = bas_i - bp_i \times tag\) 对吧,但是在这个时候只修改了 \(bp_i\) 而已,\(t_i\) 并没有变化,但在这个式子里求出的所谓 \(t_i\) 却变化了。为了解决这个问题,只能将 \(bas_i\) 也修改了。用式子大概就是 \(bp_i \gets bp_i - d\) 和 \(bas_i \gets bas_i - tag \times d\) 这样,应该还算好理解吧。

这样就又有一个问题,我们需要维护一个全局 \(\min\) 这样,但是这样修改来修改去的我已经晕了,比较基本的东西不能维护它,我们选择使用线段树。原来的式子是 \(\lfloor \frac{t_i}{bp_i} \rfloor\),现在我们令 \(t_i = bas_i - bp_i \times tag\) 之后,重新带入这个式子,可以得到新的式子是 \(\lfloor \frac{bas_i}{bp_i} \rfloor - tag\),而这个 \(tag\) 是定值,因此线段是只需要维护 \(\min \lfloor \frac{bas_i}{bp_i} \rfloor\) 了,而这个值会产生变化当且仅当它是 \(b\) 的什么质因数,\(bas\) 和 \(bp\) 就都被修改了,单点修改一下即可。

每个 \(b\) 并不会修改很多数。因为 \(2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17\) 就已经比上限 \(3 \times 10^5\) 大了,而这还只有 \(7\) 而已,可以看做一个大一点的常数(?。

哦,还有一个小细节,就是你在枚举 \(b\) 的时候需要从 \(\max a_i\) 往后的第一个质数的前一个开始枚举,因为后面的一些合数也是能凑出来的。比如 \(\max a_i = 5\),这个 \(6\) 也是能由 \(2 \times 3\) 凑出来的啦!

写起来倒是还好,但这整个思路能弄清楚是挺难的,不然你猜我为什么做了这么久呢。

总之是一个好题啦。

为了造福后人我放一下这道题的代码吧 ovo。懒得用折叠框了。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 3e5+10;
const LL up = 300006;
const LL INF = 1e18+1099;
LL n,a[N],mxA,cc[N],sum[N];
LL p[N],mnp[N],cnt;bool vis[N];
LL bas[N],bp[N],tag;//[Real]=bas-bp*tag
LL t[N<<2];//SegTree
vector<pLL> ans;//(b,c) (b!)^c
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void init(){
    vis[0]=1,vis[1]=1;cnt=0;
    for(LL i=2;i<=up;i++){
        if(!vis[i])p[++cnt]=i,mnp[i]=i;
        for(int j=1;j<=cnt&&p[j]*i<=up;j++){
            vis[p[j]*i]=1;
            mnp[p[j]*i]=p[j];
            if(i%p[j]==0)break;
        }
    }return;
}
int ls(int u){return (u<<1);}
int rs(int u){return (u<<1|1);}
void push_up(int u){
    t[u]=min(t[ls(u)],t[rs(u)]);
    return;
}
void build(int u,int l,int r){
    if(l==r){
        t[u]=(bp[l]>0?bas[l]/bp[l]:INF);
        return;
    }
    int mid=(l+r)>>1;
    build(ls(u),l,mid);
    build(rs(u),mid+1,r);
    push_up(u);return;
}
void change(int u,int l,int r,int pos){
    if(l==r&&l==pos){
        t[u]=(bp[l]>0?bas[l]/bp[l]:INF);
        return;
    }
    int mid=(l+r)>>1;
    if(pos<=mid)change(ls(u),l,mid,pos);
    else change(rs(u),mid+1,r,pos);
    push_up(u);return;
}
int main(){
    freopen("secure.in","r",stdin);
    freopen("secure.out","w",stdout);
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read(),cc[a[i]]++,mxA=max(mxA,a[i]);
    for(int i=mxA;i>=1;i--)
        sum[i]=sum[i+1]+cc[i];
    init();
    for(int i=1;i<=cnt;i++){
        if(p[i]>mxA)break;
        for(LL x=p[i];x<=mxA;x*=p[i])
            for(LL j=x;j<=mxA;j+=x)bas[p[i]]+=sum[j];
    }
    LL B=mxA+1;
    while(vis[B])B++;
    B--;
    for(int i=1;i<=cnt;i++){
        if(p[i]>mxA)break;
        for(LL x=p[i];x<=mxA;x*=p[i])bp[p[i]]+=B/x;
    }
    build(1,1,mxA);
    tag=0;
    for(LL b=B;b>1;b--){
        LL c=t[1]-tag;
        if(c>0)ans.pb({b,c}),tag+=c;//add tag
        LL tmp=b;
        while(tmp>1){
            LL pr=mnp[tmp],d=0;
            while(tmp%pr==0)tmp/=pr,d++;
            bp[pr]-=d,bas[pr]-=tag*d;
            change(1,1,mxA,pr);
        }
    }
    cout<<ans.size()<<"\n";
    for(pLL x:ans)cout<<x.fr<<" "<<x.se<<"\n";
    return 0;
}

整体反思

T1 没事;T2 没想到倒过来做很可惜,不过就算想到了倒过来做也一定要记得筛查一下负数边界这类的问题(因为确实会有出现死循环的情况嘛);T3 以后写暴力的时候细心一点,注意一些边界值,尤其是一些不带来贡献但占位的 \(0\),很重要千万不要漏掉;T4 倒是感觉进一步做下去的概率也比较小了,那么学会正解这种做法就不错啦。

后记

我是区。

csp rp ++

posted @ 2026-10-19 20:56  嘎嘎喵  阅读(30)  评论(0)    收藏  举报