26 蓝桥 B组 C/C++

OK哇也是又捐了300块给蓝桥杯官方<(。_。)>

虽然迟到了两周,但还是补完题,写个题解总结一下。

题目链接 https://www.luogu.com.cn/contest/320659

A.青春常数

这题还是比较简单,加1除2或者除2加1都行。

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

int main() {
    long long ans=(2026202520242023+1)/2;
    cout<<ans;
    return 0;
}

B.双碳战略

答案是 n * 2^(n-1),n = 2026。计算并取模,得到答案792264670。

怎么来的呢?我自己是想不出来的,洛谷题解里大佬讲了推导过程,我选了一个我觉得好理解的,也就是思路一。

思路一

原题解网址:https://www.luogu.com.cn/article/es0tmo4n

设灯光打开的状态为 1,关闭的状态为 0,可以得到一个 01 数组。

我们计数组中联通块的个数为出现连续 0 或 1 的块数,不难发现对于有 i 个联通块的数组 a,最少需要操作 i−1 次或 i 次,由于这两种操作对称,所以问题转化为如何求出 a 的个数。

使用隔板法,对于 i 个联通块,需要 i-1 个隔板,有 n-1 个位置,此时的操作次数可以用组合数来算。而总操作次数就是对 i = 1 到 i = n 的所有组合数求和,即:

image

对这个式子进行化简,就可以得到 n * 2^(n-1)。

思路二

使用打表法,得出当 n = 3,4,5的情况下,到达各个状态的操作次数。

n = 3

image

n = 4

image

整理一下,当 n = 3,操作次数0,1,2,3 对应的状态数依次为 1,3,3,1。

同理可得,当n = 4,操作次数0,1,2,3,4 对应的状态数依次为 1,4,6,4,1。

再打表,当n = 5,操作次数0,1,2,3,4,5 对应的状态数依次为 1,5,10,10,5,1。

注意到(注意力惊人!),最少操作次数为 k 的状态数恰好等于二项式系数 C (n,k)(也是杨辉三角第 n 层的数)。

由此计算总和,所有状态的最少操作次数之和为:

image

化简后,也可以得到 n * 2^(n-1)。

代码如下,使用快速幂来计算。

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll MOD=998244353;

ll ksm(ll a,ll b,ll mod){
    ll res=1;
    while(b){
        if(b&1) res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}

signed main(){
    ll n=2026;
    ll ans=n*ksm(2,n-1,MOD)%MOD;
    cout<<ans;//ans=792264670
    return 0;
}

C.循环右移

读题可发现,想要满足右移后的新数组和原数组完全相同,只有原数组的所有数都相同才行。所以只需计算给定范围X 和Y 中间有多少个数即可。

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

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t;
    long long n,x,y;
    cin>>t;
    while(t--){
        cin>>n>>x>>y;
        long long ans=y-x+1;
        if(ans<=0) cout<<0<<"\n";
        else cout<<ans<<"\n";
    }
    return 0;
}

D.蓝桥竞技

条件一,5 人成团:每支战队由且只能由 5 名选手组成。即选手总数模5 等于0 。

条件二,职业互斥:同一支战队内的 5 名选手,必须来自 5 种完全不同的位置。即Ai 的最大值必须小于等于队伍的数量,否则必定会有至少两个同职位的人被分到一个战队。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int t,n;
long long a[N];

int main() {
    cin>>t;
    while(t--){
        cin>>n;
        long long mx=0,sum=0;
        for(int i=0; i<n; i++){
            cin>>a[i];
            mx=max(a[i],mx);
            sum+=a[i];
        }
        if(sum%5==0){ //若可以整除5 ,判断条件二
            if(mx<=sum/5) cout<<"T"<<endl;
            else cout<<"F"<<endl;
        }else{ //否则直接输出 F
            cout<<"F"<<endl;
        }
    }
    return 0;
}

E.LQ聚合

贪心,思路如下。

从左到右扫描,维护:

· cntL:当前已经确定是 L 的个数(包括已处理过的 ? 中被定为 L 的)

· ans:当前已累计的 LQ 对数

对于每个 ? ,计算它后面还有多少可能成为 Q 的位置 = 后面已有的 Q 数 + 后面剩余的 ? 数。

如果当前 cntL > 这个数字,说明立即变成 Q 能获得更多(ans += cntL);

