3423 字
9 分钟
牛客寒假算法基础集训营赛后个人总结(下)
2026-02-13
统计加载中...

都给我去看创世纪的超神番超时空辉夜姬

突然发现小标题太多就无法伸缩了,所以又开了新的一篇。

算法基础集训营第四场 2.09#

赛题:https://ac.nowcoder.com/acm/contest/120564 出题人题解(出题人这次没在牛客写,不过有相应的文档):文档链接
这场神了。。。看标题就知道了。不过由于刚开的时候还在外面吃饭,所以给了2小时,后面1616点到家之后才开始打。。整体难度还是有的,基本全是构造。

A - 本场比赛灵感来源于树状数组出题组#

签到题,数据比较小,可以暴力解决,不过要注意 8080% 是除了这个数本身之外的其他数的 8080%。(没注意就WAWA了。)
思路:双循环遍历,用cntcnt记录每次遍历之后满足小于等于axa_x的总数量,循环结束后再判断是否满足入A组的条件,满足就加入sumsum
tipstips:这里在判断 8080% 的时候用了十字相乘的方法规避小数乘法精度的问题。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int a[1010];
void solved(){
int n;
cin >> n;
for (int i = 1; i <= n;i++){
cin >> a[i];
}
int sum = 0;
for (int i = 1; i <= n;i++){
int cnt = 0;
for (int j = 1; j <= n;j++){
if(a[j] <= a[i] && i != j){
cnt += 1;
}
}
if(5 * cnt >= 4 * (n - 1)){
sum += a[i];
}
}
cout << sum;
}
int main(){
solved();
return 0;
}

B - 构造部落#

这题主要是要理顺时间线的关系,取决于你怎么判断首领最后一年的年份(取前还是取后,我是取前了)。
思路:分析完题目你会发现,输出的文物的年份需要根据前任首领的在位年份进行求和,所以我们需要知道每一个首领的前几任的一共在位多少年,也就是前缀和。我这里的想法是把第一年减11再加ss年表示元年,可以保证后面每次用bb数组表示的前缀和都是前一任首领在位的最后一年,方便计算。不过这种方法输出第一位首领的数据要特判一下。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll a[200010];
ll b[200010];
void solved(){
ll n, q;
ll s;
cin >> n >> q >> s;
cin >> a[1];
a[1] = a[1] - 1 + s;
b[1] = b[0] + a[1];
for (ll i = 2; i <= n;i++){
cin >> a[i];
b[i] = b[i - 1] + a[i];
//cout << b[i] << '\n';
}
while(q--){
ll x, y;
cin >> x >> y;
if(x == 1) cout << s - 1 + y << '\n';
else cout << b[x - 1] + y<< '\n';
}
}
int main(){
solved();
return 0;
}

C - 墨提斯的排列#

依旧神奇构造。。后来查资料的时候发现居然是格雷码,一种单步变换的二进制码。
思路:我们想要让每相邻的两个数异或值最小,就让它们之间不同的位数最小(只有一位不相同),我们这里可以手动推导演示一下。
构造:0,10,100,0100,01 相差一位,和为11
0,1,3,20,1,3,2: 00,01,11,1000,01,11,10 每相邻的一位都是相差一位,和为 1+2+1=41 + 2 + 1 = 4
0,1,3,2,6,7,5,40,1,3,2,6,7,5,4000,001,011,010,110,111,101,100000,001,011,010,110,111,101,100 和为 1×4+2×2+4×1=121 \times 4 + 2 \times 2 + 4 \times 1 = 12
这里基本上就可以看出规律了,然后可以通过 i(i>>1)i \bigoplus (i >> 1) 来构造符合条件的排列。(也是格雷码的转换方式)通过错位异或的方式构造出只有一位不相同的排列。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
int n;
cin >> n;
for(int i = 0; i < (1 << n); i++){
cout << (i ^ (i >> 1)) << " ";
}
cout << '\n';
}
int main(){
solved();
return 0;
}

G - 真白的幻觉#

这题场上的时候没啥时间想了。。不过应该也是有规律的构造,所以赛后稍微补了一下。
思路:题目很好理解,就是在101810^{18}内找一个能让g(a)+g(b)g(a)+g(b)最大的值(f(x)f(x)执行次数最大),那么我们可以发现数字中有0055都是不合适的,因为这会快速导致数位出现0011 的话不会影响最终结果。而像 2,3,4,6,7,8,92,3,4,6,7,8,9 这些数则相对持久,在22的倍数中我们可以尽量选择 8833尽量选择 99,如果数位不足以构造8899,就向下构造2,42,4或者3,63,6。最终找出数位最大的那两个就可以了。(题目要求不相等)
可以暴力枚举,也可以自己造。

