2023牛客暑期多校训练营5 ABCDEGHI
A
题解
知识点:莫队,树状数组。
区间询问显然可以离线莫队,考虑端点移动对答案的影响。
不妨先考虑右端点右移一个位置,对答案的改变。假设右端点右移后在 ,我们先要知道 中和 相等的位置。对于每个这样的位置 ,我们将 中小于 的数字个数加起来,就是右移的贡献。
我们可以先树状数组预处理出,对于每个点在它之前比它小的数字个数,记为 。那么我们便可以简化最后一步的计算个数,最终答案就是 。
我们发现其中 是定值,若我们知道了 中所有与 相等的位置个数 ,那么总的贡献又可以变为 ,因此我们可以同步维护 表示 中值为 的数字个数。
同时,我们也可以同步维护 表示 中所有值为 的位置 的 的和,来直接得到最后一项求和。
类似的有其他三个操作:左端点左移、右端点左移、左端点右移。
注意,莫队分块的最佳大小是 。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; template <class T> class Fenwick { int n; vector<T> node; public: Fenwick(int _n = 0) { init(_n); } void init(int _n) { n = _n; node.assign(n + 1, T()); } void update(int x, T val) { for (int i = x;i <= n;i += i & -i) node[i] += val; } T query(int x) { T ans = T(); for (int i = x;i >= 1;i -= i & -i) ans += node[i]; return ans; } T query(int l, int r) { T ans = T(); ans += query(r); ans -= query(l - 1); return ans; } int lower_bound(T val) { int pos = 0; for (int i = 1 << __lg(n); i; i >>= 1) { if (pos + i <= n && node[pos + i] < val) { pos += i; val -= node[pos]; } } return pos + 1; } int upper_bound(T val) { int pos = 0; for (int i = 1 << __lg(n); i; i >>= 1) { if (pos + i <= n && node[pos + i] <= val) { pos += i; val -= node[pos]; } } return pos + 1; } }; /// 树状数组,修改查询O(logn),单点修改、前缀查询 //* 修改操作需满足结合律,即满足线段树的lazy标记要求 //* 任意区间查询只支持可以减法的运算 //* 倍增查找只支持基本类型 int a[500007]; int les[500007]; struct Query { int l, r, id; }Q[500007]; int cnt[500007]; ll sum_les[500007]; ll cur; void add(int x, bool isLeft) { ll res = 1LL * cnt[a[x]] * les[x] - sum_les[a[x]]; cnt[a[x]]++; sum_les[a[x]] += les[x]; cur += (isLeft ? -1 : 1) * res; } void del(int x, bool isLeft) { cnt[a[x]]--; sum_les[a[x]] -= les[x]; ll res = 1LL * cnt[a[x]] * les[x] - sum_les[a[x]]; cur -= (isLeft ? -1 : 1) * res; } ll ans[500007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, m; cin >> n >> m; Fenwick<int> fw(n); for (int i = 1;i <= n;i++) { cin >> a[i]; les[i] = fw.query(a[i] - 1); fw.update(a[i], 1); } for (int i = 1;i <= m;i++) { int l, r; cin >> l >> r; Q[i] = { l,r,i }; } int B = n / sqrt(m); sort(Q + 1, Q + m + 1, [&](Query a, Query b) { if (a.l / B == b.l / B) return a.l / B & 1 ? a.r > b.r:a.r < b.r; else return a.l / B < b.l / B; }); int L = 1, R = 0; for (int i = 1;i <= m;i++) { auto [l, r, id] = Q[i]; while (L > l) add(--L, 1); while (R < r) add(++R, 0); while (L < l) del(L++, 1); while (R > r) del(R--, 0); ans[id] = cur; } for (int i = 1;i <= m;i++) cout << ans[i] << '\n'; return 0; }
B
题解
知识点:贪心,枚举,优先队列。
若 ,当存在一个位置大于等于 时,形成自环即可,逆序数为 ;否则所有位置一定小于 ,一定无解。接下来考虑 的情况。
我们先考虑全是正数的特殊情况,显然我们优先选择合法的连续区间 的元素循环左移一次构造大环,其他位置构成自环,这样的逆序数是 ,可以证明是理论最优的,从中去掉某些元素会导致花费增大。这样,我们枚举所有区间构造是 的,用尺取法是 的。
现在问题包含了负数,尺取法就失效了,因为区间的和不具备单调性,但我们依旧可以枚举所有区间。不过,此时对于一个区间,有些位置我们是不能选的,否则就不合法了,因此选择的位置不再连续。我们先考虑不连续的位置产生的逆序数的最小值。
若在 这个连续区间中选择了 个数,我们可以将选中的位置对应的数循环左移一次,那么对于环上的元素相互产生了 个逆序对,对于每个不在环上的元素都与环元素产生了 个逆序对,即没被选择的点都额外产生了一个逆序对,这样产生的逆序数为 ,可以证明是理论最优的。
结合上面的结论,我们知道对于一个固定区间 ,选的数越多越好,这样贪心选择的方式就顺其自然的出现了:正数都选,随后负数选最大的直到不能再选。
这里,区间端点原则上是一定要选的,不然求出的答案会比真实答案要大。但对于这种情况的真实答案,其等价于一个更小区间的真实答案,因此即使舍弃导致求出错误答案,也不会使得最终答案错误,所以为了实现方便就允许存在这样的操作。
直接枚举每个区间取数的复杂度是 的。我们发现,对于每一个右端点,左端点从右到左的时候,区间长度一直在增加,那么当没被选择的数严格减小时答案才会更优,因此之前选过的负数是一定会被选上,否则没被选择的数一定比之前更大,答案不会更优。因此,用优先队列维护没被选择的负数即可,选过的就不需要考虑了。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; int a[1000007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, k; cin >> n >> k; for (int i = 1;i <= n;i++) cin >> a[i]; if (k <= 0) { bool ok = 0; for (int i = 1;i <= n;i++) ok |= a[i] >= k; if (ok) cout << 0 << '\n'; else cout << -1 << '\n'; return 0; } int ans = 1e9; for (int i = 1;i <= n;i++) { ll sum = 0; priority_queue<int> pq; for (int j = i;j >= 1;j--) { if (a[j] >= 0) sum += a[j]; else pq.push(a[j]); while (pq.size() && pq.top() + sum >= k) { sum += pq.top(); pq.pop(); } if (sum >= k) { int sz = pq.size(); ans = min(ans, i - j + sz); } } } if (ans < 1e9) cout << ans << '\n'; else cout << -1 << '\n'; return 0; }
C
题解
知识点:图匹配,竞赛图。
将 的关系转化为 ,那么原图会变为一个竞赛图,而原图的匹配关系则变为竞赛图的若干条不相交的路径。
首先竞赛图一定有一条哈密顿路径,因此答案至少为 。
当且仅当每个点都在一个哈密顿回路时(每个点都在大于等于 强连通分量内),答案才为 。可以根据兰道定理:竞赛图强连通,当且仅当从小到大的度数序列的前缀和 严格大于 。
我们可以枚举度数序列,找到所有强连通分量。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; int deg[3007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; for (int i = 1;i <= n;i++) for (int j = 1;j <= n;j++) { int x; cin >> x; deg[i] += x; } // (x,y+n)看为(x,y)的边,可转化为竞赛图 // 竞赛图必有哈密顿路径答案至少为n - 1,当且仅当所有点可以在大于等于3的环中时答案为n /// 兰道定理:一个序列是竞赛图的分数序列当且仅当度数序列前缀和s[1,i]大于等于C(i,2),i=n时等于 /// 兰道定理:竞赛图强连通当且仅当度数序列前缀和s[1,i]严格大于C(i,2) sort(deg + 1, deg + n + 1); int s = 0; for (int i = 1, j = 0;i <= n;i++) { s += deg[i]; if (s == i * (i - 1) / 2) { // 此时不强连通 if (i - j <= 2) { // [j,i)的点是强连通的,若小于等于2则不能成环, cout << n - 1 << '\n'; return 0; } j = i; } } cout << n << '\n'; return 0; }
D
题解
知识点:因数集合。
由条件 ,我们枚举所有 的因数,可以求出 ,最后验证一下即可。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; void get_factor(int n, vector<int> &factor) { for (int i = 1;i * i <= n;i++) { if (!(n % i)) { factor.push_back(i); if (i != n / i) factor.push_back(n / i); } } } bool solve() { int k, c, n; cin >> k >> c >> n; vector<int> factor; get_factor(c, factor); int ans = 0; for (auto b : factor) { if ((c - b) % k) continue; int a = (c - b) / k; if (a <= 0 || gcd(a, b) < n) continue; ans++; } 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; }
E
题解
知识点:递归,分治。
不难发现区间的嵌套关系是一个树状结构,天然满足递归的性质,我们把每个不被约束的数字单独看作一个约束区间方便实现。
先将区间按左端点从小到大排序,右端点为第二关键字从大到小排序,保证第一个遇到的一定是嵌套区间最大的。
对于一个约束区间 ,分类讨论:
- 若没有子约束,若要求奇数,我们随便交换两个即可,否则不用交换。
- 若有子约束,统计子约束的影响,若和自己同奇偶则无需改动,否则随意选两个相邻约束区间,取左边的最大值和右边的最小值交换即可。
有线性的做法,需要处理下面两件事:
- 每次递归返回最后一个区间的下标,对于上一层递归就直接可以跳转到后面继续枚举区间。
- 将操作先记录(交换的两个数字,我们只需要数字),最后开一个数组 表示数字 在排列的位置,对其直接进行操作即可。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; int n, m; array<int, 3> seg[1007]; int p[1007], cnt; void dfs(int l, int r, int w = -1) { if (l == r) { p[l] = ++cnt; return; } int pos = l, wsum = 0; vector<pair<int, int>> sub_seg; for (int i = 1;i <= m;i++) { auto [l1, r1, w1] = seg[i]; if (pos <= l1 && r1 <= r && (w == -1 || r1 - l1 + 1 < r - l + 1)) { while (pos < l1) { dfs(pos, pos, -1); sub_seg.push_back({ pos,pos }); pos++; } sub_seg.push_back({ l1,r1 }); dfs(l1, r1, w1); pos = r1 + 1; wsum ^= w1; } } while (pos <= r) { dfs(pos, pos, -1); sub_seg.push_back({ pos,pos }); pos++; } if (w != -1 && w != wsum) { auto [l1, r1] = sub_seg[0]; auto [l2, r2] = sub_seg[1]; int x = find(p + l1, p + r1 + 1, r1) - p; int y = find(p + l2, p + r2 + 1, l2) - p; swap(p[x], p[y]); } } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for (int i = 1;i <= m;i++) { int l, r, w; cin >> l >> r >> w; seg[i] = { l,r,w }; if (l == r && w) { cout << -1 << '\n'; return 0; } } sort(seg + 1, seg + m + 1, [&](auto a, auto b) { return a[0] == b[0] ? a[1] > b[1]:a[0] < b[0];}); dfs(1, n); for (int i = 1;i <= n;i++) cout << p[i] << " \n"[i == n]; return 0; }
G
题解
知识点:双指针。
尺取法枚举区间即可。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; int a[100007]; int cnt[5]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n, k; cin >> n >> k; for (int i = 1;i <= n;i++) cin >> a[i]; int ans = 1e9; int l = 1, r = 1; while (l <= n) { while (r <= n) { if (cnt[1] && cnt[2] && cnt[3] && cnt[4] >= k) break; cnt[a[r]]++; r++; } if (cnt[1] && cnt[2] && cnt[3] && cnt[4] >= k) ans = min(ans, r - l); cnt[a[l]]--; l++; } cout << ans << '\n'; return 0; }
H
题解
知识点:贪心,背包dp。
注意到背包大小是递增的,因此我们考虑后 个背包即可。
一个显然的方法是,考虑了前 次,共处理前 个奶酪的最大价值。但这样,转移时我们要枚举最后一次处理了多少奶酪,并进行背包dp,复杂度是 的。
我们可以考虑预处理区间 且背包大小是 时的最大价值,复杂度是 的,而对于之前的转移复杂度就变为 了。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; int a[207], b[207]; int sz[100007]; ll f[207][207][207]; ll g[207][207]; 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 <= n;i++) cin >> a[i] >> b[i]; for (int i = 1;i <= m;i++) cin >> sz[i]; for (int i = 1;i <= n;i++) { for (int j = i;j <= n;j++) { for (int k = 0;k <= sz[m];k++) { f[i][j][k] = f[i][j - 1][k]; if (k >= a[j]) f[i][j][k] = max(f[i][j][k], f[i][j - 1][k - a[j]] + b[j]); } } } reverse(sz + 1, sz + m + 1); m = min(m, n); reverse(sz + 1, sz + m + 1); for (int i = 1;i <= m;i++) { for (int j = 0;j <= n;j++) { for (int k = 0;k <= j;k++) { g[i][j] = max(g[i][j], g[i - 1][j - k] + f[j - k + 1][j][sz[i]]); } } } cout << g[m][n] << '\n'; return 0; }
I
题解
知识点:位运算,线性dp。
先预处理前缀异或和方便处理,并考虑按位计算贡献。
我们先考虑一个简单的问题,求出所有子区间的异或和的和,与蓝桥杯省赛的一道题完全一致。朴素求所有子区间,最暴力的就是 选完再求,稍微优化一点是 枚举右端点左端点可以递推,但实际左端点的递推过程也可以 解决,因此最佳复杂度就是 的,具体解法如下。
枚举固定的右端点 ,若一个位置 使得 的异或和第 位和 的异或和第 位不同,那么 的异或和第 位为 ,就会在以 为右端点的这个状态产生 的贡献。
因此,我们考虑维护 所有前缀(可以为空)异或和第 位的 个数 ,便可轻松获得前缀与 的异或和第 位不同的个数。例如 的异或和第 位为 ,那么贡献就是 。最后,把 的异或和的每位情况加到 即可。此时,我们可以得到以 为右端点的所有子区间的异或和。
对于这个简单问题,我们直接加到答案里即可。但为了原来问题的处理,我们考虑求 表示右端点小于等于 的所有子区间的异或和之和。这个只需要求出以 为右端点的所有子区间的异或和后保存在 ,最后枚举完所有右端点后做前缀和即可。
回到原来的问题,发现其实就是分段求异或和再乘起来,求所有情况的和。问题与 子段问题非常相似,考虑线性dp。设 表示分了 段,且所有区间右端点小于等于 时的答案,状态顺序交换也是可以写的,不过会麻烦一点。 时, 即上述简单问题最终答案,其实之后的递推思路是完全类同的。
当分了 段时,枚举第 段固定的右端点 ,若一个位置 使得 的异或和第 位和 的异或和第 位不同,那么 的异或和第 位为 ,就会在分了 段最后一段以 为右端点的这个状态(不是 )产生 的贡献。
因此,我们考虑维护 所有前缀(可以为空)分了 段且异或和的第 位为 时的总贡献 ,便可轻松获得前缀与 的异或和第 位不同时,前缀分了 段的总贡献。例如 的异或和第 位为 ,那么贡献就是 。最后,根据 的异或和每位情况,将 加到 即可。此时,我们可以得到分了 段,且最后一段以 为右端点的答案。我们对其做前缀和即可得到 。
这类问题最关键的是,枚举右端点时,将左端点的 递推,根据异或的性质优化为对每位贡献的 求和,即拆位求和的优化。拆位求和的本质就是,将区间异或和按位拆成若干个代表位权的数之和,并对所有区间异或和拆开后的数按位分组考虑,就非常方便位运算相关判断了。
这里分段之间的运算是乘法,具有分配律,因此拆位后每位都需要乘上前面的答案的和。如果换成加减法,那运算就完全不一样了,因为具有交换律,当前段的贡献与之前段的答案没有任何关联,最终将之前的答案和当前段的贡献加在一起即可。对于其他的运算符也可以类似考虑,乘法其实是比较好处理的一种运算符。
时间复杂度
空间复杂度
代码
#include <bits/stdc++.h> using namespace std; using ll = long long; const int P = 998244353; int a[200007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; for (int i = 1;i <= n;i++) cin >> a[i], a[i] ^= a[i - 1]; vector<int> f(n + 1, 1); for (int i = 1;i <= 3;i++) { vector<int> g(n + 1); array<array<int, 2>, 30> sum{}; for (int j = 0;j <= n;j++) { for (int k = 0;k < 30;k++) { (g[j] += (1LL << k) * sum[k][~(a[j] >> k) & 1] % P) %= P; (sum[k][(a[j] >> k) & 1] += f[j]) %= P; } } for (int j = 1;j <= n;j++) (g[j] += g[j - 1]) %= P; swap(f, g); } cout << f[n] << '\n'; return 0; }
本文来自博客园,作者:空白菌,转载请注明原文链接:https://www.cnblogs.com/BlankYang/p/17601662.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2022-08-02 蔚来杯2022牛客暑期多校训练营5 ABCDFGHK