整场评价与题目分类
这场题整体梯度较平滑 :前半段以思维/实现成本很低的题为主,后半段逐步过渡到需要构造与证明的中高档题。
题目分类(按 CF 难度粗略估计,仅供训练安排):
- 签到:
A(300)、G(600)、B(800) - easy:
J(1000)、H(1200) - mid:
C(1500)、F(2000) - hard:
I(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-1 与 2-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:
- 先在不经过
2的前提下,求1-1的一条最短路(BFS+前驱回溯)。把这条路上用到的0格标记为已占用;然后检查在避开这些已占用0且不经过1的前提下,2-2是否仍然联通(BFS)。 - 反过来,先求
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) 回帖