否则,变成 L(cntL++),为后续服务。

这样得到的就是最优解。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
string s;

int main() {
    cin>>n>>s;
    int a[n],cnt1=0; //a[n] 为每个位置后面的Q 的个数,cnt1 为'?' 的总数
    a[n]=0;
    for(int i=n-1; i>=0; i--){ //倒序遍历数组,计算a[n] 和cnt1
        a[i]=a[i+1];
        if(s[i]=='Q') a[i]++;
        if(s[i]=='?') cnt1++;
    }
    ll cnt=0,ans=0; //cnt 表示前面的L 的数量
    for(int i=0; i<n; i++){
        if(s[i]=='L') cnt++;
        if(s[i]=='Q') ans+=cnt; //若字符为Q ,则聚合数加上当前L 的数量
        if(s[i]=='?'){
            cnt1--; //'?' 总数减一,a[i]+cnt1即可表示该位置后面有多少个具有潜力的Q 
            if(cnt>a[i]+cnt1) ans+=cnt;
            else cnt++; 
        }
    }
    cout<<ans<<endl;
    return 0;
}

F.应急布线

题目简述

实验室有 N 台计算机,目前仅剩 M 条网线正常工作,导致网络分裂成若干个互不相连的连通块。可以架设“应急跳线”来连接这些块,每条跳线连接两台计算机(可以来自不同连通块),且会为这两台计算机各增加 1 条跳线的负载。问:

最少需要的跳线数,以及该最小值下的单台计算机最多接入跳线数的最小可能值。