NOTE

277777788888899277777788888899是文献中著名的最小的持久性1111的数,2777778999999999927777789999999999 是另一个在 101810^{18} 内的等价构造,在 101810^{18} 范围内能达到最大“乘数持久性”,g(a)=g(b)=11g(a) = g(b)=11,使得 g(a)+g(b)=22g(a)+g(b)=22 达到最大。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
cout << "27777789999999999 277777788888899";
}
int main(){
solved();
return 0;
}

I - 初华的扭蛋机#

这纯高中数学吧,数学期望这一块。。考验高中数学还记得多少的雷了。
思路:我们首先来分析颜色出现的概率,每个颜色出现的概率为 16\frac{1}{6},然后有33次独立抽取,设某种颜色出现的概率为PkP_k,则有 Pk=C3k(16)k(56)3kP_k = C_3^k (\frac{1}{6})^k (\frac{5}{6})^{3-k} (这下看懂了)。我们可以算出对应kk的概率。
P1=75216P_1 = \frac{75}{216}P2=15216P_2 = \frac{15}{216}P3=1216P_3 = \frac{1}{216}
然后我们可以算出数学期望。
Ex=2xP1+3xP2+10xP3=x275+315+101216=205216xE_x = 2xP_1 + 3xP_2 + 10xP_3 = x \frac{2 \cdot 75 + 3 \cdot 15 + 10 \cdot 1}{216} = \frac{205}{216} x
也就是每次在一个颜色下注后的期望是 205216<1\frac{205}{216} < 1,所以最好的选择就是66次都保留原来的筹码 6>205366 > \frac{205}{36}

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
cout << "######" << endl;
}
int main(){
solved();
return 0;
}

算法基础集训营第五场 2.11#

赛题:https://ac.nowcoder.com/acm/contest/120565 出题人题解:题解链接
这场的基础题目就比较偏简单模拟和模板一点,简单的挺简单,有一些难度的就可能需要学过类似的解法才好做。。

B - 智乃的瓷砖#

这题是经典的生成字符矩阵,我觉得算是基础编程能力。
思路:直接根据行列和列的奇偶性分成四种情况,打表输出,这里要注意输出要用多用一个\来转义。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
int n, m;
cin >> n >> m;
for (int i = 1; i <= n;i++){
for (int j = 1; j <= m; j++){
if(i % 2 == 1 && j % 2 == 1){
cout << '/';
}
else if(i % 2 == 1 && j % 2 == 0){
cout << '\\';
}
else if(i % 2 == 0 && j % 2 == 1){
cout << '\\';
}
else if(i % 2 == 0 && j % 2 == 0){
cout << '/';
}
}
cout << '\n';
}
}
int main(){
solved();
return 0;
}

D - 智乃的果子#

这题其实是哈夫曼树(最小生成树)的一个构建原理(可以了解一下),用优先队列直接模拟过程,但是会超时。。所以需要优化。
思路:想要最终的代价最小,其实就是要在每一次合并操作之后再找出最小的两个堆继续合并,这样不断操作到只剩最后一个堆时的代价就是最小的了。不过这题就阴在相同权重的果子并不唯一,最终一个一个模拟过程就会超时,我们需要对重复的堆进行合并。也就是当目前权重最小的堆的cc数量大于11时,我们就取整半 [p2][\frac{p}{2}] 来合并,[p2]×2w[\frac{p}{2}] \times 2 w 加入总代价,然后加入新的权值为2w2w的新堆。如果cc等于1,我们就按正常流程将它与次小权值的堆合并,生成新堆,cc00就删除该堆,一直操作即可。
这里我是用mapmap自动排序和迭代器实现了优先队列(因为mapmap方便映射对应同权值的堆的数量)。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define mod 1000000007LL
void solved(){
int n;
cin >> n;
map<ll, ll> mp;
for (int i = 1; i <= n;i++){
ll c, w;
cin >> c >> w;
mp[w] += c;
}
ll ans = 0;
while(mp.size() > 1 || (mp.size() == 1 && mp.begin()->second > 1)){
auto it = mp.begin();
ll w1 = it->first;
ll c1 = it->second;
if(c1 >= 2){
ll p = c1 / 2;
ans = (ans + (p % mod) * ((2 * w1) % mod) % mod) % mod;
ll w2 = w1 * 2;
ll d = p;
c1 %= 2;
if(c1 == 0){
mp.erase(it);
}
else{
it->second = c1;
}
mp[w2] += d;
}
else{
auto it2 = next(it);
ll w2 = it2->first;
ll c2 = it2->second;
ans = (ans + (w1 % mod) + (w2 % mod)) % mod;
ll w3 = w1 + w2;
mp.erase(it);
c2--;
if(c2 == 0){
mp.erase(it2);
}
else{
it2->second = c2;
}
mp[w3]++;
}
}
cout << ans << '\n';
}
int main(){
solved();
return 0;
}

