2025牛客寒假算法基础集训营1补题笔记
题目难度顺序大致为:A D B G M H E J C F L K I
头疼的思维+模拟。
前 \(4\) 题写得挺顺,但 \(D\) 题没看清是两种元素出现次数相同wa了一发,\(M\) 题其实一开始没有思路但暴力写了一波奇迹的过了,赛后果然被hack数据太水,\(H\) 卡了4个钟。。。\(E\) 题明显的贪心结论没有套上,歪到平均数去了,然后就一直在 \(H\) 钻牛角尖。
A.茕茕孑立之影
题意
给定一个数组,找到一个正整数 \(x\),使得 \(x\) 和数组中的元素互不为倍数关系。
思路
- 首先 \(1\) 是任何数的因数,所以有 \(1\) 的时候没有答案。
- 然后考虑没有 \(1\) 的情况,可以发现只需要找到一个比数组中的元素都大的质数就可以,因为数组元素都不超过 \(10^9\) ,直接输出 \(1000000007\) 即可。
代码
点击查看代码
#include <iostream> using namespace std; const int P = 1e9 + 7; int n; void solve() { bool flag = 0; cin >> n; for (int i = 1; i <= n; i ++) { int x; cin >> x; if (x == 1) flag = 1; } if (flag) cout << -1 << '\n'; else cout << P << '\n'; } int main() { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t --) solve(); return 0; }
D.双生双宿之决
题意
给定一个数组,判断是否为双生数组,即元素种类数为 \(2\)、且出现次数相同。
思路
按题意模拟即可。用 \(set\) 来筛选种类个数,用 \(map\) 来记录每个数出现次数。
也可以排序,检查前半部分和后半部分数是否相等即可。
代码
点击查看代码
#include <iostream> #include <algorithm> #include <map> #include <set> #define si(x) int(x.size()) #define fi first #define se second using namespace std; int n; void solve() { cin >> n; set<int> v; map<int, int> mp; for (int i = 0; i < n; i ++) { int x; cin >> x; mp[x] ++; v.insert(x); } if (n % 2 || si(v) != 2) cout << "No" << '\n'; else { int num = 0; for (auto it : mp) { if (num == 0) num = it.se; else if (num != it.se) { cout << "No" << '\n'; return ; } } cout << "Yes" << '\n'; } } int main() { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t --) solve(); return 0; }
代码2
点击查看代码
void solve() { cin >> n; for (int i = 1; i <= n; i ++) cin >> a[i]; sort(a + 1, a + 1 + n); if (n % 2 || a[1] == a[n]) return void(cout << "No" << '\n'); if (a[1] == a[n / 2] && a[n / 2 + 1] == a[n]) cout << "Yes" << '\n'; else cout << "No" << '\n'; }
B.一气贯通之刃
题意
给一棵树,找到一条路径经过所有节点。
思路
自己手动画几棵树可以发现:如果一棵树的某个节点出度超过 \(2\) ,即这个节点与至少 \(3\) 个节点有连边,那么就不存在有简单路径是经过所有节点的,所以我们只需要去遍历一遍所有节点的出度就可以了。而起点、终点,则明显是两个叶子节点,出度为 \(1\)。
题外知识:一颗树的最长简单路径就是这棵树的直径。可以用树形 \(dp\) 来解决。
代码
点击查看代码
#include <iostream> using namespace std; const int N = 1e6 + 10; int n, u, v; int a[N]; void solve() { cin >> n; for (int i = 1; i < n; i ++) { cin >> u >> v; a[u] ++, a[v] ++; } int sd = -1, ed = -1; for (int i = 1 ; i <= n; i ++) { if (a[i] > 2) return void(cout << -1 << '\n'); if (a[i] == 1) if (sd == -1) sd = i; else ed = i; } cout << sd << ' ' << ed; } int main() { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); int t = 1; while (t --) solve(); return 0; }
G.井然有序之衡
题意
给一个数组,每次操作可以使一个元素加 \(1\),另一个元素减 \(1\) ,问变成排列的最小操作次数。
思路
首先,一个元素加 \(1\), 一个元素减 \(1\),对于数组总和是不变,所以数组是否可以构造成排列,在于数组总和和排列总和是否相等。然后是计算最小操作数。
贪心的方法解决最小操作数。
将数组进行升序排序,然后按 \(1 \sim n\) 的排列顺序计算操作个数。
代码
点击查看代码
#include <iostream> #include <algorithm> #define ll long long using namespace std; const int N = 1e6 + 10; ll n; ll a[N]; void solve() { cin >> n; ll sum = 0; for (int i = 1; i <= n; i ++) { cin >> a[i]; sum += a[i]; } ll num = (n + 1) * n / 2; if (num != sum) cout << -1; else { sort(a + 1, a + 1 + n); ll res = 0; for (int i = 1; i <= n; i ++) res += abs(i - a[i]); cout << res / 2; } } int main() { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); int t = 1; while (t --) solve(); return 0; }
M.数值膨胀之美
题意
给定一个数组,可以选择一个区间将所有元素乘 \(2\),问操作后的最小极差。
思路
赛后重新思考,想到可以从第一个最小值开始维护区间,到最后包括所有最小值。
如何维护呢?
-
首先存下所有元素的值和下标,升序排序。
-
然后从第一个最小值下标开始,按区间右端点增大方向操作,到达下一个最小值的位置,区间内的数都要乘2,直到包括所有的最小值后结束。
最后这个思路只过了86.11%,看完题解才知道还要继续考虑次小值直到最大值。
(其实赛时已经发现假设选取所有元素乘2可能比选取子区间要更优,但赛后忘了。。。)
代码
点击查看代码
#include <iostream> #include <algorithm> #define fi first #define se second using namespace std; typedef pair<int, int> PII; const int N = 1e5 + 10; int n, b[N]; PII a[N]; int main() { cin >> n; for (int i = 1; i <= n; i ++) { cin >> b[i]; a[i] = {b[i], i}; } sort(a + 1, a + 1 + n); int res = 0x3f3f3f3f; a[n + 1].fi = res; int maxv = a[n].fi, l = a[1].se, r = a[1].se; for (int i = 1; i <= n; i ++) { while (a[i].se <= l) maxv = max(maxv, b[l --] * 2); while (a[i].se >= r) maxv = max(maxv, b[r ++] * 2); res = min(res, maxv - min(a[1].fi * 2, a[i + 1].fi)); } cout << res; return 0; }
H.井然有序之窗
题意
构造一个排列,满足每个元素都在一个指定的区间内。
思路
一个经典的贪心题吧,居然在这跑dfs,感觉我赛时一定是脑子抽风了。
先说结论:第 \(i\) 个位置如果多个选择,那么选择区间右端点最小的那个数,结果一定不会更劣。
为什么呢?可以自己模拟一下:
假设现在要选择一个数填入第5的位置,有3种选择:3[3, 7]、6[4, 5]、8[5, 6]。
首先我们得知道既然已经到填入第5的位置了,那么 \(1\sim4\) 的位置都已经完成填入了,所以对于这4种选择,可以发现3和6的区间是要更小的:3[5, 7]、6[5, 5]。
所以这个位置如果先选3或8填入,那么6就无法填入了,而如果每个位置的多种选案都选右端点最小的填入,那么对后面的位置影响是最小的。
实现用优先队列来维护右端点的小根堆,枚举 \(1 \sim n\)的位置,将在这个位置下的所有未选区间放入队列中,如果没有或队首的右端点小于当前位置,就没有方案可行。
代码
点击查看代码
#include <iostream> #include <algorithm> #include <queue> using namespace std; const int N = 1e6 + 10; int n; struct node { int val; int l, r; bool operator < (const node& b) const { return r > b.r; } } a[N]; priority_queue<node> pq; int ans[N]; bool cmp(node aa, node bb) { if (aa.l == bb.l) return aa.r < bb.r; return aa.l < bb.l; } void solve() { cin >> n; for (int i = 1; i <= n; i ++) { int l, r; cin >> l >> r; a[i] = {i, l, r}; } sort(a + 1, a + 1 + n, cmp); for (int i = 1, j = 1; i <= n; i ++) { while (j <= n && a[j].l <= i) pq.push(a[j ++]); if (pq.empty() || pq.top().r < i) return void(cout << -1); ans[pq.top().val] = i; pq.pop(); } for (int i = 1; i <= n; i ++) cout << ans[i] << ' '; } int main() { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); solve(); return 0; }
E.双生双宿之错
题意
给定一个数组,每次操作可以使得一个元素加1或者减1,问最小操作几次可以变成双生数组,即元素种类数为2、且出现次数相同。
思路
\(D\) 题的扩展,其实是一个贪心结论题,参考货仓选址。叫中位数定理,这个今天才知道。
先说结论:求一个数 \(x\),让一组元素与 \(x\) 的差的绝对值的和最小,那么 \(x\) 是这组元素的中位数,结果不会更劣。
我们依旧先举例模拟:有两个数3、5,中位数可以是3或5,那么差值和就是2,假如我们在大于5或小于3的范围内选一个数,比如7,那么差值和就是4+2=6比2大。
其实我们可以发现选择的那个数可以让大于它和小于它的数相抵消,如果某方有多出的数就会多增加差值,就上面的例子:5 - 3 = (4 - 3) + (5 - 4)= |3 - 5|,中间的|3 - 4| + (5 - 4)其实就是5-3,-4和+4相抵消了,如果是7变为:|3 - 7| + |5 - 7|相对于4多加了两个(7 - 5)。
有了上面的结论,解决这道题就很容易了,先将数组排序,找出前后两部分的中位数,然后求差的绝对值之和。但要处理两个中位数相等的特殊情况,可以枚举四种情况:假设前半部分的中位数为lmid,后半部分中位数为rmid,那么算出(lmid-1,rmid)、(lmid+1,rmid)、(lmid,rmid-1)、(lmid,rmid+1)的结果然后取最小值。
代码
点击查看代码
#include <iostream> #include <algorithm> using namespace std; typedef long long ll; const int N = 1e5 + 10; int n; int a[N]; void solve() { cin >> n; int m = n / 2; for (int i = 1; i <= n; i ++) cin >> a[i]; sort(a + 1, a + 1 + n); if (a[1] == a[n]) return void(cout << m << '\n'); int midl = a[(m + 1) >> 1], midr = a[(m + 1 + n) >> 1]; bool flag = 0; if (midl == midr) midl --, flag = 1; ll ans = 0; for (int i = 1; i <= m; i ++) ans += abs(a[i] - midl); for (int i = m + 1; i <= n; i ++) ans += abs(a[i] - midr); if (flag) { midl ++, midr ++; ll sum = 0; for (int i = 1; i <= m; i ++) sum += abs(a[i] - midl); for (int i = m + 1; i <= n; i ++) sum += abs(a[i] - midr); ans = min(ans, sum); midl ++, midr --; sum = 0; for (int i = 1; i <= m; i ++) sum += abs(a[i] - midl); for (int i = m + 1; i <= n; i ++) sum += abs(a[i] - midr); ans = min(ans, sum); midl --, midr ++; sum = 0; for (int i = 1; i <= m; i ++) sum += abs(a[i] - midl); for (int i = m + 1; i <= n; i ++) sum += abs(a[i] - midr); ans = min(ans, sum); } cout << ans << '\n'; } int main() { int t; cin >> t; while (t --) solve(); return 0; }
J.硝基甲苯之袭
题意
给定一个数组,问有多少对元素满足它们的gcd等于xor。
思路
一个很有趣的题,涉及到数论,我赛后写了一下发现不难。
首先
然后,假设 \(i = x \oplus y = gcd(x, y)\),根据异或的性质有
此时可以发现,\(i\) 是整除 \(y\) 的,我们可以枚举 \(i\)时,处理 \(i\) 的所有倍数,将符合上述等式且是给出的数组中的元素,那么就是一对方案,最后求和结果要除以2,因为 \(i \oplus y\) 和 \(y\) 都是数组中的元素那么就会重复算两遍。
然后是关于枚举的双重循环
for (int i = 1; i < N; i ++) for (int j = i; j < N; j += i)
这其实是一个和调和级数有关的时间复杂度。
即:
代码
点击查看代码
#include <iostream> #include <algorithm> using namespace std; typedef long long ll; const int N = 2e5 + 10; int n, x; ll cnt[N]; int main() { cin >> n; for (int i = 0; i < n; i ++) { cin >> x; cnt[x] ++; } ll ans = 0; for (int i = 1; i < N; i ++) for (int j = i; j < N; j += i) if ((i ^ j) < N && __gcd(i ^ j, j) == i) ans += cnt[i ^ j] * cnt[j]; cout << ans / 2; return 0; }
C.兢兢业业之移
题意
01矩阵,将所有1移动到矩阵左上角的四分之一区域。
思路
一道模拟题。
- 首先,我们可以从 \((0, 0)\)开始向y轴正方向枚举,到边界后回到下行首即 \((1, 0)\) 重新向y轴正方向枚举。
- 然后枚举中遇到1就将它移动到目标位置,目标位置用 \((x, y)\) 来表示。
- 移动过程中,注意移动的顺序,假设要移动的点在目标位置的上方,我们就要先移动x轴,再移动y轴,因为我这默认目标位置是一行行完成放置的,就意味着小于x的位置都已经放置了1,如果先移动y轴就会导致已放置好的1会被移出目标位置。
如下图,红色1为已放置完成的1,深蓝色线为先移动y轴的情况,会导致同行红色的1被整体向右移动一格,而橙色路线则不会对左上角目标区域造成影响。
- 接着,再考虑一个问题,如果上图的蓝色的1下一格刚好存在一个1,那么会导致这个1被移动到蓝色的1的位置上,所以,枚举时要判定这个格子在移动后是变为0的。
这个思路在最坏情况下,假设 $ \frac{n^2}{4} $ 个 1 都移动步数为 \(2n\),即 \(\frac{n^2}{4} \times 2n = \frac{n^3}{2}\),所以一定是可行的。
代码
点击查看代码
#include <iostream> #include <cstring> #include <vector> #include <array> using namespace std; const int N = 100 + 10; int n; string g[N]; vector<array<int, 4>> ans; void to_x(int i, int j, int x, int y) { while (i < x) { ans.push_back({i, j, i + 1, j}); swap(g[i][j], g[i + 1][j]); i ++; } while (i > x) { ans.push_back({i, j, i - 1, j}); swap(g[i][j], g[i - 1][j]); i --; } } void to_y(int i, int j, int x, int y) { while (j < y) { ans.push_back({i, j, i, j + 1}); swap(g[i][j], g[i][j + 1]); j ++; } while (j > y) { ans.push_back({i, j, i, j - 1}); swap(g[i][j], g[i][j - 1]); j --; } } void move(int i, int j, int x, int y) { if (i < x) { to_x(i, j, x, y); to_y(x, j, x, y); } else if (i > x) { to_y(i, j, x, y); to_x(i, y, x, y); } else { to_y(i, j, x, y); } } void solve() { ans.clear(); cin >> n; for (int i = 0; i < n; i ++) cin >> g[i]; int x = 0, y = 0; for (int i = 0; i < n; i ++) for (int j = 0; j < n; j ++) while (g[i][j] == '1') { move(i, j, x, y); g[x][y] = '2'; y ++; if (y == n / 2) x ++, y = 0; } cout << ans.size() << '\n'; for (auto [i, j, x, y] : ans) cout << i + 1 << ' ' << j + 1 << ' ' << x + 1 << ' ' << y + 1 << '\n'; } int main() { int t; cin >> t; while (t --) solve(); return 0; }
F.双生双宿之探
题意
给定一个数组,问有多少连续子数组是双生数组,即元素种类数为2、且出现次数相同。
思路
求连续子数组的题一般会涉及到双指针、前缀和、差分之类的算法。
首先,我们可以用双指针来维护选择的子数组区间,维护的标准是最长的恰好包含两个元素的子数组区间,我称为类双生数组。
有了上面的选择,我们接下只需要确定这个类双生数组里面有多少个双生数组。
根据双生数组的定义,我们只需要找到类双生数组中有多少个元素x和元素y的个数相等的子区间即可。
求解方法就是做前缀和:让元素x贡献为+1,元素y贡献为-1,然后做前缀和。那么怎么确定子区间是双生数组呢?就是前缀和的值相等的区间就是双生数组。
比如:
你会发现,第一个1和第二个1之间存在一个双生数组“x y”,第一个1和第二个1之间存在一个双生数组“x y x y”,第一个2和第二个2之间存在一个双生数组“y x”等等。
由此就可以得到,遍历前缀和数组,统计每个数出现次数,将每个数在此之前出现次数求和就是答案。特殊的前缀和为0时就是一个双生数组,所以要再加上本身出现的次数。
代码
点击查看代码
#include <iostream> #include <set> #include <map> using namespace std; typedef long long ll; const int N = 1e5 + 10; int n; int a[N]; ll get(int l, int r, int x, int y) { ll res = 0, sum = 0; map<int, ll> s; for (int i = l; i <= r; i ++) { if (a[i] == x) sum ++; else sum --; s[sum] ++; if (sum == 0) res += s[sum]; else res += s[sum] - 1; } return res; } void solve() { cin >> n; for (int i = 1; i <= n; i ++) cin >> a[i]; a[n + 1] = -1; set<int> v; map<int, ll> mp; ll ans = 0; for (int i = 1, j = 1; i <= n; i ++) { v.insert(a[i]); mp[a[i]] ++; while (v.size() > 2 && j <= i) { mp[a[j]] --; if (mp[a[j]] == 0) v.erase(a[j]); j ++; } while (v.size() <= 2 && i <= n) { i ++; v.insert(a[i]); mp[a[i]] ++; } if (v.size() > 2 || i > n) { v.erase(a[i]); mp[a[i]] --; i --; } if (v.size() != 2) break; int x = -1, y = -1; for (auto it : v) if (x == -1) x = it; else y = it; ans += get(j, i, x, y); } cout << ans << '\n'; } int main() { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); int t; cin >> t; while (t --) solve(); return 0; }
本文作者:Natural-TLP
本文链接:https://www.cnblogs.com/Natural-TLP/p/18684517
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步