本人26年国二,讲解备赛过程中做过的模拟赛。
比赛说明:本次比赛由东北大学秦皇岛分校、安徽师范大学、安徽工业大学、安徽农业大学、阜阳师范大学、郑州轻工业大学、武昌首义学院联合举办,为蓝桥杯参赛选手们提供两场免费的蓝桥杯模拟赛。赛制与题型均参考蓝桥杯。
文章目录
蓝桥杯多校模拟赛(第二场)题解
A题:好数计数
题目大意
统计区间 [1, 20262026] 中,满足以下条件的"好数"总数:数字中恰好包含2个2、1个0、1个6,其余数字无限制。
核心思路
暴力枚举 + 数位统计 + 提前剪枝。由于区间上限仅约2000万,在C++中完全可以在时限内完成全量枚举,无需数位DP等复杂算法。
正解代码
cpp
#include<bits/stdc++.h>
using namespace std;
bool ck(int x){
string s=to_string(x);
int c2=0,c0=0,c6=0;
for(char c:s){
if(c=='2')c2++;
if(c=='0')c0++;
if(c=='6')c6++;
if(c2>2||c0>1||c6>1)return 0;
}
return c2==2&&c0==1&&c6==1;
}
signed main(){
int ans=0;
for(int i=1;i<=20262026;i++)if(ck(i))ans++;
cout<<ans;
return 0;
}
代码详解
- 校验函数
ck(int x)- 将数字转为字符串,遍历每一位统计
2、0、6的出现次数。 - 遍历过程中若某数字计数超过要求(如
2的个数超过2),直接返回false剪枝,减少无效计算。 - 遍历结束后,判断是否恰好满足
c2==2 && c0==1 && c6==1。
- 将数字转为字符串,遍历每一位统计
- 主函数
- 初始化答案为0,从1到20262026逐个枚举,通过校验则答案加1。
- 最终输出答案。
复杂度分析
- 时间复杂度: O ( N × L ) O(N \times L) O(N×L),其中(N=20262026) , , ,L为数字位数(最多8位),总运算量约1.6亿次,实际因剪枝会更少,可通过。
- 空间复杂度:(O(1)),仅用常数变量。
B题:不等号数独
题目大意
在5×5的网格中填入数字1~5,满足:
- 每行、每列数字不重复(数独规则);
- 相邻格子满足题目给定的不等号大小关系;
- 题目保证有唯一解,按行优先输出最终棋盘的数字序列。
核心思路
DFS回溯 + 约束校验。按行优先顺序逐格填数,利用行、列标记数组快速去重,每填入一个数字就校验所有相关不等号约束,不满足则立即回溯。
正解代码
cpp
#include<bits/stdc++.h>
using namespace std;
int g[10][10];
bool l[10][10],h[10][10];
//l[1][1] 代表第一列已填1
//h[3][4] 第三行已填4
bool ck(){
bool fg=1;
if(g[1][2]>g[1][3]&&g[1][3]!=0)fg=0;
if(g[1][3]>g[1][4]&&g[1][4]!=0)fg=0;
if(g[1][4]>g[1][5]&&g[1][5]!=0)fg=0;
if(g[1][5]>g[2][5]&&g[2][5]!=0)fg=0;
if(g[2][4]>g[2][3]&&g[2][3]!=0)fg=0;
if(g[3][5]>g[3][4]&&g[3][4]!=0)fg=0;
if(g[4][2]>g[3][2]&&g[3][2]!=0)fg=0;
return fg;
}
void dfs(int x,int y){
if(y==6)x++,y=1;
if(x==6){
for(int i=1;i<=5;i++)
for(int j=1;j<=5;j++)
cout<<g[i][j];
return ;
}
if(g[x][y]!=0){//已填
dfs(x,y+1);
return ;
}
for(int i=1;i<=5;i++)
if(!l[y][i]&&!h[x][i]){
g[x][y]=i;
l[y][i]=h[x][i]=i;
if(ck())dfs(x,y+1);
g[x][y]=0;//回溯
l[y][i]=h[x][i]=0;
}
}
int main() {
g[2][2]=3;
g[3][1]=3;
g[5][4]=4;
g[5][5]=1;
h[2][3]=1;
h[3][3]=1;
h[5][4]=1;
h[5][1]=1;
l[1][3]=1;
l[2][3]=1;
l[4][4]=1;
l[5][1]=1;
dfs(1,1);
return 0;
}
代码详解
- 全局数组定义
g[10][10]:存储棋盘数字,0表示未填。h[10][10]:行标记,h[x][i]=1表示第x行已填数字i。l[10][10]:列标记,l[y][i]=1表示第y列已填数字i。
- 校验函数
ck()- 硬编码了题目中所有不等号的位置与方向,逐一检查已填数字是否满足大小关系。
- 若存在已填数字违反不等关系,返回
false;否则返回true。
- DFS搜索函数
dfs(int x, int y)- 边界处理:若列坐标
y==6,则换行,x++,y=1。 - 终止条件:若
x==6,说明所有格子填完,按顺序输出整个棋盘,直接返回。 - 若当前格子已有预设数字,直接递归下一格。
- 枚举1~5的数字,若该行该列未使用过该数字,则填入并标记,校验不等关系合法则继续递归,递归结束后回溯(取消标记、清空格子)。
- 边界处理:若列坐标
- 主函数
- 初始化题目给定的固定数字,同步标记行、列占用状态。
- 从左上角(1,1)开始DFS搜索。
复杂度分析
- 时间复杂度:最坏为 O ( 5 25 ) O(5^{25}) O(525),但由于强约束(数独规则+不等号),实际搜索树极小,瞬间即可出解。
- 空间复杂度:(O(1)),棋盘大小固定。
C题:自定义字符串排序
题目大意
给定n个字符串,按特殊规则排序:
- 按
a到z的顺序,依次比较每个字符的出现次数,出现次数多的字符串更小(排前面); - 若所有字符出现次数均相同,则按字典序升序排列。
核心思路
自定义比较函数 + STL sort。直接利用C++标准库的排序接口,实现符合题目规则的比较逻辑。
正解代码
cpp
#include<bits/stdc++.h>
using namespace std;
bool cmp(string s1,string s2){
int cnt1[30]={0},cnt2[30]={0};
for(char c:s1)cnt1[c-'a']++;
for(char c:s2)cnt2[c-'a']++;
int fd=0;
for(int i=0;i<26;i++){
if(cnt1[i]!=cnt2[i]){
if(cnt1[i]>cnt2[i])return 1;
else return 0;
}
}
return s1<s2;
}
signed main(){
int n;
cin>>n;
string s;
vector<string>v;
for(int i=0;i<n;i++){
cin>>s;
v.push_back(s);
}
sort(v.begin(),v.end(),cmp);
for(auto t:v)cout<<t<<' ';
return 0;
}
代码详解
- 比较函数
cmp(string s1, string s2)- 分别统计两个字符串中26个字母的出现次数,存入两个计数数组。
- 从
a到z遍历26个字母:- 若当前字母次数不同,次数多的字符串更小,返回对应布尔值。
- 若所有字母次数都相同,返回
s1 < s2(原生字典序比较)。
- 主函数
- 读入n个字符串存入vector。
- 调用
sort配合自定义cmp完成排序。 - 按顺序输出所有字符串。
复杂度分析
- 时间复杂度: O ( n log n × ( L + 26 ) ) O(n \log n \times (L + 26)) O(nlogn×(L+26)),其中 n ≤ 2 × 10 5 n \le 2\times10^5 n≤2×105,字符串最大长度(L=10)。26次字母比较可视为常数,总复杂度完全符合时限要求。
- 空间复杂度:(O(nL)),存储所有字符串。
D题:最大权值好括号
题目大意
给定由(、)、.、数字组成的字符串,定义"好括号":
- 以
(开头,以)结尾; - 括号内部不含其他括号;
- 内部由
.和正整数组成,允许.开头或结尾,允许连续.; - 内部至少包含一个正整数。
求所有好括号中,内部数字之和的最大值。
核心思路
线性扫描 + 状态标记。由于好括号内部不能嵌套括号,因此每个右括号只匹配最近的左括号,单次遍历即可完成所有计算。
正解代码
cpp
#include<bits/stdc++.h>
using namespace std;
int ans,n;
string s;
/*
()有四种情况
(( 第一个左括号不用管 num清零
() 计数并清零
)( 右括号不用管
)) 不用管
*/
signed main(){
cin>>s;
n=s.size();
bool fd=0;//记录左括号
int cnt=0,num=0,tmp=0;
for(int i=0;i<n;i++){
if(s[i]=='('){
fd=1;
num=0;
cnt=0;
}
else if(s[i]==')'){
if(fd){
cnt+=num;//防止)前一个字符不是.
ans=max(ans,cnt);
}
num=0;
cnt=0;
fd=0;
}
else if(s[i]=='.'){
cnt+=num;
num=0;
}
else {//数字
num=num*10+s[i]-'0';
}
}
cout<<ans;
return 0;
}
代码详解
- 核心变量
fd:状态标记,fd=1表示当前处于一个左括号的内部(未遇到其他括号)。num:当前正在累计的数字值。cnt:当前括号内所有数字的总和。ans:全局最大权值,初始为0。
- 逐字符处理逻辑
- 遇到
(:重置所有状态,fd=1,num和cnt清零。新的左括号意味着之前的括号内部出现了嵌套,不再是好括号,直接丢弃。 - 遇到
):若当前处于括号内(fd=1),则将当前累计的num加入cnt,用cnt更新全局最大值ans,随后重置所有状态。 - 遇到
.:将当前累计的num加入cnt,num清零,准备累计下一个数字。 - 遇到数字:
num = num * 10 + (s[i]-'0'),逐位累计数值。
- 遇到
复杂度分析
- 时间复杂度:(O(n)) , n ,n ,n为字符串长度( ≤ 10 6 \le 10^6 ≤106),单次线性遍历。
- 空间复杂度:(O(1)),仅用常数变量。
E题:子矩阵旋转查询
题目大意
初始n×n矩阵满足 a[i][j] = (i-1)*n + j,进行q次操作:每次选中一个子矩阵,顺时针或逆时针旋转90°。最后回答d个查询,输出指定位置的最终数值。
其中 n , q ≤ 10 5 n,q \le 10^5 n,q≤105, d ≤ 10 d \le 10 d≤10。
核心思路
逆向思维 + 坐标变换。直接模拟矩阵旋转会因数据范围过大超时,而查询次数极少,因此对每个查询点,逆序执行所有操作的坐标反变换,找到该位置的数值在初始矩阵中的对应坐标,直接用公式计算。
正解代码
cpp
#include <bits/stdc++.h>
using namespace std;
struct ca{
int op;
int x;
int y;
int len;
} ;
int main()
{
int n,q,d,dx,dy;
cin>>n>>q;
ca a[110000];
for(int i=0;i<q;i++)
{
cin>>a[i].op>>a[i].x>>a[i].y>>a[i].len;
}
cin>>d;
while(d--)
{
cin>>dx>>dy;
for(int i=q-1;i>=0;i--)
{
int cntx=dx-a[i].x;
int cnty=dy-a[i].y;
if(cntx>=0&&cntx<a[i].len&&cnty>=0&&cnty<a[i].len)
{
if(a[i].op==1)
{
dx=a[i].len+a[i].x-1-cnty;
dy=a[i].y+cntx;
}
else
{
dx=a[i].x+cnty;
dy=a[i].len+a[i].y-1-cntx;
}
}
}
cout<<(long long)(dx-1)*n+dy<<endl;
}
return 0;
}
代码详解
- 核心思想
旋转操作是可逆的。对于最终位置(dx, dy),我们从最后一次操作倒推到第一次操作,每次判断该点是否在操作的子矩阵内:若在,则执行反向的坐标变换,得到操作前的坐标。最终得到的初始坐标代入公式即可算出数值。 - 坐标变换推导(相对坐标法)
设子矩阵左上角为(x, y),边长为len。先将绝对坐标转为相对左上角的偏移量:cntx = dx - x,cnty = dy - y。- 正向顺时针旋转90°的逆向(即逆时针变换) :
dx = x + len - 1 - cnty
dy = y + cntx - 正向逆时针旋转90°的逆向(即顺时针变换) :
dx = x + cnty
dy = y + len - 1 - cntx
- 正向顺时针旋转90°的逆向(即逆时针变换) :
- 主函数流程
- 读入n、q,存储所有操作的类型、坐标、边长。
- 读入d个查询,对每个查询点:
- 逆序遍历所有q次操作;
- 若当前点在子矩阵范围内,执行对应坐标变换;
- 所有操作处理完毕后,用公式
(dx-1)*n + dy计算初始值并输出。
复杂度分析
- 时间复杂度: O ( d × q ) O(d \times q) O(d×q), d ≤ 10 d \le 10 d≤10, q ≤ 10 5 q \le 10^5 q≤105,总运算量约100万,效率极高。
- 空间复杂度:(O(q)),存储所有操作。
F题:最大异或分段与值
题目大意
将数组划分为m个连续区段,每个区段计算异或和,再将所有区段的异或和做按位与运算。求该与运算结果的最大值。
核心思路
按位贪心 + 前缀异或校验。二进制高位的权重远大于所有低位之和,因此从最高位到最低位依次尝试保留该位,用校验函数判断是否存在合法划分。
正解代码
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+9;
int n,m,a[N],pre[N];
/*
按位贪心
从二进制高位30到低位枚举
看是否存在m段中都有该位 若有加入到ans中 继续枚举
否则继续向低位枚举
ck(x) 函数检查是否存在m段 每段异或和为x
先分为m-1段
最后一段使用前缀异或表示
*/
bool ck(int x){
int cnt=0,last=0;
for(int i=1;i<=n;i++){
int tmp=pre[i]^pre[last];
if((tmp&x)==x){
cnt++;
last=i;
if(cnt==m-1)break;
}
}
//最后一段 [last+1,n] pre[last]^pre[n]
if(cnt==m-1&&((pre[last]^pre[n])&x)==x)return 1;
return 0;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
pre[i]=pre[i-1]^a[i];
}
int ans=0;
for(int j=31;j>=0;j--){
int x=ans+(1ll<<j);
if(ck(x))ans=x;
}
cout<<ans;
return 0;
}
代码详解
- 前缀异或预处理
定义pre[i]为前i个数字的异或和,则区间[l, r]的异或和为pre[r] ^ pre[l-1]。 - 校验函数
ck(int x)
判断是否存在一种划分,使得所有m个区段的异或和都包含x的所有二进制位(即(异或和 & x) == x)。- 初始化
cnt=0(分段数),last=0(上一段的结束位置)。 - 遍历数组,若当前区间
[last+1, i]的异或和满足条件,则分一段,cnt++,更新last=i。 - 当分到
m-1段时提前终止,检查最后一段[last+1, n]是否满足条件。若满足则返回true。
- 初始化
- 按位贪心过程
- 初始答案
ans=0,从第30位(因为 a i < 2 31 a_i < 2^{31} ai<231)到第0位倒序枚举。 - 令候选值
x = ans | (1 << j),即尝试把第j位加入答案。 - 若
ck(x)为真,说明可以保留该位,更新ans = x。 - 枚举结束后输出
ans。
- 初始答案
复杂度分析
- 时间复杂度: O ( 31 × n ) O(31 \times n) O(31×n), n ≤ 3 × 10 5 n \le 3\times10^5 n≤3×105,总运算量约900万,非常高效。
- 空间复杂度:(O(n)),存储前缀异或数组。
关键技巧
- 按位贪心:位运算求最大值的经典策略,优先保证高位为1。
- 提前终止:分到m-1段就停止遍历,只校验最后一段,减少常数开销。
G题:01串最小切割
题目大意
将01字符串切割为若干连续子串,满足:
- 子串不是"交替串"(交替串定义:长度≥2且相邻字符均不同);
- 子串长度不超过K。
求最少需要切割多少次(切割次数 = 分段数 - 1)。
一、暴力DP思路(过80%)
核心思路
线性DP + 暴力枚举转移 。定义dp[i]为前i个字符的最少分段数,对每个位置向前枚举所有合法长度的子串,判断是否为非交替串,若是则进行状态转移。
暴力代码(过80%)
cpp
#include <bits/stdc++.h>
using namespace std;
const int inf=1e9;
int t,n,k;
string s;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin>>t;
while(t--){
cin>>n>>k>>s;
//if(n>1000)continue;
s=" "+s;
int cnt=0;
for(int i=1;i<n;i++)
if(s[i+1]!=s[i])cnt++;
if(cnt==n-1){ //交替串
cout<<n-1<<'\n';
continue;
}
vector<int>dp(n+1,inf);
//前i个字符最少分为多少段
dp[0]=0;
for(int i=1;i<=n;i++)
{
dp[i]=dp[i-1]+1;//长度为1的字符串
int pos=i-k+1;
bool fg=0;//检查是否为交替串
//字串为s[j...i] 长度为i-j+1
for(int j=i-1;j>=max(1,pos);j--)
{
//加入s[j]后产生新相邻对 s[j],s[j+1]
if(s[j]==s[j+1])fg=1;
if(fg)dp[i]=min(dp[i],dp[j-1]+1);
}
}
cout<<dp[n]-1<<'\n';
}
return 0;
}
代码详解
- 全交替串特判
若整个字符串都是交替串(所有相邻字符都不同),则无法分出长度≥2的合法子串,只能全部分为长度1的段,答案为n-1,直接输出。 - DP定义与初始化
dp[i]:前i个字符的最少分段数,初始化为无穷大,dp[0] = 0。
- 状态转移
- 保底转移:
dp[i] = dp[i-1] + 1,即第i个字符单独成段(长度1永远合法)。 - 枚举转移:向前枚举左端点
j,范围[max(1, i-K+1), i-1],对应子串长度2到K。- 用
fg标记子串是否为非交替串:从后往前遍历,若遇到s[j] == s[j+1],说明存在相邻相同,不是交替串,fg=1。 - 若
fg=1(子串合法),则dp[i] = min(dp[i], dp[j-1] + 1)。
- 用
- 保底转移:
- 最终答案
切割次数 = 分段数 - 1,即dp[n] - 1。
复杂度分析
- 时间复杂度:(O(nK)) ,当 n 和 K 均为 1 e 5 时退化为 O ( n 2 ) ,当n和K均为1e5时退化为O(n^2) ,当n和K均为1e5时退化为O(n2),会超时,仅能通过约80%的数据。
- 空间复杂度:(O(n))。
二、正确优化思路(满分解法)
核心优化方向
利用交替串的性质,将暴力枚举转移优化为滑动窗口最小值查询,时间复杂度降至(O(n))。
具体推导
- 合法子串的性质
一个子串不是交替串,等价于子串内至少存在一对相邻相同的字符。
预处理相邻相同的位置:定义bad[i] = 1当且仅当s[i] == s[i+1]。则子串[l, r]合法,当且仅当[l, r-1]中存在至少一个bad[i] = 1。 - 转移区间的确定
对每个右端点i,找到它前面最后一个相邻相同的位置p(即最大的p < i且bad[p]=1):- 所有左端点
j ≤ p的子串[j, i],都包含这个相邻相同对,因此都是非交替串(合法); - 左端点
j > p的子串都是交替串(不合法)。
结合长度限制K,合法的左端点范围为:j ∈ [max(1, i-K+1), p]。
- 所有左端点
- DP优化
转移方程变为:dp[i] = min{ dp[j-1] } + 1,其中j在上述区间内。
这是典型的滑动窗口最小值 问题,可以用单调队列 维护区间内dp[j-1]的最小值,每次转移只需(O(1))时间。 - 边界处理
保底的单独分段(j=i)始终有效,若区间为空则直接取保底值。
复杂度
- 时间复杂度:(O(n)),单次遍历即可完成预处理和DP转移。
- 空间复杂度:(O(n))。
H题:多关键点最短路径最小值
题目大意
给定无向带权连通图,以及k个关键点,求任意两个不同关键点之间最短路径的最小权值。
其中 n , m ≤ 10 5 n,m \le 10^5 n,m≤105, k ≤ 10 5 k \le 10^5 k≤105。
核心思路
多源Dijkstra + 归属源点标记。将所有关键点同时作为源点跑最短路,记录每个点归属的最近关键点;当遇到连接不同归属点的边时,用两端距离加边权更新全局最小值。
正解代码
cpp
#include<bits/stdc++.h>
#define pii pair<int,int>
#define int long long // 开long long防止路径长度爆int
using namespace std;
const int N=1e5+9, inf=1e18;
int t,n,m,k,ans,x;
signed main(){
cin>>t;
while(t--){
cin>>n>>m;
vector<vector<pii>>g(n+1);
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
g[b].push_back({a,c}); // 无向边,双向加边
}
cin>>k;
priority_queue<pii,vector<pii>,greater<>>q; // 小顶堆
vector<int>src(n+1,-1),d(n+1,inf);
vector<bool>st(n+1);
ans=inf+9;
for(int i=0;i<k;i++){
cin>>x;
d[x]=0; // 源点到自身距离为0
src[x]=x; // 源点的归属是自己
q.push({0,x});// 所有源点同时入队
}
while(q.size()){
auto [dd,id]=q.top();q.pop();
if(st[id])continue;
st[id]=1; // 标记该节点最短路已确定
for(auto [j,w]:g[id]){
// 跨源边:两端归属源点不同,更新答案
if(src[j]!=-1 && src[id]!=-1 && src[j]!=src[id])
ans=min(ans, w + d[id] + d[j]);
// 标准松弛操作
if(d[j]>d[id]+w){
src[j]=src[id]; // 继承归属源点
d[j]=d[id]+w;
q.push({d[j],j});
}
}
}
cout<<ans<<'\n';
}
return 0;
}
代码详解
- 初始化
- 建图:用邻接表存储无向带权图。
- 距离数组
d[]初始化为无穷大,归属源点数组src[]初始化为-1。 - 所有关键点入队:
d[x] = 0,src[x] = x,加入小顶堆优先队列。
- 多源Dijkstra过程
- 每次取出堆顶距离最小的点
u,若已标记则跳过,否则标记为已确定最短路。 - 遍历
u的所有邻边(v, w):- 跨源更新答案 :若
u和v都有归属源点,且归属不同,则说明两个不同关键点可以通过这条边连通,路径长度为d[u] + w + d[v],用其更新全局最小值ans。 - 标准松弛操作 :若
d[v] > d[u] + w,则更新d[v] = d[u] + w,设置src[v] = src[u],将v加入优先队列。
- 跨源更新答案 :若
- 每次取出堆顶距离最小的点
- 输出答案
遍历结束后,输出全局最小值ans。
正确性证明
对于任意两个关键点a和b,它们的最短路径上必然存在一条边(u,v),使得u的最近关键点是a,v的最近关键点是b。当算法处理这条边时,就会计算出a到b的最短路径长度并更新答案,因此最终能得到正确的最小值。
复杂度分析
- 时间复杂度: O ( m log n ) O(m \log n) O(mlogn),与单次Dijkstra一致,远优于k次Dijkstra的 O ( k m log n ) O(km \log n) O(kmlogn),完美应对k=1e5的情况。
- 空间复杂度:(O(n+m)),存储邻接表与距离、归属数组。