竞赛讨论区 > 【题解】2026牛客寒假算法基础集训营3题解
头像
比那名居的桃子
发布于 02-07 18:00 北京
+ 关注

【题解】2026牛客寒假算法基础集训营3题解

整场评价与题目分类

这场题整体梯度较平滑 :前半段以思维/实现成本很低的题为主,后半段逐步过渡到需要构造与证明的中高档题。

题目分类(按 CF 难度粗略估计,仅供训练安排):

  • 签到A(300)G(600)B(800)
  • easyJ(1000)H(1200)
  • midC(1500)F(2000)
  • hardI(2100)D(2200)E(2400)

(以上为赛前估的难度,赛后更正:H->1400 C->1800 F->1800 E->2200,其余题不变)

具体难度细表:

题目 综合难度(CF) 思维难度 代码难度 知识点难度 出题人
A 300 0 1 0 神崎兰子
B 800 3 1 2 神崎兰子
C 1800 6 4 4 TRfirst
D 2200 8 6 7 神崎兰子
E 2200 10 8 7 TRfirst
F 1800 8 2 4 神崎兰子
G 600 1 2 2 神崎兰子
H 1400 4 3 4 神崎兰子
I 2100 6 6 8 神崎兰子
J 1000 2 3 3 神崎兰子

A. 宙天

难度:CF 300

知识点:枚举、简单数学

思路:

题目要求判断给定正整数 是否能表示为两个连续自然数的乘积,即是否存在自然数 使得

由于 很小,直接枚举 即可。注意 ,所以枚举 判断是否存在即可。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int x;
    cin>>x;
    for(int i=1;i<=9;i++){
        if(i*(i+1)==x){
            cout<<"YES\n";
            return 0;
        }
    }
    cout<<"NO\n";
    return 0;
}

G. スピカの天秤

难度:CF 600

知识点:贪心、排序、前缀和

思路:

设左侧总重量为 ,右侧总重量为 。当前状态只由 的大小关系决定。每次“拿走砝码”就是从某一侧删掉若干个元素,使对应总和减少。目标是让新状态与旧状态不同。

  • :删掉任意一个砝码都会打破平衡,答案为
  • :不妨设 ,记 。要让状态不再是“左大于右”,只能通过从左侧拿走一些砝码使左侧总和减少到不超过右侧:

因此问题变为:从左侧选若干个数,使它们的和至少为 ,并让选的个数最少。因为所有砝码重量为正数,为了最小化个数,显然应当优先拿走最大的砝码:
把较重一侧的砝码按从大到小排序,取最大的若干个,直到前缀和 ,这个最小前缀长度就是答案。

证明(为什么取最大的是最少个数):对于固定个数 ,能拿走的最大总重量就是“取 个最大值”的和。若这都凑不够 ,则任何 个都不够;反之第一个满足的 一定可行且最小。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

int a[202020],b[202020];

void solve(){
    int n,m;
    cin>>n>>m;
    int sa=0,sb=0;
    for(int i=1;i<=n;i++){cin>>a[i];sa+=a[i];}
    for(int i=1;i<=m;i++){cin>>b[i];sb+=b[i];}
    if(sa==sb){cout<<1<<"\n";return;}
    vector<int>v;
    int d;
    if(sa>sb){
        d=sa-sb;
        v.assign(a+1,a+n+1);
    }else{
        d=sb-sa;
        v.assign(b+1,b+m+1);
    }
    sort(v.begin(),v.end(),greater<int>());
    int s=0;
    for(int i=0;i<(int)v.size();i++){
        s+=v[i];
        if(s>=d){cout<<i+1<<"\n";return;}
    }
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)solve();
    return 0;
}

B. Random

难度:CF 800

知识点:暴力、概率(随机数据)、gcd

思路:

题目保证数组元素为 的随机生成数据,因此可以用“暴力 + 小范围枚举”的方式近似必过。

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

总 gcd 次数为 ,当 时约为 ,非常快。

对于一个“失败概率”下确界的理解:先只看“偶数”。随机生成时每个数为偶数的概率约为 ,设偶数个数为 ,则 ,期望为

如果某个长度为 的连续区间里出现了两个偶数,那么这两个偶数位置差不超过 ,而 ,我们就一定能在枚举窗口时找到答案。

