Skip to content

20231030 B 组模拟赛 题解

前言

从这场开始模拟赛质量开始断崖式下跌,天天一堆锅。所以后面到 NOIP 前很多我都不打算改了。

密码是通用密码

T1

首先显而易见的是,对于任意一个 $k$,$[n+1,2n]$ 中在二进制下有 $k$ 个 $1$ 的数的数量是单调不减的。

证明很简单,考虑 $n\to n+1$,会去掉 $n+1$,新增 $2n+2,2n+1$。容易发现 $2n+2$ 和 $n+1$ 中 $1$ 的个数是相等的,只会新增一个 $2n+1$。即不会减少,只会增加。

那么可以二分答案。然后对于区间内“二进制下有 $k$ 个 $1$ 的数的数量”可以拆成两个前缀,还是比较好求的,大概思路类似于数位 DP,就不展开讲了。

另外题目有锅,答案在 long long 范围内,不知道文件是改前还是改后的。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=__int128;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
void print(i64 x,i64 p=0){
    if(x==0){
        if(p==0){
            putchar('0');
        }
        return;
    }
    print(x/10,p+1);
    putchar(x%10+'0');
}
#undef gc
const i64 inf=2e18;
i64 t,m,k;
i64 C[75][75],p2[75];
i64 f(i64 x,i64 k,i64 st=70){
    if(st<k) return 0;
    if(x==0&&k==0) return 1;
    if(x==0||k==0) return 0;
    i64 h=st;
    while(p2[h]>x){
        --h;
    }
    i64 res=C[h][k];
    res+=f(x-p2[h],k-1,h);
    return min(res,inf);
}
signed main(){
    t=read();
    forup(i,0,70){
        C[i][0]=1;
        forup(j,1,i){
            C[i][j]=C[i-1][j-1]+C[i-1][j];
            if(C[i][j]>inf) C[i][j]=inf;
        }
    }
    p2[0]=1;
    forup(i,1,70){
        p2[i]=p2[i-1]*2;
    }
    while(t--){
        m=read();k=read();
        i64 ll=1,rr=1e18,mm;
        while(ll<rr){
            mm=(ll+rr)>>1;
            if(f(mm<<1,k)-f(mm,k)<m) ll=mm+1;
            else rr=mm;
        }
        i64 ans1=ll;
        ll=ans1,rr=2e18;
        while(ll<rr){
            mm=(ll+rr)>>1;
            if(f(mm<<1,k)-f(mm,k)==m) ll=mm+1;
            else rr=mm;
        }
        i64 ans2=ll-ans1;
        print(ans1);putchar(' ');print(ans2);puts("");
    }
}

T2

首先容易发现我们可以对每个数找到“能删掉它的最小数”。具体而言,能删掉 $x$ 的最小数 $y=(\left\lfloor\sqrt{2x}\right\rfloor+1)^2-x$。容易发现它大概是分成一段一段递减的(就是对于前半段相等的,后半段显然是个一次函数)。观察小数据发现上段 $y$ 的最大值小于下一段的最小值,接下来考虑证明。

首先分段的依据是 $(\left\lfloor\sqrt{2x}\right\rfloor+1)^2$ 不相等,即 $\left\lfloor\sqrt{2x}\right\rfloor$ 不相等。设 $k=\left\lfloor\sqrt{2x}\right\rfloor$。

容易发现,对于一个 $k$,该段最大的 $x$ 为 $\left\lfloor\frac{(k+1)^2-1}{2}\right\rfloor$,最小的 $x$ 为上一段的最大值 $+1$,即 $\left\lfloor\frac{k^2-1}{2}\right\rfloor+1$。那么对应的 $y$ 就分别是 $(k+1)^2-\left\lfloor\frac{(k+1)^2-1}{2}\right\rfloor$ 和 $(k+1)^2-\left\lfloor\frac{k^2-1}{2}\right\rfloor-1$。故 $k+1$ 段 $y$ 的最小值就是 $(k+2)^2-\left\lfloor\frac{(k+2)^2-1}{2}\right\rfloor$。我们要证明的就是 $(k+1)^2-\left\lfloor\frac{k^2-1}{2}\right\rfloor-1<(k+2)^2-\left\lfloor\frac{(k+2)^2-1}{2}\right\rfloor$。