F - 智乃的算法竞赛群友#

这题有点逆向思维内味了,通过反向枚举来将判断所有的情况,然后取出最优的(奈何场上的时候根本想不到)。
思路:这题其实子串所有可能出现的情况只有qcjjkktqcjjkkttdtdqcjjkktdqcjjkktd一共三种情况,而最先的想法肯定是尽可能地取qcjjkktdqcjjkktd,八个字符串双倍收益,也是贪心。而对于剩下不能再组成88个字符的则枚举全为77或者全为22的情况,取其中一种快乐值较高的。而除此之外还有两种情况,一种是44tdtd11qcjjkktdqcjjkktd高收益,另一种是88qcjjkktqcjjkkt77qcjjkktdqcjjkktd高收益,都进行一次反向枚举判断即可。
一共四种枚举情况,不过ii的最大值在这题中刚好取88都能过(亲测),一般来说还是尽可能往大的取,不超时就行。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
int t;
cin >> t;
while(t--){
ll n, a, b;
cin >> n >> a >> b;
ll ans = 0;
for (int i = 0; i <= 60 &&i * 7 <= n;i++){
ans = max(ans, i * a + (n - i * 7) / 8 * (a + b));
}
for (int i = 0; i <= 60 &&i * 2 <= n;i++){
ans = max(ans, i * b + (n - i * 2) / 8 * (a + b));
}
for (int i = 0; i <= 60 &&i * 8 <= n;i++){
ans = max(ans, i * (a + b) + (n - i * 8) / 7 * a);
}
for (int i = 0; i <= 60 &&i * 8 <= n;i++){
ans = max(ans, i * (a + b) + (n - i * 8) / 2 * b);
}
cout << ans << '\n';
}
}
int main(){
solved();
return 0;
}

G - 智乃的箭头魔术#

这题就纯粹的模拟,至少我是选择把全部情况列出来然后判断的。。
思路:根据题意描述,最初是状态是 "0""0",然后可以通过66种操作改变状态,让我们输出每一次改变后这个玩具的状态。其实实际上就是去根据每一种情况来判断与继承状态,我们可以把 4×6=244 \times 6 = 24 种情况全部列出然后对每个字符串进行判断即可。
这里可能需要一点空间想象能力,如果感觉烧脑的话就自己做一张简单的小卡片来玩一下就好了。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
char t = '0';
string s = "0112233445142015320125410214530214510214102302142025101203201451451522302514203214510021454101002532";
for(char c:s){
if(t == '0' && c == '0'){t = '3';cout << t;}
else if(t == '0' && c == '1'){t = '0';cout << t;}
else if(t == '0' && c == '2'){t = '1';cout << t;}
else if(t == '0' && c == '3'){t = '2';cout << t;}
else if(t == '0' && c == '4'){t = '1';cout << t;}
else if(t == '0' && c == '5'){t = '3';cout << t;}
else if(t == '1' && c == '0'){t = '2';cout << t;}
else if(t == '1' && c == '1'){t = '3';cout << t;}
else if(t == '1' && c == '2'){t = '0';cout << t;}
else if(t == '1' && c == '3'){t = '1';cout << t;}
else if(t == '1' && c == '4'){t = '2';cout << t;}
else if(t == '1' && c == '5'){t = '0';cout << t;}
else if(t == '2' && c == '0'){t = '1';cout << t;}
else if(t == '2' && c == '1'){t = '2';cout << t;}
else if(t == '2' && c == '2'){t = '3';cout << t;}
else if(t == '2' && c == '3'){t = '0';cout << t;}
else if(t == '2' && c == '4'){t = '3';cout << t;}
else if(t == '2' && c == '5'){t = '1';cout << t;}
else if(t == '3' && c == '0'){t = '0';cout << t;}
else if(t == '3' && c == '1'){t = '1';cout << t;}
else if(t == '3' && c == '2'){t = '2';cout << t;}
else if(t == '3' && c == '3'){t = '3';cout << t;}
else if(t == '3' && c == '4'){t = '0';cout << t;}
else if(t == '3' && c == '5'){t = '2';cout << t;}
}
}
int main(){
solved();
return 0;
}

J - 智乃的幻方#