反过来,若所有长度为 的区间里至多一个偶数,则偶数总数至多为 。因此只要

就必然存在两偶数距离 ,算法必然输出答案。

而由 Chernoff 界(或二项分布尾界),

这里 ,当 时上界约为 ,几乎为 0。也就是说仅靠“找两偶数”这一条直觉,就足以解释为什么窗口暴力在随机数据上极稳。

(以下是严格概率证明)

失败概率(随机模型估算):对两独立随机整数,经典结论

一个简短推导(容斥/莫比乌斯反演):令事件 表示“素数 同时整除 ”,则 。利用容斥可得

其中 为莫比乌斯函数(容斥系数)。另一方面

因此一次检查命中 的概率约为 。把每次 gcd 检查近似看作独立,则做 次检查仍未命中的概率近似为

,有

几乎不可能失败。

注:这是基于“随机生成数据”的题面特性给出的概率性做法;对最坏情况排列并不保证一定找到解。本题保证了数据随机,所以可以用该做法。

参考代码:

#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;
}

J. Branch of Faith

难度:CF 1000

知识点:完全二叉树编号、位运算、二进制分段

思路:

题目给出一棵有 个点的完全二叉树,根为 ,点 的左儿子为 ,右儿子为 。对每个询问给出 ,问与 深度相同的点有多少个(包含 ),但树只有编号 的点存在。

关键性质:编号的二进制最高位决定深度。设 的最高位为 ,则 的深度为 ,这一层的节点编号范围为

但由于树只到 ,实际存在的同层节点是

答案就是

实现时只要取 为不超过 的最大 的幂(highest power of two),然后 即可,单次询问

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

void solve(){
    unsigned long long n;
    int q;
    cin>>n>>q;
    while(q--){
        unsigned long long x;
        cin>>x;
        unsigned long long L=1ULL<<(63-__builtin_clzll(x));
        unsigned long long R=min(n,L*2-1);
        cout<<(long long)(R-L+1)<<"\n";
    }
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)solve();
    return 0;
}

H. Tic Tac DREAMIN'

难度:CF 1200

知识点:解析几何、三角形面积、方程、二分

思路:

给定 ,要在 轴上找点 使得三角形 面积为 ,即

,则二维叉积为

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

目标是

特判

  • ,则 为常数:
    • ,任意 都行(输出 即可);
    • 否则无解,输出 no answer
  • ,则 为严格单调一次函数, 必有解(甚至有两解),输出任意一个即可。

做法一(二分答案):先找到“面积为 的交点”,也就是解 得到
因为 是一次函数, 两侧单调递增且趋于无穷。题目坐标范围很小,因此直接取一个足够远的上界(如 )即可保证 ,从而在区间 内对 二分找到满足 的解。

坑点(精度):判定条件是“面积误差 ”,但面积对 的变化率是 ,最大可到 量级;因此为了让面积误差在 内, 往往需要精确到 甚至更小,只保留 3 位小数会直接 WA。直接输出 10 位小数最稳。

参考代码(做法一,二分):

#include<bits/stdc++.h>
using namespace std;
#define int long long

double xa,ya,xb,yb;

double g(double x){
    return -(yb-ya)*x + (yb-ya)*xa - (xb-xa)*ya;
}

void solve(){
    cin>>xa>>ya>>xb>>yb;
    if(yb==ya){
        double t=fabs((xb-xa)*ya);
        if(fabs(t-4.0)<=1e-9) cout<<0.0<<"\n";
        else cout<<"no answer\n";
        return;
    }
    double A=-(yb-ya);
    double B=(yb-ya)*xa-(xb-xa)*ya;
    double x0=-B/A;
    double l=x0,r=1e18;
    for(int i=0;i<200;i++){
        double mid=(l+r)/2;
        if(fabs(g(mid))<4.0) l=mid;
        else r=mid;
    }
    cout<<fixed<<setprecision(10)<<((l+r)/2)<<"\n";
}

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

做法二(推式子):直接解 。当 时,取 的一个解即可:

参考代码(做法二,公式):

#include<bits/stdc++.h>
using namespace std;
#define int long long