右边减左边,得:

$$ \begin{aligned} &(k+2)^2-\left\lfloor\frac{(k+2)^2-1}{2}\right\rfloor-\left((k+1)^2-\left\lfloor\frac{k^2-1}{2}\right\rfloor-1\right)\\ =&k^2+4k+4-k^2-2k-1+1+\left\lfloor\frac{k^2-1}{2}\right\rfloor-\left\lfloor\frac{(k+2)^2-1}{2}\right\rfloor\\ =&2k+4+\left\lfloor\frac{k^2-1}{2}\right\rfloor-\left\lfloor\frac{k^2+4k+3}{2}\right\rfloor \end{aligned} $$

然后分奇偶性讨论(因为下取整里面的分母是 $2$)。

若 $k$ 是奇数,那么 $k^2-1,k^2+4k+3$ 均是偶数,则有:

$$ \begin{aligned} &2k+4+\left\lfloor\frac{k^2-1}{2}\right\rfloor-\left\lfloor\frac{k^2+4k+3}{2}\right\rfloor\\ =&2k+4+\frac{(k^2-1)-(k^2+4k+3)}{2}\\ =&2k+4+\frac{-4k-4}{2}\\ =&2>0 \end{aligned} $$

若 $k$ 是偶数,那么那俩东西均是奇数,则有:

$$ \begin{aligned} &2k+4+\left\lfloor\frac{k^2-1}{2}\right\rfloor-\left\lfloor\frac{k^2+4k+3}{2}\right\rfloor\\ =&2k+4+\left\lfloor\frac{k^2-2}{2}\right\rfloor-\left\lfloor\frac{k^2+4k+2}{2}\right\rfloor\\ =&2k+4+\frac{(k^2-2)-(k^2+4k+2)}{2}\\ =&2k+4+\frac{-4k-4}{2}\\ =&2>0 \end{aligned} $$

其实几乎是一样的

那么也就是说,我们找到 $n$ 对应的 $k$ 后,$1\sim k-1$ 段全部都能被删掉,第 $k$ 段有一个后缀可以被删掉。

实现很简单,就不细说了。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=__int128;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
void print(i64 x,i64 p=0){
    if(x==0){
        if(p==0){
            putchar('0');
        }
        return;
    }
    print(x/10,p+1);
    putchar(x%10+'0');
}
#undef gc
const i64 N=1e18,inf=0x3f3f3f3f;
i64 t,n,ans;
signed main(){
    t=read();
    while(t--){
        n=read();
        ans=(1+n)*n/2;
        i64 nw=sqrtl((n-1)*2+1);
        i64 pp=((nw-1)*(nw-1)+1)/2-1,ed=(nw*nw-1)/2,len=n-(nw*nw)/2,st=ed-len+1;
        ans-=(1+pp)*pp/2;ans-=(st+ed)*(ed-st+1)/2;
        print(ans);puts("");
    }
}

T3

首先容易发现,我们构造出的操作序列必然是先通过一系列操作得到一个 $a=b$ 的状态,然后执行多次 $a\gets a+b$,然后再进行若干次减一。容易发现对于一个固定的 $a=b$,后面两步要执行多少次是可以 $O(1)$ 计算的。也就是说,我们把问题转化成了“对于所有 $i=[1,n]$,求出到达 $a=b=i$ 的状态至少需要多少次操作”。

首先一个比较简单的想法是,将一个数 $i$ 向它的所有倍数 $ki$ 和 $i-1$ 连边,边权是转化到对应状态需要的代价(分别是 $k$(因为还要执行一次 $b\gets a$)和 $1$)。然后跑一边迪杰求最短路。这样边数是 $O(n\log n)$($\frac{n}{1}+\frac{n}{2}+\frac{n}{3}\dots$)的,$O(n\log^2n)$ 不太能过,考虑优化。

