2025牛客寒假算法基础集训营1
题目链接:2025牛客寒假算法基础集训营1
总结:排名:566,过题数:6
开题太慢了,被A卡了,中后期题没开,后面下机了(doge)
- gcd和按位没有关系
A. 茕茕孑立之影
tag:签到
Solution:输出一个大于1e9的质数即可,判断有无1。
Competing:开始就被卡了,一直再想怎么构造(难绷)。
void solve(){ int n; cin >> n; set<int> st; for (int i = 0; i < n; i ++){ int x; cin >> x; st.insert(x); } if (st.find(1) != st.end()) cout << -1 << endl; else{ cout << 1000000000271 << endl; } }
B. 一气贯通之刃
tag:思维
Solution:判断是否是一条链即可。
Competing:写复杂了,只需要判断是否恰好两个点的度为1,其它全为2即可。
void solve(){ int n; cin >> n; vector<int> d(n + 1); vector<int> ans; for (int i = 1; i < n; i ++){ int v, u; cin >> v >> u; d[v] ++, d[u] ++; } int c = 2; for (int i = 1; i <= n; i ++){ if (d[i] == 1){ ans.pb(i); } else if (d[i] == 2){ c ++; } } if ( 2 == ans.size() && c == n){ cout << ans[0] << " " << ans[1]; } else{ cout << "-1\n"; } }
C. 兢兢业业之移
tag:模拟
Description:给定一个n行n列的矩阵,可以任意多次交换相邻两个元素,将其变为左上角全为1,其它位置全为0的矩阵,输出操作序列。
Solution:枚举左上角的区域,对于每一个位置用bfs找到离它最近的1,同时记录路径即可。
- 对于一个需要放箱子的空位,直接用bfs跑最短路,遇到的第一个箱子路径上一定没有其他箱子,通过路径还原即可。
int dir[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; void solve(){ int n; cin >> n; vector g(n + 1, vector<int>(n + 1)); vector flag(n + 1, vector<int>(n + 1)); for (int i = 1; i <= n; i ++) for (int j = 1; j <= n; j ++) { char ch; cin >> ch; g[i][j] = ch - '0'; } vector<array<int, 4>> ans; for (int i = 1; i <= n / 2; i ++) { for (int j = 1; j <= n / 2; j ++) { if (flag[i][j]) continue; if (g[i][j] == 1) flag[i][j] = true; else { vector pre(n + 1, vector<pii>(n + 1)); vector vis(n + 1, vector<int>(n + 1)); queue<pii> qu; qu.push({i, j}); vis[i][j] = true; while (qu.size()) { auto [x, y] = qu.front(); qu.pop(); if (g[x][y] == 1){ swap(g[i][j], g[x][y]); flag[i][j] = true; while (x != i || y != j){ auto [sx, sy] = pre[x][y]; ans.pb({x, y, sx, sy}); x = sx; y = sy; } break; } for (int k = 0; k < 4; k ++) { int tx = x + dir[k][0], ty = y + dir[k][1]; if (tx < 1 || tx > n || ty < 1 || ty > n) continue; if (flag[tx][ty] || vis[tx][ty]) continue; qu.push({tx, ty}); pre[tx][ty] = {x, y}; vis[tx][ty] = true; } } } } } cout << ans.size() << endl; for (auto[a, b, c, d] : ans) { cout << a << " " << b << " " << c << " " << d << endl; } }
D. 双生双宿之决
tag:签到
Solution:用map记录即可。
void solve(){ int n; cin >> n; vector<int> a(n); map<int, int> mp; for (int i = 0; i < n; i ++){ cin >> a[i]; mp[a[i]] ++; } if (n & 1 || mp.size() != 2){ cout << "No\n"; } else{ int a = -1, b; for (auto [x, y] : mp){ if (a == -1) a = y; else b = y; } if (a == b){ cout << "Yes\n"; } else{ cout << "No\n"; } } }
E. 双生双宿之错
tag:思维
Description:给定一个长度为偶数的数组,每次操作可以将一个元素 + 1或者 - 1,就最小操作次数使得该数组只存在两个元素并且两个元素的个数相等。
Solution:将数组排序后,显然我们需要将前一半的数变为一个值(x),后一半的数变为一个值(y)。
- 现在考虑一个数组将所有数变为一个值的最小操作次数,显然是全部全为中位数。
- 当x == y时,考虑减小x或者增大y。
Addition:带权中位数
- 给定n个点,每个点的人数是ai,求一个值,使所有人移动到该值的距离和最小。
- 人数刚好过半的那个点(等价于求所有值的中位数)。
Competing:榜被带歪了,看了一眼E以为是难题就下机了。
void solve(){ int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; i ++){ cin >> a[i]; } sort(a.begin() + 1, a.end()); int x = a[(n / 2 + 1) / 2], y = a[n / 2 + (n / 2 + 1) / 2]; int ans = 0; if (x != y){ for (int i = 1; i <= n; i ++){ if (i <= n / 2){ ans += abs(x - a[i]); } else{ ans += abs(y - a[i]); } } } else{ int ans1 = 0, ans2 = 0; x --; for (int i = 1; i <= n; i ++){ if (i <= n / 2){ ans1 += abs(x - a[i]); } else{ ans1 += abs(y - a[i]); } } x ++, y ++; for (int i = 1; i <= n; i ++){ if (i <= n / 2){ ans2 += abs(x - a[i]); } else{ ans2 += abs(y - a[i]); } } ans = min(ans1, ans2); } cout << ans << endl; }
F. 双生双宿之探
tag:双指针
Description:给定一个数组,求该数组有多少个连续子数组满足:数组的元素为两种,且每个元素的个数相等。
Solution: 先使用双指针找出所有只包含两个元素的最长区间,对于每个区间我们只需要判断有多少连续子区间使两个元素的个数相等,我们令一个元素为-1,另一个为1,那么等价与求有多少个前缀和为0.
void solve(){ int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i ++){ cin >> a[i]; } vector<pii> v; map<int, int> mp; for (int l = 0, r = 0; r < n; r ++){ mp[a[r]] ++; if (mp.size() == 3){ v.pb({l, r - 1}); while (mp.size() > 2){ mp[a[l]] --; if (mp[a[l]] == 0){ mp.erase(a[l]); } l ++; } } if (r == n - 1 && mp.size() == 2){ v.pb({l, r}); } } int ans = 0; for (auto [l, r] : v){ map<int, int> mmp; mmp[0] = 1; int t = 0, x = -1; for (int i = l; i <= r; i ++){ if (x == -1) x = a[i]; if (a[i] == x) t += 1; else t -= 1; ans += mmp[t]; mmp[t] ++; } } cout << ans << endl; }`
G. 井然有序之衡
tag:模拟
Description:给定一个序列,每次操作选择两个数一个数+1,另一个数-1,求最小操作次数使其变为排列,不能输出-1。
Solution:注意到序列之和不变,判断序列之和是否符合排列的条件,然后直接模拟即可。
void solve(){ int n; cin >> n; vector<int> a(n); int sum = 0; for (int i = 0; i < n; i ++){ cin >> a[i]; sum += a[i]; } if (sum != (1 + n) * n / 2){ cout << -1 << endl; return; } int ans = 0; sort(a.begin(), a.end()); for (int i = 0; i < n; i ++){ if (a[i] < i + 1){ ans += i + 1 - a[i]; } else{ break; } } cout << ans << endl; }
H. 井然有序之窗
tag:贪心
Description:给定n个区间(l, r)
,为每个区间分配一个数字,要求分配的数字是一个排列,如果不能输出-1。
Solution:经典贪心问题,一个数i可以放在所有l <= i <= r
的区间内,将其放在r最小的区间内不会让答案更劣(如果将其让在r更大的区间,那么这两个区间是等价的)。
- 将区间按左端点排序,将左端点小于等于i的右端点放入优先队列。
Competing:这种题做过很多次了,没想到还是不会(o(╥﹏╥)o)。
struct node{ int l, r, idx; bool operator < (const node& a) const{ return l < a.l; } }; void solve(){ int n; cin >> n; vector<node> a(n); for (int i = 0; i < n; i ++){ int l, r; cin >> l >> r; a[i] = {l, r, i}; } sort(a.begin(), a.end()); vector<int> ans(n, -1); priority_queue<pii, vector<pii>, greater<pii>> qu; for (int i = 1, j = 0; i <= n; i ++){ // 枚举左端点 while (j < n && a[j].l <= i){ qu.push({a[j].r, a[j].idx}); j ++; } if (qu.size() == 0){ cout << -1 << endl; return; } else{ auto [r, idx] = qu.top(); if (r < i){ cout << -1 << endl; return; } qu.pop(); ans[idx] = i; } } for (int i = 0; i < n; i ++){ cout << ans[i] << " \n"[i + 1 == n]; } }
I. 井然有序之桠
tag:构造
Description:给定一个长度为n的排列a,要求构造一个长度为n的排列b,使得
Solution:现将a排列,那么可以得到sum的最小值n和最大者(n + 1) * n / 2
。
- 令sum = 最大值,考虑如何减小sum。交换相邻两个数可以使sum减小:x + x - 1 - 2。
- 我们从后往前枚举,如果能交互相邻两个数则进行交换。注意如果当前数是x,当sum - k < x,可以直接交换1 和 x。
- 唯一一个特例时,当n = 4,k = 6时,需要排列为
3 4 1 2
。
void solve() { int n, k; cin >> n >> k; vector<int> a(n + 1); for (int i = 1; i <= n; i ++) { cin >> a[i]; } vector<int> b(n + 1); iota(b.begin(), b.end(), 0); int sum = (n + 1) * n / 2; if (k < n || k > sum) { cout << -1 << endl; return; } int x = n; while (sum > k && x > 1) { if (sum - k <= x - 1) { // 交换1和t,令sum -= t - 1 int t = sum - k + 1; sum = k; swap(b[1], b[t]); break; } int t = 2 * x - 3; // 交换x - 1和x,变化值为:x + x - 1 - 2 if (sum - k >= t) { sum -= t; swap(b[x - 1], b[x]); x --; } x --; } if (sum != k) { b[1] = 3; b[2] = 4; b[3] = 1; b[4] = 2; sum = k; } for (int i = 1; i <= n; i ++) { cout << b[a[i]] << " \n"[i == n]; } }
J. 硝基甲苯之袭
tag:数论
Description:给定n个数,求有多少对(i, j)满足gcd(ai, aj) = ai xor aj(i < j),1 <= n <= 2e5
。
Solution:由gcd(x, y) = x xor y
可以得到gcd(x, y) = x xor y = x - y(x > y)
。
x - y <= x xor y <= x + y,gcd(x, y) = d 小于等于 x - y(x - y == kd)
,因此gcd(x, y) == x - y = d
。- 那么我们枚举d,然后枚举y(d的倍数),得当x = y + d,检查
gcd(x, y)是否等于x - y
即可。 - 时间复杂度
,调和级数。
void solve(){ int n; cin >> n; map<int, int> mp; for (int i = 0; i < n; i ++){ int x; cin >> x; mp[x] ++; } int ans = 0; n = mp.rbegin() -> fi + 5; for (int k = 1; k <= n; k ++){ for (int b = k; b + k <= n; b += k){ if (mp.find(b) == mp.end()) continue; int a = b + k; if (mp.find(a) == mp.end()) continue; if ((a ^ b) == k){ ans += mp[a] * mp[b]; } } } cout << ans << endl; }
补充:因为x的gcd一定是x的倍数,那么我们枚举x的倍数j,假设j就是gcd(x, y) == x xor y,那么y == x xor j,我们判断gcd(x, y)是否等于j即可,时间复杂度
void solve(){ int n; cin >> n; map<int, int> mp; vector<int> a(n); for (int i = 0; i < n; i ++){ cin >> a[i]; } int ans = 0; for (int i = 0; i < n; i ++){ // x ^ y == d => y = x ^ d for (int j = 1; j <= a[i] / j; j ++){ // 枚举因子 if (a[i] % j == 0){ if (j == gcd(a[i], a[i] ^ j) && mp.find(a[i] ^ j) != mp.end()){ ans += mp[a[i] ^ j]; } if (j * j != a[i]){ int t = a[i] / j; if (t == gcd(a[i], a[i] ^ t) && mp.find(a[i] ^ t) != mp.end()){ ans += mp[a[i] ^ t]; } } } } mp[a[i]] ++; } cout << ans << endl; }
K. 硝基甲苯之魇
tag:前缀和、二分、st表/线段树
Description:给定一个数组,求有多少个区间满足区间gcd == 区间异或和。
Solution:gcd跟位运算无关。暴力:枚举每一个区间然后求每个区间的gcd和异或和。
- 优化1:快速求某个区间的gcd(st表/线段树)、快速求某个区间的异或和(前缀和)。
- 优化2:暴力枚举每个区间肯定超时,考虑如何优化区间的查询:固定左端点为l,注意到右端点的取值是一段一段的(即一个区间内的gcd是相等的0),并且最多只有log个段。(因为下一个gcd一定是当前gcd的因子);
- 固定l,当前区间(就a[l]一个点)的gcd为ap[l],我们二分找到gcd小于x的第一个点idx1,然后找到小于当前gcd的第一个点idx2,那么区间[idx1, idx2 - 1]的gcd相等,注意第一个区间和最后一个区间的计算。
- 如何求这段区间内异或和等于y的区间个数。
- 优化3:区间内求某个数出现次数
- map套vector:vector记录每个数出现的位置(排好序)。
- 查询时只需要二分第一个大于等于左端点的值和最后一个小于等于右端点的值。
- 时间复杂度:
- 对拍有大用(需要可以私信)
int n, m; int st[N][20]; int xo[N]; int a[N]; void init(){ for (int j = 0; j <= 20; j ++) { for (int i = 1; i + (1 << j) - 1 <= n; i ++) { if (j == 0) { st[i][j] = a[i]; } else{ st[i][j] = gcd(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]); } } } } int qgcd(int l, int r) { int k = log2(r - l + 1); return gcd(st[l][k], st[r - (1 << k) + 1][k]); } int qxor(int l, int r){ return xo[r] ^ xo[l - 1]; } void solve(){ cin >> n; map<int, vector<int>> mp; xo[0] = 0; mp[0].pb(0); for (int i = 1; i <= n; i ++) { cin >> a[i]; xo[i] = xo[i - 1] ^ a[i]; mp[xo[i]].pb(i); } init(); auto find = [&](int i, int x){ // 二分找到gcd == x 的最后一个数的下标 // i是枚举的区间左端点 int l = i, r = n + 1; while (l + 1 < r){ int mid = l + r >> 1; if (qgcd(i, mid) >= x) l = mid; else r = mid; } return l; }; int ans = 0; for (int i = 1; i <= n - 1; i ++) { // 枚举左端点 int tx = gcd(a[i], a[i + 1]); // 当前gcd int l = i; // 当前区间的左端点 int f = qgcd(i, n); // 记录需要查询答案的区间(l, r, gcd) vector<array<int, 3>> qu; while (l <= n) { int idx = find(i, tx); // 那么当前tx对应的区间为(l, idx),当前区间gcd为tx qu.pb({l, idx, tx}); // 更新l和tx l = idx + 1; if (l > n) break; tx = qgcd(i, l); } for (auto [l, r, tg] : qu) { int t = qxor(1, i - 1) ^ tg; // t ^ qxor == tg // debug(i, l, r, tg, t); // 左边界大于等于l // 左边界至少是i + 1 int li = lower_bound(mp[t].begin(), mp[t].end(), max(l, i + 1)) - mp[t].begin(); // 右边界大于r - 1 int ri = upper_bound(mp[t].begin(), mp[t].end(), r) - mp[t].begin(); if (ri - 1 >= li) { // 能找到几个数代表有几个区间 ans += ri -1 - li + 1; } } } cout << ans << endl; }
L. 一念神魔之耀
tag:构造、思维
Description:给定长度为
- 选择长度为x的区间,将它们取反。
- 选择长度为y的区间,将它们取反。
- 最多执行
次操作,求能否将其全变为1。 1 <= n <= 500, 1 <= x, y <= n/3
Solution:显然操作两次等价于不操作,操作顺序不影响答案。一定要重视题目给的数据范围。
- 先操作一个y,再y的基础上操作一个x可以得到y - x的操作长度(后x区间被还原),那么显然可以用y和x得到一个长度为
的区间。 - 那么我们现在可以操作长度为
的区间,显然我们从以第一个位置开始,没遇到一个为0的位置都需要执行一次。 - 如果得到长度为
的区间?先执行长度为y的区间(y > x),然后保留前面长度为 的区间,从y开始往前面还原,如果剩下的部分 > x,则减去x,如果剩下一段小于x,那么在剩余部分后面加上y,然后继续-x,直到减完只剩g为止。 - 为何能满足条件:gcd的长度 < min(x, y),剩余部分小于x,加上一段y,总长度小于n。
- 但是需要注意我们需要保证0尽可能再前面才行,因此需要先使用操作将后面的0变为1。(debug两小时,o(╥﹏╥)o)。
void solve(){ int n, x, y; string s; cin >> n >> x >> y >> s; if (x < y) { swap(x, y); } s = '&' + s; int g = gcd(x, y); // 顺序不影响答案 // 记录答案的左右端点,操作两次等于没操作 map<pii, int> ans; function<void (int, int)> op = [&](int l, int r) { // 将l到r取反 ans[{l, r}] ++; for (int i = l; i <= r; i ++) { if (s[i] == '0') s[i] = '1'; else s[i] = '0'; } }; function<bool(int, int)> check = [&](int l, int r) { if (l < 1 || r > n) { return false; } return true; }; function<void(int)> f = [&](int i) { // 第i位是0 // debug(i); vector<pii> tmp; // 先将x位取反 bool flag = true; if (check(i, i + x - 1)) { tmp.pb({i, i + x - 1}); } else{ flag = false; } // 前g位不变,从i + x - 1位开始往前变 int r = i + x - 1; int l = i + g - 1; while (r > l && flag) { // 减到r == l就刚好 if (r - y + 1 <= l) { // 长度不够y先加一段x if (check(r + 1, r + 1 + x - 1)) { tmp.pb({r + 1, r + 1 + x - 1}); } else { flag = false; } r = r + 1 + x - 1; } if (check(r - y + 1, r)) { tmp.pb({r - y + 1, r}); } else { flag = false; } r = r - y; } if (flag) { for (auto [x, y] : tmp) { op(x, y); } } }; for (int i = n; i - y + 1 >= 1; i --) { if (s[i] == '0') { op(i - y + 1, i); } } for (int i = 1; i <= n; i ++) { if (s[i] == '0') { f(i); } } if (count(s.begin(), s.begin() + n + 1, '0')) { cout << -1 << endl; } else { int res = 0; for (auto&& [t, cnt] : ans) { cnt = cnt % 2; res += cnt; } assert(res <= n * n); cout << res << endl; for (auto [t,cnt] : ans) { if (cnt) { cout << t.fi << " " << t.se << endl; } } } }
M. 数值膨胀之美
tag:思维
Description:给定一个数组,需要选择一个区间(非空),将区间中的每个数 * 2,问数组的极差(最大值与最小值之差)最小是多少?
Solution:显然可以想到将最小值扩大(维护一个区间,将所有最小值都扩大),那么下一个区间需要将所有次小值包括进去否则不会改变结果,依次类推。
- 注意开始时可能只需要扩大一个最小值而不是一个区间( 等价于没改变极差)。
Competing:开始贪心的变大了最小值,因为数据太水过了(难绷啊)。
void solve(){ int n; cin >> n; vector<pii> a(n); multiset<int> st; vector<int> b(n); for (int i = 0; i < n; i ++){ cin >> a[i].fi; a[i].se = i; st.insert(a[i].fi); b[i] = a[i].fi; } sort(a.begin(), a.end()); int l = a[0].se, r = a[0].se; st.insert(a[0].fi * 2); st.erase(st.find(a[0].fi)); auto f = [&](){ // 得到当前极值 return *st.rbegin() - *st.begin(); }; int res = f(); for (int i = 1; i < n; i ++){ while (l > a[i].se){ l --; st.insert(b[l] * 2); st.erase(st.find(b[l])); } while (r < a[i].se){ r ++; st.insert(b[r] * 2); st.erase(st.find(b[r])); } res = min(res, f()); } cout << res << endl; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!