void solve(){
    double xa,ya,xb,yb;
    cin>>xa>>ya>>xb>>yb;
    if(yb==ya){
        double t=fabs((xb-xa)*ya);
        if(fabs(t-4.0)<=1e-9) cout<<0.0<<"\n";
        else cout<<"no answer\n";
        return;
    }
    double A=-(yb-ya);
    double B=(yb-ya)*xa-(xb-xa)*ya;
    double x=(4.0-B)/A;
    cout<<fixed<<setprecision(10)<<x<<"\n";
}

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

C. Inverted World

难度:CF 1500 (本题出题人TRfirst估1600)

知识点:构造、前缀和、Kadane(最大子段和)

思路:

目标是把串变成交替串(0101...1010...)。操作是:选择一个子序列 ,要求 中相邻字符不同(0/1 交替),然后把 中所有字符翻转。

先固定目标为 0101...(0 下标偶位为 0,奇位为 1)。把所有已经在正确位置上的字符按原顺序抽出来形成一个新串 mid

  • 偶位正确意味着该位是 0;奇位正确意味着该位是 1

接下来只需在 mid 上计算一个值:把 1 记为 +1,0 记为 -1,求该序列的最大子段和最小子段和,答案就是二者绝对值的最大值:

直观理解:mid 中的 1 表示“奇位已经对了”,0 表示“偶位已经对了”。一次合法操作相当于在某一段上把“奇位对/偶位对”的优势反转,因此每次最多把某段的优势差缩小 1;最少次数就是全局最大优势差(也就是最大绝对子段和)。

同理再对目标 1010... 计算一次,取两者最小值即可。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

int f(string s){
    int mx=0,mn=0,now=0;
    for(int i=0;i<(int)s.size();i++){
        if(s[i]=='1')now++;
        else now--;
        if(now<0)now=0;
        mx=max(mx,now);
    }
    now=0;
    for(int i=0;i<(int)s.size();i++){
        if(s[i]=='1')now++;
        else now--;
        if(now>0)now=0;
        mn=min(mn,now);
    }
    return max(mx,abs(mn));
}

void solve(){
    int n,i;
    string s,t;
    cin>>n>>s;
    t="";
    for(i=0;i<n;i++){
        if((i&1)&&s[i]=='1')t+=s[i];
        if(!(i&1)&&s[i]=='0')t+=s[i];
    }
    int ans=f(t);
    t="";
    for(i=0;i<n;i++){
        if((i&1)&&s[i]=='0')t+=s[i];
        if(!(i&1)&&s[i]=='1')t+=s[i];
    }
    ans=min(ans,f(t));
    cout<<ans<<"\n";
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)solve();
    return 0;
}

F. Energy Synergy Matrix

难度:CF 2000

知识点:博弈、最短路、构造

思路:

把每个格子看成点,相邻格子连边;“放障碍”就是删点,但要求删完后从起点 仍能到达第 列。双方轮流删点直到无点可删,最后在最终障碍布局下,小红走最短路到第 列。

关键观察

  • 游戏结束时,剩余可走格子一定构成一条简单路:如果还存在某个格子不在“所有 路径”上,那么删掉它仍然不会断开到第 列的连通性,游戏就不会结束。
  • 网格里,从第 列走到第 列,最短路必然至少走 次向右;额外步数只来自上下换行。因此最终最短步数可以写成

其中 是最短路中“换行”的最小次数(每换行一次多走 步)。

结论:

的“梯子图”上,要让换行变成不可避免,必须制造一个最小的“强制换行结构”,它至少要占用连续 列(否则总能通过删点把其中一行打通成直走,不产生换行)。

同时,小紫(想让最短路尽量长)可以把这些结构放进互不重叠的 列块里:每 列至少逼出一次换行;而小红(想让最短路尽量短)也能通过优先把局部删成“一条走廊”,保证每 列最多只会被逼出一次换行。

因此最终恰有

答案为

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

void solve(){
    int n;
    cin>>n;
    cout<<n-1+n/5<<"\n";
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)solve();
    return 0;
}

I. BenzenE

难度:CF 2100

知识点:位运算()、XOR 线性基、构造(回溯)

思路:

给定 ,数组 。要输出 满足

1)转化成“子集异或等于目标”

先令初始方案全选 ,记

,直接输出 即可。

否则,若把第 位从 换成 ,整体异或会变化

设你最终换掉的下标集合为 ,则新异或为

要它变成 0,等价于

因此问题变成:从 里选一子集异或出目标 ,并回溯出这个子集。