直接用并查集就行,并不难,但苯人当时并没有想到,,,可恶啊(ノ`⊿´)ノ

思路分析

问题1. 最少跳线数

将现有的每台计算机看作一个点,已有的网线看作边。使用并查集可以求出当前图中的连通块个数。

要使 c 个连通块连通,至少需要 c−1 条边(跳线)。因此 ans1 = c−1。

问题2. 最小化单台计算机的最大跳线数

已知需要添加的跳线总数为 ans1 =c−1。

每条跳线会为两台计算机各贡献 1 度(接入跳线数),所以所有计算机的新增度数总和为 2×ans1 。将这 2×ans1 度分配到 N 台计算机上,就使得每台计算机的度数尽可能平均,从而最小化最大值。

因此 ans2 = (2*ans1)/N(向上取整)

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
vector<int> siz(N,1);
int f[N],n,m;

int find(int x){ //查找+路径压缩
    if(f[x]==x) return x;
    return f[x]=find(f[x]);
}

void unionset(int x,int y){ //按秩合并
    x=find(x),y=find(y);
    if(x==y) return ;
    if(siz[x]<siz[y]) {f[x]=y; siz[y]+=siz[x];}
    else {f[y]=x; siz[x]+=siz[y];}
}

int main() {
    cin>>n>>m;
    for(int i=1; i<=n; i++) f[i]=i;
    for(int i=1; i<=m; i++){
        int x,y;
        cin>>x>>y;
        unionset(x,y);
    }
    int ans=0;
    for(int i=1; i<=n; i++){
        if(find(i)==i) ans++; //有多少个父节点,就有多少个连通块
    }
    cout<<ans-1<<" "<<ceil(2.0*(ans-1)/n)<<endl; //注意答案二要向上取整
}

G.理想温度

重述问题

有两个数组 A(当前温度)和 B(理想温度)。一次操作:选择一个连续区间 [l, r] 和一个整数 k,让这个区间内所有 A[i] 变成 A[i] + k。

目标:操作后,让尽量多的位置满足 A[i] == B[i]。

第一步:转化为差值数组

令 a[i]=A[i]−B[i]。一次操作相当于在区间 [l,r] 内给每个 a[i] 加上 k。操作后,位置 i 满足理想温度当且仅当 a[i]+k=0,即 a[i]=−k。

设 c=−k,则我们只需要关注区间内值为 c 的位置(它们会变成 0),以及区间外原本就是 0 的位置(保持不变)。

暴力做法

枚举所有区间 [l,r],统计区间内出现次数最多的值 c(众数),那么:

· 区间内 c 的个数 = 操作后新增的 0 的个数

· 区间外 0 的个数 = 前缀 0 个数 + 后缀 0 个数

· 答案 = 区间内众数个数 + 区间外 0 个数。

暴力枚举所有区间,用 pre 和 suf 数组快速得到区间外 0 的个数,再用 map 统计众数,复杂度 O(n^2 logn),能过 30% 数据。

主要代码如下。

int main() {
    int n; cin>>n;
    vector<int> a(n+1);
    vector<int> pre(n+1);//记录前缀0 的个数
    for(int i=1; i<=n; i++) cin>>a[i];
    for(int i=1,x; i<=n; i++){
        cin>>x;
        a[i]-=x;
        pre[i]=pre[i-1]+(a[i]==0);
    }
    vector<int> suf(n+1);//记录后缀0 的个数
    for(int i=n; i>0; i--){
        suf[i]=suf[i+1]+(a[i]==0);
    }
    int ans=0;
    for(int l=1; l<=n; l++){
        map<int,int> mp;//存储区间内的每个数及其出现次数
        int mx=0;
        for(int r=l; r<=n; r++){//枚举区间
            mp[a[r]]++;
            mx=max(mx,mp[a[r]]);//记录众数的出现次数
            ans=max(ans, mx+pre[l-1]+suf[r+1]);//取每次枚举的最大值
        }
    }
    cout<<ans<<endl;
}

优化思路

固定一个 c,定义权值数组 w[i]:

· 若 a[i]=c,则 w[i]=1(选中它将来会变成 0)

· 若 a[i]=0,则 w[i]=−1(原本是 0,被操作后会变成非 0,损失一个)

· 否则 w[i]=0

那么对于一个区间 [l,r],操作后净增的 0 的数量就是就是 w 在区间上的和。

我们的目标变成:对每个 c,求 w 的最大子段和 S(c),最终答案 = 原有 0 的总数 + max(0,maxS(c))。

由于c 的取值只可能是数组a[i](减了b[i]过后的)里出现过的值,且最优子段的两端一定是值为 c 的位置(如果两端是 0 或其他值,可以收缩到最近的 c,净收益不会减少)。因此,我们只需要考虑 c 的所有出现位置,而相邻两个 c 之间的净增益为 cur = 1 - ( pre[r-1] - pre[l] ),其中 l 和 r 分别表示这两个 c 的位置。

利用动态规划求的思想,令 sum 表示以当前 c 结尾的最大子段和,初始 sum = 1(只取第一个 c)。遍历 j=1…m−1,计算 cur、sum = max(1, sum + cur),更新 mx = max(mx, sum)。

最后,该 c 对应的最大净增量为 mx,该 c 能得到的匹配数为 pre[n] + mx。

由于只处理 c 出现的次数,无论 c 为多少,整个循环遍历下来一共只会执行 n 次,所以复杂度为 O(n)。

代码如下。

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

int main() {
    cin>>n;
    vector<int> a(n+1);
    for(int i=1; i<=n; i++) cin>>a[i];
    vector<int> pre(n+1,0);//存储前缀零
    map<int, vector<int>> mp;//存储数c出现的位置
    for(int i=1,x; i<=n; i++){
        cin>>x;
        a[i]-=x;
        pre[i]=pre[i-1]+(a[i]==0);
        mp[a[i]].push_back(i);
    }
    int ans=pre[n];//初始答案为差值数组中0的个数
    int t0=pre[n];
    for(auto & [t,v]:mp){//枚举每个数c
        if(t==0) continue;// c=0 时净收益 ≤0,无需处理
        int sum=1,mx=1,m=v.size();
        for(int i=1; i<m; i++){//枚举每个数c出现的位置
            int l=v[i-1], r=v[i];
            int cur=1-(pre[r-1]-pre[l]);//从上一个c 到这一个c ,中间的净增益
            sum=max(1,sum+cur);//延申的净增量
            mx=max(mx,sum);//更新mx 为所有sum 的最大值
        }
        ans=max(ans,mx+t0);//答案等于原有0 的个数+ 修改后区间内的最大净增益
    }
    cout<<ans<<endl;
    return 0;
}

H.足球训练

参考了这篇题解 https://www.luogu.com.cn/article/pu8f68bq

要让操作后的累乘最大,每次操作就要找到 a/b 最小的一个队员,因为这样操作会让整体增幅最大。

可以举例验证,a = 1,b = 100 和 a=100,b=100 ,选择前者,总乘积会直接翻101倍,而选后者,总乘积只能增加2倍。

暴力思想是,用小根堆按 a/b 排序,每次弹出最小值,执行 a+=b 后放回,重复 m 次。但这样只能拿60分。

100分的做法,由于每次操作后 a/b 会变大,min(x/y) 一定是单调递增的。二分最终所有 min(a/b) 的最大值,二分结果记为 mid。计算每个队员至少需要操作多少次才能使其 a/b ≥ mid(即 a + k*b ≥ mid * b => k ≥ ceil(mid - a/b)),累加操作数 C,若 C ≤ m 则 mid 可行。二分得到最优的 mid 后,先对每个人操作至满足阈值,剩余操作次数 m' < n(若不小于,说明可以再对 n 个人都进行一轮操作,那么之前的 mid 就不是最优的),也就是m'<1e5,这部分就可以套用60分的做法了。

代码如下

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD=998244353;

struct node{
    ll x, y, id;//分别为当前实力、天赋、编号
    bool operator<(const node & n) const{
        ll nx=n.x, ny=n.y;
        return nx*y< ny*x;
    }
};

int main() {
    ll n,m;
    cin>>n>>m;
    vector<ll> a(n+1),b(n+1);
    for(ll i=1; i<=n; i++){
        cin>>a[i]>>b[i];
    }

    auto check = [&](double mid){//定义二分检查函数
        ll C=0;
        for(ll i=1; i<=n; i++){
            double v= 1.0*a[i]/b[i];//比值
            ll cur = (ll)ceil(mid-v);
            cur = max(0LL, cur);
            C+=cur;
            if(C>m) return 0;
        }
        return 1;
    };

    ll cnt=100;
    double l=0, r=2e14;
    while(cnt--){//循环100次保证精度
        double mid=(l+r)/2;
        if(check(mid)) l=mid;
        else r=mid;
    }
    priority_queue<node> q;
    for(ll i=1; i<=n; i++){
        /*根据二分得到的最佳最小值 l ,为每个队员分配基础训练天数cur
         使其 实力值/天赋值 不低于 l */
        double v= 1.0*a[i]/b[i];
        ll cur=(ll)ceil(l-v);
        cur = max(0LL, cur);
        a[i]+=cur*b[i];
        m-=cur;
        q.push({a[i],b[i],i});
    }

    //剩下的用贪心,每次从队列里取 x/y 最大的队员,训练后放回
    while(m--){
        auto [x,y,id]=q.top();
        q.pop();
        a[id]+=y;
        q.push({a[id],y,id});
    }
    ll ans=1;
    for(ll i=1; i<=n; i++){//计算最终答案
        assert(a[i]<=1e18);
        a[i]%=MOD;
        ans=(ans*a[i])%MOD;
    }
    cout<<ans<<endl;
    return 0;
}


个人总结

现根据个人比赛当天情况,以及补题后的感受,从夯到拉评价各题目的难易程度。

A题,十分简单,拉完了。

B题,比赛的时候看到二进制,猜到了或许要用快速幂,但没有一点思路,打表也没看出规律来,谁曾想居然是杨辉三角……直接给到夯,太难想了。

C题和D题,都挺简单的,思路不难想,代码实现也不难,只能给到 npc。

E题和F题,现在看觉得也不难,贪心和并查集都是会的,但比赛当天脑子晕了,没做出来,直接用的暴力,拿了点点分。介于npc和人上人之间,不给顶级是因为确实不算难;不给npc 是因为自己拉跨了,得分比较低。

G题,这题挺难的,但这题懂了也不是很难(左右脑互博中)。最大子段和知道,但比赛的时候不会;暴力不难,但比赛的时候没想到。这个介于人上人和顶级吧。

H题,比赛的时候靠暴力拿了点点分。感觉这题也好难……完全没想到要找 a/b 最小的,还是题练少了,思维没跟上。理解了为什么要找 a/b 最小的,100分的做法也想不出来。而且大致思路都懂了后,要用代码实现出来也不简单,还是题练少了,编程能力太弱了。这题给到顶级。

总结就是太菜了,还得练啊<(。_。)>

posted @ 2026-04-27 19:04  SDSSpJ306  阅读(59)  评论(0)    收藏  举报