题解 | 湖南工业大学蓝桥杯省赛选拔赛
update 2025/3/3 G题已更新
题解链接:https://blog.nowcoder.net/n/47b9455b10a44284b7d9c7b9a42e13f3
比赛链接:https://ac.nowcoder.com/acm/contest/102926
密码:lqbhut31
A 603
概要
链接:https://ac.nowcoder.com/acm/contest/102926/A
估测难度:1600
算法:线性动态规划
时间复杂度:
题意
给一个纯数字字符串,其中不包含"603"子串的子序列有多少个?
思路
第一时间可能会联想到组合数学,通过组合数去处理。
然后就会发现越想越复杂,需要同时维护若干种拼接。虽然这种方法也能在O(n)下解决问题,但是思考难度和代码实现过于复杂。
所以我们使用动态规划解决问题。
对于拼接一个已知的字符串(如:"603","石碑文")等,dp是一个经典的方法。
设计状态:dp[1]存以6结尾的串的数量,dp[1]存以60结尾的串的数量,dp[0]存除前两者以外,并不以"603"结尾的所有串的数量
状态转移见代码注释
代码
using namespace std;
using i64 = long long;
constexpr int M=998244353;
void solve() {
int n; cin>>n;
string s; cin>>s;
vector<i64> dp(3); dp[0] = 1;//初始时有一个空串
for(int i=0; i<n; ++i) {
//拷贝一个 dp 数组名为 f
auto f = dp;
//将s[i]加入到当前串尾部, 枚举f[0], f[1], f[2]三个状态依次转移
if(s[i] == '6') {
dp[1] += f[0];
dp[1] += f[1];
dp[1] += f[2];
} else if(s[i] == '0') {
dp[0] += f[0];
dp[2] += f[1];
dp[0] += f[2];
} else if(s[i] == '3') {
dp[0] += f[0];
dp[0] += f[1];
//dp[0] += f[2];此状态无法转移,否则会构成“603”
} else {
dp[0] += f[0];
dp[0] += f[1];
dp[0] += f[2];
}
dp[0] %= M;
dp[1] %= M;
dp[2] %= M;
}
//最后需要将全不选的情况(即空串)除去, -1
cout<<((dp[0]+dp[1])%M+dp[2]-1+M)%M<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
B 打打打打结
概要
链接:https://ac.nowcoder.com/acm/contest/102926/B
估测难度:1200
算法:二分答案
时间复杂度: 2
题意
给 n 条线段,在每条线段上选一个点,最大化相邻点之间的最短距离。
思路
看到最大最小一眼二分就完了, 直接干。
排序之后直接选最左边线段的左端点作为第一个。
然后now依次+mid,前提是 a[i].l <= now+mid <= a[i].r;
如果a[i].l > now + mid, now = a[i].l;
如果a[i].r < now + mid, 直接return false
代码
using namespace std;
using i64 = long long;
void solve() {
int n; cin>>n;
vector<pair<int,int>> a(n+1);
for(int i=1; i<=n; ++i) {
int x, len; cin>>x>>len;
a[i] = {x, x+len};
}
sort(a.begin()+1, a.end());
auto check = [&](int x) {
int now = a[1].first;
for(int i=2; i<=n; ++i) {
if(a[i].first <= now+x and now+x <= a[i].second) {
now += x;
} else if(a[i].first > now+x) {
now = a[i].first;
} else {
return false;
}
} return true;
};
i64 l = 0, r = 1e9;
while(r-l > 1) {
int mid = l+r >> 1;
if(check(mid)) l = mid;
else r = mid;
}
cout<<l<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
C 邮票
概要
链接:https://ac.nowcoder.com/acm/contest/102926/C
估测难度:1400
算法:概率论(数学)
时间复杂度:
题意
有 n-1 种普通邮票(同种凑齐两张获胜)和 1 张特殊邮票(拿到立刻获胜), 每次等概率抽取邮票,期望抽取次数是多少。
思路
状态转移:当手里已经有一张该种普通邮票时,该种普通邮票也可以视为特殊邮票。
随着拿的次数越来越多,若此时还未获胜,特殊邮票会越来越多,每拿一次之后都增加一种特殊邮票,
所以第 次拿的胜率是
。
因此,我们只需要枚举拿的次数 ,拿第
次的概率就是失败率的前缀积 p(初始为1.0, 显然没拿的时候100%失败), ans = ∑ i * p;
代码
using namespace std;
using i64 = long long;
void solve() {
double n; cin>>n;
double ans = 0, p = 1;//p为拿第i次的概率
for(int i=1; i<=n; ++i) {
ans += 1.0 * p;
p = p * (n-i) / n;//失败率的前缀积
}
cout<<fixed<<setprecision(2)<<ans<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
D 遗迹探险
概要
链接:https://ac.nowcoder.com/acm/contest/102926/D
估测难度:1300
算法:线性动态规划
时间复杂度:
题意
从左上角走到右下角,只能向右或向下走,找到一条路径——其中每个节点的前缀和都是非负数,输出这样的路径中终点权值最大的。如果没有,输出-1
思路
对于每个位置只能从上放或者左方转移过来,直接两层循环枚举
状态转移 f[i][j] = max(f[i][j-1], f[i-1][j]) + a[i][j];
当f[i][j] < 0 时,标记为-无穷,禁止通过。
代码
using namespace std;
using i64 = long long;
constexpr i64 INF=1e18;
void solve() {
int n, m; cin>>n>>m;
vector a(n+1, vector<i64>(m+1));
for(int i=1; i<=n; ++i) {
for(int j=1; j<=m; ++j) {
cin>>a[i][j];
}
}
vector f(n+1, vector<i64>(m+1, -INF)); f[1][1] = a[1][1];//初始化
for(int i=1; i<=n; ++i) {
for(int j=1; j<=m; ++j) {
if(i > 1) f[i][j] = max(f[i][j], f[i-1][j] + a[i][j]);
if(j > 1) f[i][j] = max(f[i][j], f[i][j-1] + a[i][j]);
if(f[i][j] < 0) f[i][j] = -INF;//无法经过,标记为-无穷
}
}
cout<<(f[n][m]>0?f[n][m]:-1)<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
E maximum matrix
概要
链接:https://ac.nowcoder.com/acm/contest/102926/E
估测难度:1400
算法:二分答案,前缀和
时间复杂度: 2
题意
给一个仅有0/1组成的矩阵,在里面找到最大的 全0方阵 和 全1方阵,分别输出面积
思路
对原矩阵做二维前缀和。
二分最大方阵的边 L,暴力枚举所有位置,如果该方阵前缀和 == L * L * t,有合法解return true。
代码
using namespace std;
using i64 = long long;
void solve() {
int n, m; cin>>n>>m;
vector s(n+1, vector<i64>(m+1));
for(int i=1; i<=n; ++i) {
for(int j=1; j<=m; ++j) {
cin>>s[i][j];
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + s[i][j];
}
}
auto check = [&](int x, int t) {
for(int i=x; i<=n; ++i) {
for(int j=x; j<=m; ++j) {
if(s[i][j] - s[i-x][j] - s[i][j-x] + s[i-x][j-x] == x*x*t) {
return true;
}
}
} return false;
};
auto Find = [&](int t) {
int l = 0, r = min(n, m)+1;
while(r-l > 1) {
int mid = l+r >> 1;
if(check(mid, t)) l = mid;
else r = mid;
}
return 1ll*l*l;
};
cout<<Find(0)<<" ";
cout<<Find(1)<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
F 盐都不盐了
概要
链接:https://ac.nowcoder.com/acm/contest/102926/F
估测难度:900
知识点:组合数学数学 快速幂数学
https://oi-wiki.org/math/binary-exponentiation/
时间复杂度:
题意
给一堆没用的字符串拼接在一起,里面分别有多少个子序列是 "6", "66", "666"
思路
统计6的个数 cnt,输出C(1, cnt), C(2, cnt), C(3, cnt)
注意当数字取模后,不能使用除法,所以需要乘上逆元。
如 15 % 7 / 3 != 15 / 3 % 7
代码
using namespace std;
using i64 = long long;
constexpr int M=998244353;
i64 qpow(i64 a, i64 b) {
i64 ans = 1;
while(b) {
if(b & 1) ans = ans*a%M;
a = a*a%M, b>>=1;
} return ans;
}
void solve() {
int n; cin>>n;
i64 cnt = 0;
for(int i=1; i<=n; ++i) {
i64 k; cin>>k;
string s; cin>>s;
i64 res = 0;
for(auto c: s) {
res += (c == '6');
}
cnt += 1ll * res * k;
}
i64 a = cnt%M;
i64 b = cnt * (cnt-1)%M * qpow(2, M-2)%M;
i64 c = cnt * (cnt-1)%M * (cnt-2)%M * qpow(6, M-2)%M;
cout<<a<<" "<<b<<" "<<c<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
G 气象工程
概要
链接:https://ac.nowcoder.com/acm/contest/102926/G
估测难度:2400
算法:主席树
时间复杂度: 2
题意
给出 块木板(区间)和它的耐久,接下来发射 m 颗子弹,被击中的木板每次扣 1 点耐久,扣至0点会破碎,输出每次发射子弹后破碎的木板数
思路
不是哥们,盐豆不盐了,真以为我会啊T_T
诶嘿,我还真会。
前置芝士:主席树,可以在 2
的时空复杂度保存单点修改的
所有历史数组
可以在2
查询任意两个历史间数组内的第k小元素。(很高级.jpg)
首先考虑到求解第 i 块木板在什么时候破碎。
容易想到一个22
的做法:
主席树保存所有历史数组,枚举 块木板,二分破碎的时间,从复杂度上来看是一个很极限的做法,实际上因为主席树常数过大,并且题目只给了
,很无情的被卡常了。
考虑怎么优化,前面的算法只用到了主席树的存储功能,并没有非常好的使用到主席树的查询,从查询算法入手。
对于第 块木板,所有击中他的子弹中的时间是绝对有序的,我们可以在所以击中木板的子弹中查询第
小时间,就是第一颗击碎木板的子弹。
对于主席树的插入,我们从以发射位置为修改点,逐一保存每个发射位置发射之后的数组,利用主席树在任意两个历史数组间的查询功能,完成对区间 的 第
小查询。不再使用二分算法逼近,时间复杂度优化到
2
代码
using namespace std;
using i64 = long long;
struct PST {
struct SMN {
int l, r;
int sum;
};
int n, cnt; vector<SMN> tr;
PST() {}
PST(int n) {init(n);}
void init(int n) {
this->n = n;
tr.assign(n<<5|1, {});
cnt = 0;
}
inline int build(int l, int r) {
//初始化
int u = ++cnt;
tr[u].sum = 0;
if(l==r) return u;
int mid = l+r >> 1;
tr[u].l = build(l, mid);
tr[u].r = build(mid+1, r);
return u;
}
inline int insert(int l, int r, int old, int pos) {
int u = ++cnt;
tr[u] = tr[old]; tr[u].sum++;
if(l==r) return u;
int mid = l+r >> 1;
if(pos<=mid) tr[u].l = insert(l, mid, tr[u].l, pos);
else tr[u].r = insert(mid+1, r, tr[u].r, pos);
return u;
}
inline int query(int l, int r, int u, int v, int k) {
if(l==r) return l;
int mid = l+r >> 1;
int x = tr[tr[v].l].sum - tr[tr[u].l].sum;
if(k<=x) return query(l, mid, tr[u].l, tr[v].l, k);
return query(mid+1, r, tr[u].r, tr[v].r, k-x);
}
};
void solve() {
int n, m; cin>>n>>m;
vector<array<int,3>> a(n+1);
for(int i=1; i<=n; ++i) {
int l, r, k; cin>>l>>r>>k;
a[i] = {l, r, k};
}
vector<array<int,2>> q(m+1);
for(int i=1; i<=m; ++i) {
int x; cin>>x;
q[i] = {x, i};
}
sort(q.begin()+1, q.end());
PST X(200000);
vector<int> t(200001); t[0] = X.build(1, 200000);
for(int i=1,j=1; i<=200000; ++i) {
t[i] = t[i-1];
while(q[j][0] == i and j<=m) {
t[i] = X.insert(1, m, t[i], q[j][1]);
++j;
}
}
vector<int> ans(m+1);
for(int i=1; i<=n; ++i) {
auto [l, r, k] = a[i];
if(k > X.tr[t[r]].sum - X.tr[t[l-1]].sum) continue;
ans[X.query(1, m, t[l-1], t[r], k)]++;
}
for(int i=1; i<=m; ++i) {
cout<<ans[i]<<"\n";
}
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}
H 查找单词
概要
链接:https://ac.nowcoder.com/acm/contest/102926/H
估测难度:800
知识点: getline
时间复杂度: O(n)
题意
给出一行文本和模式串,文本里能找到多少个模式串。
思路
暴力枚举文本每一个位置,匹配模式串
注意需要读取空格所以使用读行函数
代码
using namespace std;
using i64 = long long;
void solve() {
string s; getline(cin, s);
string t; getline(cin, t);
int cnt = 0;
for(int i=0; i<s.size(); ++i) {
if(s[i] == t[0]) {
int k = i, j = 0, f = 1;
for( ;j<t.size() and k<s.size(); ++j,++k) {
if(s[k] != t[j]) f = 0;
}
if(j != t.size()) f = 0;
cnt += f;
}
}
cout<<cnt<<"\n";
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t=1;// cin>>t;
while (t--) solve();
return 0;
}