2)XOR 线性基 + 回溯方案

对所有 建 XOR 线性基。因为 ,只需要维护 0..30 共 31 位,线性基维度最多 31。

为了能输出具体换哪些位置:对每个“插入进基的向量”编号为 ,用一个 unsigned long long 掩码记录线性组合(第 位为 1 表示选了第 个插入向量),同时用 idd[k] 记录该插入向量对应原数组的下标

用线性基去表示目标

  • 若某一位需要消掉但基里没有对应向量,则无解输出 -1
  • 否则得到掩码 pick,把 pick 里为 1 的那些 idd[k] 位置改选 ,其余保持 ,即可保证最终异或为 0。

复杂度:每个测试用例

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

int a[202020],b[202020];
int bas[35];
unsigned long long msk[35];
int idd[70];
char use[202020];

void solve(){
    int n,i,j;
    cin>>n;
    int X=0;
    for(i=1;i<=n;i++){cin>>a[i];X^=a[i];}
    for(i=1;i<=n;i++)cin>>b[i];
    if(X==0){
        for(i=1;i<=n;i++)cout<<a[i]<<(i==n?'\n':' ');
        return;
    }
    for(i=0;i<=30;i++)bas[i]=0,msk[i]=0;
    int cnt=0;
    for(i=1;i<=n;i++){
        int v=a[i]^b[i];
        if(!v)continue;
        int cur=v;
        unsigned long long curm=1ull<<cnt;
        for(j=30;j>=0;j--){
            if(((cur>>j)&1)==0)continue;
            if(!bas[j]){
                bas[j]=cur;
                msk[j]=curm;
                idd[cnt]=i;
                cnt++;
                break;
            }
            cur^=bas[j];
            curm^=msk[j];
        }
    }
    unsigned long long pick=0;
    int cur=X;
    for(j=30;j>=0;j--){
        if((cur>>j)&1){
            if(!bas[j]){cout<<-1<<"\n";return;}
            cur^=bas[j];
            pick^=msk[j];
        }
    }
    for(i=1;i<=n;i++)use[i]=0;
    for(i=0;i<cnt;i++)if((pick>>i)&1)use[idd[i]]=1;
    for(i=1;i<=n;i++)cout<<(use[i]?b[i]:a[i])<<(i==n?'\n':' ');
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)solve();
    return 0;
}

D. 系ぎて

难度:CF 2200

知识点:网格 BFS、最短路、构造

思路:

题意:在 网格里只有两个 1 和两个 2。要分别连通 1-12-2,两条路径都只能走上下左右相邻格,且两条路径不能共用任何格子;并且 1 的路径不能经过 2 的信号点,2 的路径不能经过 1 的信号点;0 格最多被一条路径使用一次。

做法一(贪心+特判):

  • 用一些局部结构(角点被异类完全堵死、2x2 交叉块等)先判 NO;
  • 若存在信号点在内部则直接 YES;
  • 若四个点都在边界,沿边界顺时针读出遇到的信号序列(长度 4),若呈交替 1,2,1,2 则 NO,否则 YES。

这种写法实现简单,但依赖于对平面拓扑的观察与特判。

参考代码(做法一):

#include<bits/stdc++.h>
using namespace std;
#define int long long

int n,m;
vector<string>g;

int f(int r,int c){return r==0||r==n-1||c==0||c==m-1;}

int bc(int r,int c,int r1,int c1,int r2,int c2){
    char x=g[r][c];
    if(x!='1'&&x!='2')return 0;
    char y=(x=='1'?'2':'1');
    return g[r1][c1]==y&&g[r2][c2]==y;
}