那么很显然,一个点只需要向它的质数倍连边,复杂度就变成了 $O(n\log n\log\log n)$,就能过了。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=1e6+5,inf=0x3f3f3f3f;
int n,dis[N<<1],vis[N<<1],ans;
struct Node{
    int a,dis;
    bool operator <(const Node &r)const{return dis>r.dis;}
};
priority_queue<Node> q;
vector<int> pri;
int vv[N<<1];
void init(){
    forup(i,2,n*2){
        if(!vv[i]){
            pri.push_back(i);
        }
        for(auto j:pri){
            if(i*j>n*2) break;
            vv[i*j]=1;
            if(!(i%j)) break;
        }
    }
}
signed main(){
    n=read();
    if(n==0){
        puts("1");
        return 0;
    }
    init();
    mem(dis,0x3f);
    dis[1]=0;
    q.push(Node{1,0});
    while(q.size()){
        int a=q.top().a;q.pop();
        if(vis[a]) continue;
        vis[a]=1;
        for(auto i:pri){
            if(a*i>n*2) break;
            if(dis[a*i]>dis[a]+i){
                dis[a*i]=dis[a]+i;
                q.push(Node{a*i,dis[a*i]});
            }
        }
        if(a>0&&dis[a-1]>dis[a]+1){
            dis[a-1]=dis[a]+1;
            q.push(Node{a-1,dis[a-1]});
        }
    }
    printf("%d\n",dis[n]);
}

T4

很聪明的一道题,但是我很蠢。

题目的意思就是让你复制一些边(复制两端点和边权),把原图变成欧拉回路,问加的这些边边权和最小是多少。

首先考虑经典结论:欧拉回路存在的充要条件为每个点度数都为偶数,证明略。由于每条边会给整张图贡献 $2$ 的度数,那么总度数就是偶数。可以简单推出有偶数个点度数为奇数。我们的目标就是把它们的度数变为偶数。

容易发现若复制了一条链,那么只有链两端的点度数奇偶性会改变。也就是说如果我们想同时改变 $A,B$ 度数的奇偶性,只需要复制他们俩之间的最短路即可,可以先跑一遍 floyd 预处理。

那么相当于最初有一个大小为偶数的点集 $S$,表示 $S$ 内的点度数全为奇数。然后每次操作可以花费 $dis_{x,y}$ 的代价把 $x,y$ 两个点都删掉,问删掉整个点集需要的最小代价,容易想到 dp。设 $f_{msk}$ 为删成 $msk$ 需要的最小代价,转移就是枚举删那两个,记搜即可。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=20,inf=1e18;
i64 n,m,mp[N+5][N+5],ans,dp[1<<N|5],deg[N+5];
i64 solve(i64 msk){
    if(dp[msk]!=inf) return dp[msk];
    i64 res=inf;
    forup(i,1,n-1){
        if(!(msk&(1<<(i-1)))||!(deg[i]&1)) continue;
        forup(j,i+1,n){
            if(!(msk&(1<<(j-1)))||!(deg[j]&1)) continue;
            res=min(res,solve(msk^(1<<(i-1))^(1<<(j-1)))+mp[i][j]);
        }
    }
    dp[msk]=res;
    return res;
}
signed main(){
    n=read();m=read();
    forup(i,1,n){
        forup(j,1,n){
            mp[i][j]=inf;
        }
        mp[i][i]=0;
    }
    forup(i,1,m){
        i64 u=read(),v=read(),w=read();
        mp[u][v]=mp[v][u]=min(mp[u][v],w);
        ans+=w;
        deg[u]++;deg[v]++;
    }
    forup(k,1,n){
        forup(i,1,n){
            if(i==k) continue;
            forup(j,1,n){
                mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]);
            }
        }
    }
    i64 msk=0;
    forup(i,1,n){
        forup(j,1,n){
            if(mp[i][j]==inf){
                puts("-1");
                return 0;
            } 
        }
        if(!(deg[i]&1)) msk|=(1<<(i-1));
    }
    mem(dp,0x3f);
    forup(i,0,(1<<n)-1){
        dp[i]=inf;
    }
    dp[msk]=0;
    printf("%lld\n",solve((1<<n)-1)+ans);
}

Comments