2023牛客寒假算法基础集训营2 A-L
A
题解
知识点:数学。
用 减去区间1的端点得到匹配的一个区间,求一下与区间2的交集。
一个小公式,两区间 和 的交集长度为 。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; bool solve() { int n; cin >> n; int l1, r1, l2, r2; cin >> l1 >> r1 >> l2 >> r2; int y = n - l1, x = n - r1; cout << max(0, min(y, r2) - max(x, l2) + 1) << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
B
题解
知识点:数学。
用 减去区间1的端点得到匹配的一个区间,求一下与区间2的交集。
一个小公式,两区间 和 的交集长度为 。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; bool solve() { int n; cin >> n; int l1, r1, l2, r2; cin >> l1 >> r1 >> l2 >> r2; int y = n - l1, x = n - r1; cout << max(0, min(y, r2) - max(x, l2) + 1) << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
C
题解
方法一
知识点:枚举,差分,前缀和。
枚举每个区间 的匹配区间 ,匹配区间的每个点被其他区间覆盖的次数总和。
我们可以预处理出每个点被区间覆盖的次数 ,可以用差分再前缀和得到。对此,再做一次前缀和,就可以快速得到 每个点被所有区间覆盖的次数总和, 。
再减去与 重合部分的一段,可以用公式 。
要注意先特判 和 的无交集情况。
之后,再处理 和 的越界情况。
时间复杂度
空间复杂度
方法二
知识点:枚举,差分,排列组合。
同样先求出每个点被覆盖多少次 ,对于一个点 能匹配到 因此次数总数为 。
考虑每个区间 的匹配区间 与自己相交的情况,需要减去相交部分一次,因此可以一开始都减好,之后再对每个点累和。
时间复杂度
空间复杂度
代码
方法一
#include <bits/stdc++.h> using ll = long long; using namespace std; const int P = 998244353; int L[400007], R[400007]; ll d[200007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m; cin >> n >> m; for (int i = 1;i <= m;i++) { cin >> L[i] >> R[i]; d[L[i]]++; d[R[i] + 1]--; } for (int i = 1;i <= 2e5;i++) d[i] += d[i - 1]; for (int i = 1;i <= 2e5;i++) d[i] += d[i - 1]; ll ans = 0; for (int i = 1;i <= m;i++) { int y = n - L[i], x = n - R[i]; if (y <= 0 || x > 2e5) continue; x = max(x, 1); y = min(y, 200000); ans = ans + d[y] - d[x - 1] - max(0, min(y, R[i]) - max(x, L[i]) + 1); ans %= P; } cout << ans << '\n'; return 0; }
方法二
#include <bits/stdc++.h> using ll = long long; using namespace std; const int P = 998244353; int L[400007], R[400007]; ll d[200007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m; cin >> n >> m; ll ans = 0; for (int i = 1;i <= m;i++) { cin >> L[i] >> R[i]; d[L[i]]++; d[R[i] + 1]--; ans -= max(0, min(n - L[i], R[i]) - max(n - R[i], L[i]) + 1); (ans += P) %= P; } for (int i = 1;i <= 2e5;i++) d[i] += d[i - 1]; for (int i = max(n - 200000, 1);i <= min(200000, n - 1);i++) { int j = n - i; ans += 1LL * d[i] * d[j]; ans %= P; } cout << ans << '\n'; return 0; }
D
题解
知识点:贪心。
一个节点的深度就是这个节点的能量能被获取的次数,显然深度越大的节点能量应该越大,所以直接求完深度从小到大排序,能量也从小到大排序,乘在一起加起来就行。
因为 ,所以可以直接求出每个点的深度,不需要树形dp。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; const int N = 200007; int a[N]; int dep[N]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; dep[1] = 1; for (int i = 2;i <= n;i++) { int f; cin >> f; dep[i] = dep[f] + 1; } for (int i = 1;i <= n;i++) cin >> a[i]; sort(a + 1, a + n + 1); sort(dep + 1, dep + n + 1); ll ans = 0; for (int i = 1;i <= n;i++) { ans += 1LL * dep[i] * a[i]; } cout << ans << '\n'; return 0; }
E
题解
知识点:数学,二分。
通过一些不容易的证明,可以知道全局最小值一定出现在 两个点。
具体的,我们考虑 容易知道 就是最小值点,但对于 ,考虑 两边的变化率。若 , 右侧 的减量小于 的增量,所以 的减量小于等于 增量;左侧 的增量大于 的减量,所以 的增量大于等于 减量,所以全局最小值一定出现在 两个点,就可以比较得出最小值所在点了。
设全局最小值点为 ,若 显然答案为 。
否则,考虑区间 的局部最小值点,因为 递减,所以局部最小值点为 ,我们在 内二分找到最左侧的最小值点即可。
注意这道题直接三分不行,考虑三分:
2211222|2222222|2222222 2222222|2222222|2221122
显然此时我们就无法判断是向左还是向右收缩。当然可以动用人类智慧,三分找到个局部点左右枚举 个数qwq。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; ll n, L, R; ll x; ll f(ll x) { return n / x + x - 1; } bool check(ll mid) { return f(mid) - f(x) > 0; } bool solve() { cin >> n >> L >> R; x = sqrt(n); x = f(x) <= f(x + 1) ? x : x + 1;//全局最小值点 if (L >= x) cout << L << '\n'; else { x = min(R, x);//[L,R]的最小值点 ll l = L, r = x; while (l <= r) { ll mid = l + r >> 1; if (check(mid)) l = mid + 1; else r = mid - 1; } cout << l << '\n'; } return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
F
题解
知识点:BFS。
找到同时能到起点和终点的点即可,于是从起点和终点分别搜索。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; int n, k; bool dt[500007][10]; struct node { int x, y; }; queue<node> q; void bfs(node st, vector<vector<int>> &vis, vector<vector<int>> &dir) { vis[st.x][st.y] = 1; q.push(st); while (!q.empty()) { node cur = q.front(); q.pop(); for (int i = 0;i < 2;i++) { int xx = cur.x + dir[i][0]; int yy = cur.y + dir[i][1]; if (xx <= 0 || xx > n || yy <= 0 || yy > 3 || vis[xx][yy] || dt[xx][yy]) continue; vis[xx][yy] = 1; q.push({ xx,yy }); } } } bool solve() { cin >> n >> k; for (int i = 1;i <= n;i++) dt[i][1] = dt[i][2] = dt[i][3] = 0; for (int i = 1;i <= k;i++) { int x, y; cin >> x >> y; dt[x][y] ^= 1; } vector<vector<int>> vis1(n + 1, vector(4, 0)); vector<vector<int>> vis2(n + 1, vector(4, 0)); vector<vector<int>> dir1 = { {1,0},{0,1} }; vector<vector<int>> dir2 = { {-1,0},{0,-1} }; bfs({ 1,1 }, vis1, dir1); bfs({ n,3 }, vis2, dir2); if (!vis1[n][3]) cout << 0 << '\n'; else { int ans = 0; for (int i = 1;i <= n;i++) { for (int j = 1;j <= 3;j++) { if (vis1[i][j] && vis2[i][j]) ans++; } } cout << ans - 1 << '\n'; } return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
G
题解
知识点:线段树,STL,枚举。
显然,怪物可以视作障碍物。
先考虑一个简化模型,怪物固定不变的地图,如何快速确定可以走到的区域。
考虑第一列和第三列暂定有效区域:
- 第一列向下的第一个障碍往下是一定不能走的,记这个怪物往上一格的位置为 ,第一列的暂定有效区域为 。
- 第三列向上的第一个障碍往上是一定不能走的,记这个怪物往下一格的位置为 ,第二列的暂定有效区域为 。
然后,我们可以通过第一列和第三列的障碍确定第二列的有效区域 :
- 若第二列第 行以及往下存在障碍物,则可以与第一列联合阻挡,记其行数为 。继续找到 往上的第一个空位记作 ,则第二列的有效区域的下界为 。
- 若第二列第 行以及往上存在障碍物,则可以与第三列联合阻挡,记其行数为 。继续找到 往下的第一个空位记作 ,则第二列的有效区域的上界为 。
最后,我们通过第二列的有效区域得到第一列和第三列最终有效区域:
- 第一列 部分可能会被第二列 障碍阻挡,则最终有效区域为 ,令 为 。
- 第三列 部分可能会被第二列 障碍阻挡,则最终有效区域为 ,令 为 。
如果 或者 或者 则无解。
否则答案为 ,其中 指 的障碍个数。
再考虑变化的情况,我们发现可以利用 set
来维护障碍和空位的修改和查找的功能,三列障碍物用三个 set
,再加上一个 set
维护第二列空位。同时, 用线段树或者树状数组维护区间和即可。
这里用 set
查找时候有个小技巧,给 set
加边界,同时边界不影响正常使用。这里是 和 ,很好的避免了越界,同时可以直接用来进行下一次查找,非常方便。
这样的目的是,保证任何查找一定有结果,避免查找到 st.end()
和 prev(st.begin())
的越界情况。需要注意的是,找最小值的 set
应加极大值边界而不能加极小值边界,防止使用 *st.begin()
时出错,同理找最大值的也一样;对于使用 xx_bound
查找任意大于(等于)、小于(等于)的各类情况,应该同时加极大值极小值边界。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; ///线段树,建树O(nlogn)、修改查询O(logn),单点修改、区间查询 //需要魔改内部,就把泛型删掉 template<class T, class F> class SegmentTree { const int n; vector<T> node; void update(int rt, int l, int r, int pos, F f) { if (r < pos || l > pos) return; if (l == r) { node[rt] = f(node[rt]); return; } int mid = l + r >> 1; update(rt << 1, l, mid, pos, f); update(rt << 1 | 1, mid + 1, r, pos, f); node[rt] = node[rt << 1] + node[rt << 1 | 1]; } T query(int rt, int l, int r, int x, int y) { if (l > y || r < x) return T::e(); if (x <= l && r <= y) return node[rt]; int mid = l + r >> 1; return query(rt << 1, l, mid, x, y) + query(rt << 1 | 1, mid + 1, r, x, y); } public: SegmentTree(int _n):n(_n), node(_n << 2, T::e()) {} SegmentTree(int _n, vector<T> &src):n(_n), node(_n << 2, T::e()) { function<void(int, int, int)> build = [&](int rt, int l, int r) { if (l == r) { node[rt] = src[l]; return; } int mid = l + r >> 1; build(rt << 1, l, mid); build(rt << 1 | 1, mid + 1, r); node[rt] = node[rt << 1] + node[rt << 1 | 1]; }; build(1, 1, n); } void update(int pos, F f) { update(1, 1, n, pos, f); } T query(int x, int y) { return query(1, 1, n, x, y); } }; ///节点元封装类,定义单位元"e"、合并"+" struct T { int val; static T e() { return T{ 0 }; } friend T operator+(const T &a, const T &b) { return { a.val + b.val }; } }; ///修改元封装类,定义映射"()" struct F { int diff; T operator()(const T &x) { return T{ diff + x.val }; } }; bool solve() { int n, k; cin >> n >> k; set<int> st[4]; SegmentTree<T, F> sgt(n); //加边界防止跑到end()或者prev(begin) for (int i = 0;i <= n + 1;i++) st[0].insert(i); st[1].insert(n + 1); st[3].insert(0); st[2].insert(0); st[2].insert(n + 1); while (k--) { int x, y; cin >> x >> y; if (auto it = st[y].find(x);it != st[y].end()) { st[y].erase(it); if (y == 2) sgt.update(x, { -1 }), st[0].insert(x); } else { st[y].insert(x); if (y == 2) sgt.update(x, { 1 }), st[0].erase(x); } int r = *st[1].begin() - 1; int R = *prev(st[0].lower_bound(*st[2].lower_bound(r))); int l = *prev(st[3].end()) + 1; int L = *st[0].upper_bound(*prev(st[2].upper_bound(l))); r = min(r, R); l = max(l, L); if (r < L || R < L || R < l) cout << 0 << '\n'; else cout << r + (n - l + 1) + (R - L + 1 - sgt.query(L, R).val) - 1 << '\n'; } return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
H
题解
方法一
知识点:枚举,差分,贪心。
因为分成子序列,因此可以随意分配,我们优先把一个数字分在一个序列里,剩下的分在同一个。
因此,我们可以先求出每个数字的出现次数,再考虑出现次数对答案的贡献:
k/贡献/出现次数 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 0 |
2 | 1 | 2 | 1 | 1 | 1 |
3 | 1 | 2 | 3 | 2 | 2 |
4 | 1 | 2 | 3 | 4 | 3 |
5 | 1 | 2 | 3 | 4 | 5 |
我们发现规律 时为 ,否则为 。
我们对此进行两次差分得到表格:
k/贡献二次差分/出现次数 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 0 |
2 | -1 | 2 | 1 | 1 | 1 |
3 | 0 | -2 | 1 | 0 | 0 |
4 | 0 | 0 | -2 | 1 | 0 |
5 | 0 | 0 | 0 | -2 | 1 |
我们在 处加 , 处减一即可。
最后前缀和两次,就是答案了。
时间复杂度
空间复杂度
方法二
知识点:枚举,贪心,前缀和,二分。
利用上面发现的规律:
时为 ,否则为 。
我们先求出每个数字出现的次数,然后按照次数从小到大排序,随后枚举 。
我们用二分找到 的位置,于是 的部分为 用前缀和得到总和,否则就是个数乘 。
时间复杂度
空间复杂度
代码
方法一
#include <bits/stdc++.h> using ll = long long; using namespace std; int cnt[100007]; int ans[100007]; bool solve() { int n; cin >> n; for (int i = 1;i <= 1e5;i++) ans[i] = cnt[i] = 0; for (int i = 1;i <= n;i++) { int x; cin >> x; cnt[x]++; } for (int i = 1;i <= 1e5;i++) { if (!cnt[i]) continue; ans[2]++; ans[cnt[i]]++; ans[cnt[i] + 1] -= 2; } for (int i = 1;i <= n;i++) ans[i] += ans[i - 1]; for (int i = 1;i <= n;i++) ans[i] += ans[i - 1]; for (int i = 1;i <= n;i++) cout << ans[i] << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
方法二
#include <bits/stdc++.h> using ll = long long; using namespace std; int cnt[100007]; int sum[100007]; bool solve() { int n; cin >> n; for (int i = 1;i <= 1e5;i++) cnt[i] = 0; for (int i = 1;i <= n;i++) { int x; cin >> x; cnt[x]++; } sort(cnt + 1, cnt + 100000 + 1); for (int i = 1;i <= 1e5;i++) sum[i] = sum[i - 1] + cnt[i]; for (int i = 1;i <= n;i++) { int idx = upper_bound(cnt + 1, cnt + 100000 + 1, i) - cnt; cout << sum[idx - 1] + (100000 - idx + 1) * (i - 1) << '\n'; } return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
I
题解
知识点:枚举。
我们不可能每个整数都平移一次,因为每次平移有可能没有一个点的贡献发生变化,是无效平移。
因此,我们考虑对每个区间取一个点作为判定点,所有点与此判定点位置作差作为平移距离放进数组中,从小到大排序后依次平移,每次一定至少有一个点的值发生变化,总体复杂度是 的。
为了方便,我们一开始将所有的点平移到 号区间,方便之后依次累加。
平移到 号区间后,对于每个点,我们从 到 号区间依次求出到每个判定点的距离,以及相邻区间贡献的差值放入数组。然后,按距离从小到大排序,每一次平移都保证比上一次平移长,因此对于上一次得到的答案,我们只需要加一次这次改变的点的贡献改变量,就能得到这一次的答案。贡献改变量一定是对应相邻区间的差值,因为是按距离从小到大遍历,不可能直接跨越两个区间。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; ll x[200007]; int line[6];//区间的判定点 int v[6]; bool solve() { int n; cin >> n; for (int i = 1;i <= n;i++) cin >> x[i]; for (int i = 2;i <= 5;i++) cin >> line[i]; for (int i = 1;i <= 5;i++) cin >> v[i]; line[5]++; int delta = line[2] - 1 - *max_element(x + 1, x + n + 1); for (int i = 1;i <= n;i++) x[i] += delta;//平移到1区间,方便后面累加 ll ans = 1LL * v[1] * n; vector<pair<ll, int>> d;//(原点到判定点的距离,判定点与后面上一个判定点的差值),按距离加一次就可以累加 for (int i = 1;i <= n;i++) { for (int j = 2;j <= 5;j++) { d.push_back({ line[j] - x[i],v[j] - v[j - 1] }); } } sort(d.begin(), d.end()); ll sum = ans; int l = 0, r = 0; while (l < d.size()) { while (r < d.size()) { if (d[l].first != d[r].first) break; sum += d[r].second; r++; } l = r; ans = max(ans, sum); } cout << ans << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
J
题解
知识点:数学。
分类讨论:
综上 。
所以
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; bool solve() { int n; cin >> n; ll ans = 0; for (int i = 1;i <= n;i++) { int x; cin >> x; ans += abs(x); } ans *= 2 * n; cout << ans << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
K
题解
知识点:图论,根号分治,枚举。
题目可以转化为 个点 条边的一张无向图。 次询问,每次选出 个点,求这些点构成的最大子图的边数。
直接枚举的最坏复杂度是 ,显然不可行,问题出在有些点的边可能很多。
此时我们利用平衡思想。因为边的方向不重要,所以可以给边定向,减少某些点的边数。我们考虑一条边的两个点 的度数 ,当其满足 时,建 的边;否则,建 的边。这种操作能将边数平衡到 的复杂度。
证明:
设 的出边个数为 ,则有 ,因此可以证明 。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; struct Graph { struct edge { int v, nxt; }; int idx; vector<int> h; vector<edge> e; Graph(int n, int m):idx(0), h(n + 1), e(m + 1) {} void init(int n) { idx = 0; h.assign(n + 1, 0); } void add(int u, int v) { e[++idx] = edge{ v,h[u] }; h[u] = idx; } }; const int N = 2e5 + 7, M = 2e5 + 7; Graph g(N, M); int feat[N]; bool vis[N]; int deg[N]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m, q; cin >> n >> m >> q; vector<pair<int, int>> edge(m + 1); for (int i = 1;i <= m;i++) { int u, v; cin >> u >> v; edge[i] = { u,v }; deg[u]++; deg[v]++; } for (int i = 1;i <= m;i++) { auto [u, v] = edge[i]; if (deg[u] < deg[v]) g.add(u, v); else g.add(v, u); } while (q--) { int k; cin >> k; for (int i = 1;i <= k;i++) cin >> feat[i], vis[feat[i]] = 1; int ans = 0; for (int i = 1;i <= k;i++) { for (int j = g.h[feat[i]];j;j = g.e[j].nxt) { int v = g.e[j].v; if (!vis[v]) continue; ans++; } } cout << ans << '\n'; for (int i = 1;i <= k;i++) vis[feat[i]] = 0; } return 0; }
L
题解
知识点:数论,枚举。
考虑先将 和 的值的个数统计到 和 中。
随后 ,但此时我们没考虑 或 的情况,我们只要单独把 的答案减去 即可,代表减掉 两组。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using ll = long long; using namespace std; int a[5007]; int cnta[5007], cntb[5007]; ll ans[5007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, p; cin >> n >> p; for (int i = 1;i <= n;i++) cin >> a[i]; for (int i = 1;i <= n;i++) cnta[a[i] % p]++; for (int i = 1;i <= n;i++) for (int j = 1;j <= n;j++) if (i != j) cntb[1LL * a[i] * a[j] % p]++; for (int i = 0;i < p;i++) for (int j = 0;j < p;j++) ans[(i + j) % p] += 1LL * cnta[i] * cntb[j]; for (int i = 1;i <= n;i++) for (int j = 1;j <= n;j++) if (i != j) ans[(1LL * a[i] * a[j] + a[i]) % p] -= 2; for (int i = 0;i < p;i++) cout << ans[i] << " \n"[i == p - 1]; return 0; }
本文来自博客园,作者:空白菌,转载请注明原文链接:https://www.cnblogs.com/BlankYang/p/17060976.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