依旧简单模拟,直接判断。
思路:直接根据题意判断所有条件是否符合,注意不要漏掉 090-9 是否只出现一次这个条件。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int a[4][4];
map<int, int> mp;
void solved(){
for (int i = 1; i <= 3;i++){
for (int j = 1; j <= 3;j++){
cin >> a[i][j];
mp[a[i][j]]++;
}
}
for(auto&it :mp){
if(it.second > 1){
cout << "No";
return;
}
}
int sum1 = a[1][1] + a[1][2] + a[1][3];
int sum2 = a[2][1] + a[2][2] + a[2][3];
int sum3 = a[3][1] + a[3][2] + a[3][3];
int sum4 = a[1][1] + a[2][1] + a[3][1];
int sum5 = a[1][2] + a[2][2] + a[3][2];
int sum6 = a[1][3] + a[2][3] + a[3][3];
int sum7 = a[1][1] + a[2][2] + a[3][3];
int sum8 = a[1][3] + a[2][2] + a[3][1];
if(sum1 == sum2 && sum2 == sum3 && sum3 == sum4 && sum4 == sum5 && sum5 == sum6 && sum6 == sum7 && sum7 == sum8){
cout << "Yes";
}
else{
cout << "No";
}
}
int main(){
solved();
return 0;
}

算法基础集训营第六场 2.13#

赛题:https://ac.nowcoder.com/acm/contest/120566
出题人题解(这场也是只有文档):文档链接
难难难。。第二道题就已经开始二分和简单的dpdp了,后面更是烟斗不带烟,做不了一点。。

A - 小L的三角尺#

这题看出来是贪心,不过没推出来规律,思路是错的。。整体也比较难,后续简单补了一下这题。
思路:我们可以看看每次打磨之后斜边的减少情况是怎么样的,这里可以数学推导一下:
设剩余长度为rr,初始时,r=yir = y_i
Δ=xi2+r2xi2+(r1)2\Delta = \sqrt{x^2_i + r^2} - \sqrt{x^2_i + (r - 1) ^ 2}
我们可以发现随着rr减少,Δ\Delta 严格递减,也就是磨得越多,收益越小。那么我们就可以知道,想要每次磨得收益最大化,就要将每次斜边最大的尺子磨掉一个单位,最终的收益就可以最大化,这才是真正要贪心的地方。我们可以用优先队列模拟这个过程。
实现:这里由于数据非常多且杂乱,所以用了一个结构体来记录对应尺子的所有数据并计算,tt代表磨掉的单位长度,内置gg函数代表计算对应的收益。(方便每一步的计算)然后我们使用了二元组记录收益和对应尺子的下标,lambdalambda定义比较器来按收益排列的优先队列,以方便我们的操作。
第一次操作的时候,我们把每一个yy不为00的尺子都计算收益然后加入队列中。然后我们开始循环执行打磨操作,用pp记录操作的次数,当达到ww或者队列为空的时候停止循环。每一次循环我们都取出堆顶的元素进行操作,如果收益为00就终止,否则就定位到相应的rr数组下标的元素,对内置tt加一(打磨一个单位),记录斜边总长的sumsum减去这个收益,然后计算新的收益g1g1存入这个数组元素,重新入堆(队列)排列。

NOTE

auto cmp = [](const pair<double, int> &p1, const pair<double, int> &p2){return p1.first < p2.first;};是一种LambdaLambda的表达式来定义比较器,指定了p2的优先级高于p1,所以在进入队列之后,p2就会排在p1前面。
decltype(cmp)由于cmpLambdaLambda,所以需要decltype获取其比较器类型。
pq(cmp)构造函数,将cmp传入队列,让其使用该比较器。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct Ruler{
ll x, y;
int t;
double g() const{
ll r = y - t;
if(r == 0) return 0.0;
double a = sqrt(x * x + r * r);
double b = sqrt(x * x + (r - 1) * (r - 1));
return a - b;
}
};
void solved(){
ll n, w;
cin >> n >> w;
vector<Ruler> r(n);
double sum = 0.0;
auto cmp = [](const pair<double, int> &p1, const pair<double, int> &p2)
{
return p1.first < p2.first;
};
priority_queue < pair<double, int>, vector<pair<double, int>>, decltype(cmp)> pq(cmp);
for (int i = 0; i < n;i++){
ll x, y;
cin >> x >> y;
r[i] = {x, y, 0};
sum += sqrt(x * x + y * y);
if(y > 0){
double g = r[i].g();
pq.emplace(g, i);
}
}
int p = 0;
while(p < w && !pq.empty()){
auto [g, x] = pq.top();
pq.pop();
if(g <= 0) break;
auto &r1 = r[x];
r1.t++;
sum -= g;
p++;
if(r1.y - r1.t > 0){
double g1 = r1.g();
pq.emplace(g1, x);
}
}
printf("%.10f", sum);
}
int main(){
solved();
return 0;
}

