2026牛客寒假训练营3 | 部分题解

Hanzhi_OvO的头像 发布于 2026-02-14 146 次阅读


按照通过和赛后补题顺序:A、B、G、J、H、C、F。

请确保您在阅读本文前已经严肃阅读原题面并且简单理解,因为这里并不提供题面和简要题意

原题指路:https://ac.nowcoder.com/acm/contest/120563#question

钝评

总体来说比较友好,A开起来就是一个直白的签到,难度梯度比较平滑,好评++。坏消息是一些题还是比较阴,H题卡精度是我没想到的。

A:宙天

由于数据范围只有100,拆成数字就是10*10。那么开一个 i 从1到10往下遍历,检测 i*i+1 是不是输入的数字 x 即可。

void solve() {
    // code here'
    int x;
    cin>>x;
    bool flag=false;
    for(int i=1,j;i<=10;i++)
    {
        j=i+1;
        if(x==i*j) 
        {
            flag=true;
            break;
        }
    }
    if(flag) cout<<"YES"<<endl;
    else cout<<"NO"<<endl;
}

B:Random

这里提供两个解答方案。

1.官方解:题目中强调了随机数据,可以严格证明,生成的数字只有极小概率不能用暴力做出来。

严格证明请见:https://ac.nowcoder.com/discuss/1609142

做法:对每个位置 iii,只检查后面固定数量的元素(例如 30 个):

for j=i+1..min(n,i+30), 若 gcd(ai,aj)>1 就输出这对。\text{for }j=i+1.. \min(n,i+30),\ \text{若 }\gcd(a_i,a_j)>1\ \text{就输出这对。}总 gcd 次数为 O(30n)O(30n),当 n=2×105n=2\times 10^5 时约为 6×1066\times 10^6,非常快。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int a[202020];
void solve(){
    int n,i,j;
    cin>>n;
    for(i=1;i<=n;i++)cin>>a[i];
    for(i=1;i<=n;i++){
        for(j=i+1;j<=n&&j<=i+30;j++){
            if(__gcd(a[i],a[j])>1){
                cout<<a[i]<<" "<<a[j]<<"\n";
                return;
            }
        }
    }
    cout<<-1<<"\n";
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    solve();
    return 0;
}

2.个人解:经典的暴力美学,先预处理一个质数表,并手写一个分解质因数(应该有人猜到我怎么想的了)。读入数组数据后用 unordered_map 存下数组中所有数字及其出现次数(这里,如果同一个数出现多次,那么我们直接输出这个数字两次并结束即可。另外,从这里开始到后续的步骤,我们都将跳过元素 1 的存在,毕竟这个数字不算质数也没啥贡献)。然后对数组中非 1 的元素进行分解质因数,并对某个元素的质因数表中含有的元素在原数组中进行搜索,一旦存在该元素的质因数在原数组中,我们输出这对数,否则搜索失败。(好消息:hash了;坏消息:白hash了)

void init()
{
    vector<bool> isp(maxn+1,true);
    isp[0]=isp[1]=false;
    for(int i=2;i<=maxn;i++) {
        if(isp[i]) {
            sprime.push_back(i);
            if(1LL*i*i<=maxn) {
                for(int j=i*i;j<=maxn;j+=i) 
                    isp[j]=false;
            }
        }
    }
}
 
vector<int> fac(int x)
{
    vector<int> factor;
    if(x%2==0) {
        factor.push_back(2);
        while(x%2==0) x/=2;
    }
    for(int p:sprime) {
        if(1ll*p*p>x) break;
        if(p==2) continue;
        if(x%p==0) {
            factor.push_back(p);
            while(x%p==0) x/=p;
        }
    }
    if(x > 1) factor.push_back(x);
    return factor;
}
 
void solve() {
    // code here
    int n;
    cin >> n;
    vector<ll> a(n);
    for(int i=0;i<n;i++) cin>>a[i];
    ftval.clear();
    unordered_map<int, int> val_count;
    for(int i = 0; i < n; i++) {
        if(a[i] != 1) {
            val_count[a[i]]++;
            if(val_count[a[i]] >= 2) {
                cout<<a[i]<<" "<<a[i]<<endl;
                return;
            }
        }
    }
    for(int i=0;i<n;i++) {
        if(a[i]==1) continue;
        vector<int> fs=fac(a[i]);
        for(int f:fs) {
            if(ftval.count(f)) {
                cout<<ftval[f]<<" "<<a[i]<<endl;
                return;
            }
            ftval[f]=a[i];
        }
    }
    cout<<"-1"<<endl;
}

G:スピカの天秤

读入数据之后先判断当前属于什么状态,用 cntl cntr 两个变量维护左右两侧的总重量,如果是平衡态那么很简单,只需要任意拿左边或者右边的一个秤砣就可以破坏平衡,所以如果 cntl==cntr 那么直接输出 1 即可。如果不平衡,我们只需要让偏重的那一侧的重量减小到小于等于偏轻的那一侧。为了尽可能少取,我们肯定优先拿走大的秤砣。