void gao(){
    cin>>n>>m;
    g.assign(n,"");
    vector<pair<int,int>>p;
    for(int i=0;i<n;i++){
        cin>>g[i];
        for(int j=0;j<m;j++){
            if(g[i][j]=='1'||g[i][j]=='2')p.push_back({i,j});
        }
    }
    if(bc(0,0,0,1,1,0)||bc(0,m-1,0,m-2,1,m-1)||bc(n-1,0,n-2,0,n-1,1)||bc(n-1,m-1,n-2,m-1,n-1,m-2)){
        cout<<"NO\n";
        return;
    }
    for(int i=0;i+1<n;i++){
        for(int j=0;j+1<m;j++){
            char a=g[i][j],b=g[i][j+1],c=g[i+1][j],d=g[i+1][j+1];
            if((a=='1'||a=='2')&&a==d&&(b=='1'||b=='2')&&b==c&&a!=b){
                cout<<"NO\n";
                return;
            }
        }
    }
    for(auto &q:p){
        if(!f(q.first,q.second)){
            cout<<"YES\n";
            return;
        }
    }
    vector<int>s;
    for(int j=0;j<m;j++){
        char c=g[0][j];
        if(c=='1'||c=='2')s.push_back(c-'0');
    }
    for(int i=1;i<n;i++){
        char c=g[i][m-1];
        if(c=='1'||c=='2')s.push_back(c-'0');
    }
    for(int j=m-2;j>=0;j--){
        char c=g[n-1][j];
        if(c=='1'||c=='2')s.push_back(c-'0');
    }
    for(int i=n-2;i>=1;i--){
        char c=g[i][0];
        if(c=='1'||c=='2')s.push_back(c-'0');
    }
    if((int)s.size()==4&&s[0]==s[2])cout<<"NO\n";
    else cout<<"YES\n";
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)gao();
    return 0;
}

做法二(最短路优先 + 另一对联通性检查):

分别尝试两种顺序,只要一种成功就是 YES:

  1. 先在不经过 2 的前提下,求 1-1 的一条最短路(BFS+前驱回溯)。把这条路上用到的 0 格标记为已占用;然后检查在避开这些已占用 0 且不经过 1 的前提下,2-2 是否仍然联通(BFS)。
  2. 反过来,先求 2-2 最短路并占用其 0,再检查 1-1 是否联通。

实现上,最短路只需要记录前驱并回溯出路径,然后把路径上的 0 置为已占用即可。

参考代码(做法二):

#include<bits/stdc++.h>
using namespace std;
#define int long long

int n,m;
string g[505];
int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};

int id(int x,int y){return x*m+y;}

vector<int>sp(int s,int t,char ban,vector<char>&u){
    int N=n*m;
    vector<int>pre(N,-1);
    queue<int>q;
    pre[s]=s;
    q.push(s);
    while(!q.empty()){
        int v=q.front();q.pop();
        int x=v/m,y=v%m;
        for(int k=0;k<4;k++){
            int nx=x+dx[k],ny=y+dy[k];
            if(nx<0||nx>=n||ny<0||ny>=m)continue;
            int w=id(nx,ny);
            if(pre[w]!=-1)continue;
            if(w!=t&&w!=s){
                if(g[nx][ny]==ban)continue;
                if(g[nx][ny]!='0')continue;
                if(u[w])continue;
            }else{
                if(g[nx][ny]==ban)continue;
            }
            pre[w]=v;
            q.push(w);
        }
    }
    if(pre[t]==-1)return {};
    vector<int>p;
    for(int v=t;;v=pre[v]){
        p.push_back(v);
        if(v==s)break;
    }
    reverse(p.begin(),p.end());
    return p;
}

int ok(int s,int t,char ban,vector<char>&u){
    int N=n*m;
    vector<char>vis(N);
    queue<int>q;
    vis[s]=1;
    q.push(s);
    while(!q.empty()){
        int v=q.front();q.pop();
        if(v==t)return 1;
        int x=v/m,y=v%m;
        for(int k=0;k<4;k++){
            int nx=x+dx[k],ny=y+dy[k];
            if(nx<0||nx>=n||ny<0||ny>=m)continue;
            int w=id(nx,ny);
            if(vis[w])continue;
            if(w!=t&&w!=s){
                if(g[nx][ny]==ban)continue;
                if(g[nx][ny]!='0')continue;
                if(u[w])continue;
            }else{
                if(g[nx][ny]==ban)continue;
            }
            vis[w]=1;
            q.push(w);
        }
    }
    return 0;
}

vector<char>u;

int go(int s1,int t1,char ban1,int s2,int t2,char ban2){
    fill(u.begin(),u.end(),0);
    vector<int>p=sp(s1,t1,ban1,u);
    if(p.empty())return 0;
    for(int v:p){
        int x=v/m,y=v%m;
        if(g[x][y]=='0')u[v]=1;
    }
    return ok(s2,t2,ban2,u);
}