G - 小L的散步#

这题很好理解,我们每次都对小LL附近的石块进行判断是否踩中缝隙就好了。
思路:我们首先要用前缀和记录每块石块距离起点的长度,方便我们每次根据对应走出的总距离判断是否踩中缝隙。然后我们要找到离小LL最近的裂缝进行判断是否踩到。这里的查询我使用了二分查找(lower_boundlower\_bound函数),在bb数组中找到第一个比总步数ansans大的数,然后判断是否踩中了缝隙。
这里注意要特判是否还没开始走就踩缝隙了,因为后续的判断都是走了一步的。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll a[200010];
ll b[200010];
void solved(){
ll n, m, l;
cin >> n >> m >> l;
ll ans = 0;
for (int i = 1; i <= n;i++){
cin >> a[i];
b[i] = b[i - 1] + a[i];
}
if(b[1] < ans + l){
cout << "YES" << '\n';
return;
}
for (int i = 1; i <= m;i++){
ll x;
cin >> x;
ans += x;
auto it = lower_bound(b + 1, b + 1 + n, ans + 1);
if(it != b + n + 1 && *it < ans + l){
cout << "YES" << '\n';
return;
}
}
cout << "NO" << '\n';
}
int main(){
solved();
return 0;
}

H - 小L的数组#

得亏于数据只开到20472047,如果你意识到事实上结果也就最大取到20472047那就好做了。
思路:因为两个数组最大的数只能取到20472047,而无论是减法操作还是两数异或都不会让这个数更大了(不会超过20472047),那么你就只需要记录在[0,2047][0,2047]这个范围内,哪些数是取过的,然后遍历找出最大的就行了。我这里开辟了两个boolbool数组ccdd用来记录这种状态。然后用嵌套循环分别对AABB数组中每一个能取到的数进行记录,而取到的数在新的内层循环可以继续取新的数(其实就是dpdp),由此可以把所有可能的结果都记录到cc中。
这里提一下:只用cc数组更新状态会导致本来只能执行一次的操作内执行了多次(反复增加新状态),所以要把下次要变换的状态用dd数组先储存,等这一轮的所有可能操作结束后再更新到cc数组里面。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int a[2055];
int b[2055];
vector<bool> c(2055,0);
void solved(){
int n;
cin >> n;
for (int i = 0; i < n;i++){
cin >> a[i];
}
for (int i = 0; i < n;i++){
cin >> b[i];
}
c[0] = 1;
for (int i = 0; i < n;i++){
vector<bool> d(2055,0);
for (int j = 0; j < 2048;j++){
if(c[j] == 0){
continue;
}
if(j - a[i] >= 0){
d[j - a[i]] = 1;
}
else{
d[0] = 1;
}
}
for (int j = 0; j < 2048;j++){
if(c[j] > 0){
d[j ^ b[i]] = 1;
}
}
c.swap(d);
}
int ans = 0;
for (int i = 2047; i >= 0;i--){
if(c[i] > 0){
ans = i;
break;
}
}
cout << ans << '\n';
}
int main(){
solved();
return 0;
}

K - 小L的游戏1#

签到题,不过刚开始打没睡醒。用了whilewhile嵌套去模拟,结果超时了。。
思路:对zz取模(m+n)(m + n),然后判断取模后的数落在哪个区间。在 (1,m](1,m] 区间内小LL最后一手,输出00,在 {1}(m,m+n)\lbrace 1 \rbrace \cup (m,m+n) 中,fallleaves01fallleaves01 是最后一手,输出 11

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solved(){
int t;
cin >> t;
while(t--){
ll x = 0;
ll i = 1;
ll m, n, z;
cin >> m >> n >> z;
ll x1 = z % (m + n);
if((x1 > m && x1 < m + n) || x1 == 0){
cout << 1;
}
else if(x1 <= m && x1 > 0){
cout << 0;
}
}
}
int main(){
solved();
return 0;
}

后记#

随着最后一场比赛结束,寒假的算法学习就暂告一段落了。至少后续过年这段时间不会再学了。very painful , brovery\ painful\ ,\ bro。不过新年期间应该还会写篇有关拜年祭的杂谈来练一下笔。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

牛客寒假算法基础集训营赛后个人总结(下)
https://mkrari.cn/posts/nc_sum2/
作者
Mkrari
发布于
2026-02-13
许可协议
CC BY-NC-SA 4.0