void solve() {
    // code here
    int n,m;
    cin>>n>>m;
    int state=0;
    vector<ll> a(n),b(m);
    ll suml=0,sumr=0;
    for(int i=0;i<n;i++) 
    {
        cin>>a[i];
        suml+=a[i];
    }
    for(int j=0;j<m;j++) 
    {
        cin>>b[j];
        sumr+=b[j];
    }
    if(suml>sumr) state=1;
    else if(suml<sumr) state=-1;
    if(state==0) cout<<1<<endl;
    else if(state==1)
    {
        sort(a.begin(),a.end());
        ll sum=0;
        int i=0;
        while(i<n&&sum+a[i]<=sumr)
        {
            sum+=a[i];
            i++;
        }
        cout<<n-i<<endl;
    }
    else
    {
        sort(b.begin(),b.end());
        ll sum=0;
        int i=0;
        while(i<m&&sum+b[i]<=suml)
        {
            sum+=b[i];
            i++;
        }
        cout<<m-i<<endl;
    }
}

J:Branch of Faith

考察了完全二叉树的常识,对于一个有 n 个节点的完全二叉树,我们定义根节点编号为 1,那么对于节点 i 来说,编号为 2i 的节点就是其左儿子,编号为 2i+1 的节点就是其右儿子。询问节点 x,我们想知道与 x 节点深度相同的节点数(包括 x 节点)。

对于 x,它的深度是由十进制数字 x 转为二进制序列后最高位的位数决定。如果 x 用 2 的幂表达,最高位就是 2d2^d,那么 d 就是 x 节点的深度。显然,同一层的节点编号范围就是 [ 2d2^d, 2d+112^{d+1}-1 ]. 不过需要注意下最大不超过总结点数 n就好了。答案就是 min(2d+11,n)2d+1min ( 2^{d+1}-1,n) - 2^d+1.

int cal_len(ll x)
{
    int res=0;
    while(x>0)
    {
        x>>=1;
        res++;
    }
    return res;
}
 
void solve() {
    // code here
    ll n,q;
    cin>>n>>q;
    int maxd=cal_len(n)-1;
    while(q--)
    {
        ll x;
        cin>>x;
        int depth=cal_len(x)-1;
        if(depth<maxd) cout<<(1ll<<depth)<<endl;
        else
        {
            ll lst_d=1ll<<depth;
            cout<<n-lst_d+1<<endl;
        }
    }
}

H:Tic Tac DREAMIN’

说是算法题,不如说是解析几何的公式推导题。

给定 A(xa,ya)A(x_a,y_a)B(xb,yb)B(x_b,y_b),要在 xx 轴上找点 O(x,0)O(x,0) 使得三角形 ABOABO 面积为 22,即

12(BA)×(OA)=2.\frac12\left| (B-A)\times(O-A)\right|=2.

u=BA=(xbxa, ybya)u=B-A=(x_b-x_a,\ y_b-y_a)v=OA=(xxa, ya)v=O-A=(x-x_a,\ -y_a),则二维叉积为

u×v=(xbxa)(ya)(ybya)(xxa).u\times v=(x_b-x_a)(-y_a)-(y_b-y_a)(x-x_a).

把它写成关于 xx 的一次函数:

g(x)=(ybya)x+(ybya)xa(xbxa)ya.g(x)=-(y_b-y_a)\,x + (y_b-y_a)x_a-(x_b-x_a)y_a.

目标是:g(x)=4.|g(x)|=4.

个人采取了暴力求解,直接解 g(x)=4|g(x)|=4。当 ybyay_b\ne y_a时,取 g(x)=4g(x)=4 的一个解即可:

x=4((ybya)xa(xbxa)ya)(ybya).x=\frac{4-\big((y_b-y_a)x_a-(x_b-x_a)y_a\big)}{-(y_b-y_a)}.

void solve() {
    // code here
    int xa,ya,xb,yb;
    cin>>xa>>ya>>xb>>yb;
    ll c=xa*yb-xb*ya;
    ll d=ya-yb;
    if(d==0) 
    {
        if(abs(c)==4) cout<<"0.0"<<endl;
        else cout<<"no answer"<<endl;
    }
    else
    {
        double res=(4-c)*1.000/d*1.000;
        cout<<fixed<<setprecision(10)<<res<<endl;
    }
}

值得一提的是,提交前最好多确认一下精度是否符合要求。这道题因为卡精度害得我吃了两发罚时。

C:Inverted World

首先明确,根据题意不管是怎么样的原始串给到手,我们最后得到的目标串只有两种形式:010101...和101010... 所以我们需要考虑两种模式串下分别需要对原始串进行几次改动,并选择改动次数最小的。

在开始思考怎么计算改动次数之前,请一定!一定!一定!要读一下题目中对于子序列概念的表述(是的,我赛时没有写出来就是因为没看这个),子序列允许元素不相邻,但是子序列中相邻元素必须相异