int solve2(){
    vector<pair<int,int>>p1,p2;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            if(g[i][j]=='1')p1.push_back({i,j});
            if(g[i][j]=='2')p2.push_back({i,j});
        }
    }
    int a1=id(p1[0].first,p1[0].second),b1=id(p1[1].first,p1[1].second);
    int a2=id(p2[0].first,p2[0].second),b2=id(p2[1].first,p2[1].second);
    u.assign(n*m,0);
    if(go(a1,b1,'2',a2,b2,'1'))return 1;
    if(go(a2,b2,'1',a1,b1,'2'))return 1;
    return 0;
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--){
        cin>>n>>m;
        for(int i=0;i<n;i++)cin>>g[i];
        cout<<(solve2()?"YES\n":"NO\n");
    }
    return 0;
}

E. 躯树の墓守

难度:CF 2400 (本题出题人TRfirst估本题1800)

知识点:最小生成树、Kruskal、构造、贪心

思路:

要构造一个含 点、 边的简单连通无向图,所有边权互不相同且都在 ,并且整张图的最小生成树(MST)边权和恰好为

核心想法:先固定 MST 的形状为一条链 ,然后通过“跳过”一些小权值来调节 MST 和,同时保证被跳过的小权值只能出现在“已经连通的前缀组件”内部,从而不会破坏 Kruskal 选出的 MST。

1)固定 MST 权值形式

把 MST 的第 条边(连接 )权值设为

其中

最小的 MST 和是

更小则直接输出 NO。令

则我们需要

另外因为边权必须在 且互不相同,最大那条 MST 边满足 ,即

由于 单调不降,所有 都有同样上界。

2)“结构容量”约束(保证 MST 不被更小边替换)

考虑 Kruskal 扫描到权值 之前,链上的前 个点 已经通过 连成一个连通块。

之前一共会出现多少“非 MST 的小边权”?恰好是被跳过的那些权值个数:

为了不让这些更小的边把第 个点提前连进来(从而改变 MST),我们把所有这些小权边都放在点集 的内部边上。

点集 内部一共有 条边,其中链上已经用了 条,所以可放置的“非树边槽位”数量为

因此必须满足

3)构造 (从后往前贪心)

我们要找一组单调不降的 ,同时满足上述每个位置的上界、以及总和为 。做法:从 逆序贪心,令

  • 维护右侧的上界 (逆序时就是“当前不能超过 next”)。

每一步取

并让 ,更新 。若最后 ,说明无论如何都凑不出目标和,输出 NO。

4)把剩余权值塞进非树边

MST 边已经占用了一些权值 ,其余未用的权值按从小到大收集成数组
然后枚举所有非相邻点对 (即 ),按 从小到大、 从小到大输出,并依次赋予权值

由于前缀容量约束 保证了所有小于 的非 MST 权值都能完全放在点集 内部,因此在 Kruskal 过程中,连接新点 的最小跨割边仍然是链边 (权值 ),MST 形状与权值和就被锁死为我们想要的。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long

void solve(){
    int n,m,k;
    cin>>n>>m>>k;
    int mn=n*(n-1)/2;
    if(k<mn){cout<<"NO\n";return;}
    int d=k-mn;
    vector<int>x(n+1);
    int nxt=(1LL<<62);
    for(int i=n-1;i>=1;i--){
        int s=(i-1)*(i-2)/2;
        int v=m-n+1;
        int cap=min(s,v);
        int val=min(cap,min(nxt,d));
        x[i]=val;
        d-=val;
        nxt=val;
    }
    if(d>0){cout<<"NO\n";return;}
    cout<<"YES\n";
    vector<char>used(m+1);
    vector<int>R;
    for(int i=1;i<n;i++){
        int w=i+x[i];
        used[w]=1;
        cout<<i<<" "<<i+1<<" "<<w<<"\n";
    }
    for(int w=1;w<=m;w++)if(!used[w])R.push_back(w);
    int need=m-(n-1),id=0;
    for(int v=3;v<=n&&id<need;v++){
        for(int u=1;u<=v-2&&id<need;u++){
            cout<<u<<" "<<v<<" "<<R[id++]<<"\n";
        }
    }
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)solve();
    return 0;
}

全部评论

(6) 回帖
加载中...
话题 回帖

等你来战

查看全部

热门推荐