既然子序列允许元素不连续,那么干脆把原始串和模式串对比,当某个位置的字符与模式串不匹配,我们把这个字符放入一个新串。也就是说,这个新串记录了当前模式串下所有不匹配的字符,我们称作不匹配串。我们希望对于这个不匹配串中挑选出尽可能少的子序列数,完成扭转。

由于扭转操作是把字符 0 和 1 进行交换,所以我们希望在子序列中挑选出尽可能长的01交替子序列来减少我们的操作次数。聪明的你已经意识到,根据子序列的要求,我们可以用DP方法求出最少的子序列个数。具体地,我们用 c0 c1 这两个变量分别记录以字符 0 和字符 1 为结尾的子序列,计数规则如下:

如果当前遍历到的字符为 1,那么我们第一想法是,如果当前有一个用 0 结尾的子序列(也就是说,变量c0 的值大于0),我们会把这个 1 加入到这个以 0 结尾的子序列后面,这样我们可以少开一个子序列,也就意味着可以减少一次操作次数。当然如果没有以 0 结尾的子序列我们就不强求,另外单独开一个串,也就是 c1++. 同理我们可以知道字符为 0 的处理方式。

int cal(string s) {
    int c0=0,c1=0;
    for(auto i:s)
    {
        if(i=='1') 
        {
            if(c0==0) c1++;
            else
            {
                c0--;
                c1++;
            }
        }
        else
        {
            if(c1==0) c0++;
            else
            {
                c1--;
                c0++;
            }
        }
    }
    return c0+c1;
}
 
void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    string s_n = "";
    for (int i = 0; i < n; i++) {
        char tar = (i % 2 == 0) ? '0' : '1';
        if (tar != s[i]) s_n += s[i];
    }
    int res1 = cal(s_n);
    s_n = "";
    for (int i = 0; i < n; i++) {
        char tar = (i % 2 == 0) ? '1' : '0';
        if (tar != s[i]) s_n += s[i];
    }
    int res2 = cal(s_n);
    cout << min(res1, res2) << endl;
}

E:Energy Synergy Matrix

这道题目不一定用嘴巴能说明清楚...大概知识点在:构造,博弈,最短路(请一定要磕好博弈最短路这两个关键词)。再次提醒,请确保您已经认真阅读题面并简要理解题面

由于是小红先手,作为小红方希望每一步都能下成妙手的效果,也就是意味着一次操作要尽可能让小小红到达的位置远,而且路径长度小。所以小小红在我们(小红)的控制下肯定不能向下拐弯。接下来我们转换视角,来到后手的小紫,由于是后手我们第一要义就是见招拆招,但是又不能让游戏没法玩,不能把路一口气堵死。所以小小红在我们(小紫)的控制下要尽可能产生路线的拐弯。需要注意,小小红也不是傻子,会根据当前地图情况自己规划最优路线。

接下来我们把玩几个小范围样例:n=1 情况下没有啥好玩的,直接 0 步就能走到目标位置。n=2 的时候小红任意选择下方的格子进行放置,小紫为了保证有一条路,如果小红在 (1, 2) 位置放置障碍,那么小紫在被迫放置障碍 (2, 2) 处放置,如果小红在 (2, 2) 位置放置,那么小紫相反,在 (1, 2) 位置放置。此时路径长度为 1 步。

这是 n=2 的时候的初始地图

接下来我们把 n 拓展到 3,小红先手堵住 (2, 1) 的位置才能保证小小红从起点出发不会拐弯。当我们再切换回小紫视角就会发现,小红这一步非常绝妙,如果我们想要满足小紫的目标,我们此时已经无计可施(显然,我们不能堵住上面这一列中的任何一个区块)。此时最短路径长度为 2 步。进一步思考,小红找到了一个绝佳位置 (2, 2),我们把 n 拓展到 4 发现,小红的操作效果还在持续,我们照样没有办法堵路。此时最短路径长度为 3 步。

到这里,我们应该有猜想,一般情况下结论就是 最短路径长度为 n1n-1. 可是样例告诉我们 n = 5 的时候就不成立了。有几种可能:1.这个结论不完全,还缺少了几个多项式;2.这个结论不完全,我们还需要分类讨论。所以接下来我们开始看看 n = 5 的情况:

根据前面的分析,小红在有一次操作机会的时候最优选择只有 (2, 1) 的位置。但是此时这个位置防止小小红转弯的效果已经无法持续了,作为小紫,我们选择 (5, 1) 的位置填堵,此时小小红必然需要转一次弯,所以当 n = 5 的时候必然会有一步多余步数产生。并且不难验证,每 5 格拓展下去必然会多一次拐弯,因此,我们知道,对于上面的结论,正确的应该写为:n1+n/5n-1+\lfloor n/5 \rfloor

void solve() {
    // code here
    ll n;
    cin>>n;
    cout<<n-1+(n/5)<<endl;
}
此作者没有提供个人介绍。
最后更新于 2026-05-10