CSUACM2024新生赛 - 第2场 题解
写在前面
比赛地址:https://www.luogu.com.cn/contest/213194。
难度大跳水!(大概
描述了获奖条件以及奖品的字符串为:本次新生赛的第1名,以及排名大于等于3小于10的,截止新生赛开始前,获得积分题的积分数量最多的同学,将获得ICPC2024杭州的纪念背包一个
。
A 线段树套平衡树
官方题解
诈骗题,在题目背景也有加粗提示注意数据范围。
对于 1 操作直接修改当前数组即可。
对于其余操作复制当前数组对应区间并排序即可轻松实现。
复杂度 。
当然有能力的同学可以逝逝线段树套平衡树。
有趣的是我的队友lmx给我验数据时,真的弄了个树套树,然而在过于小的范围下树套树对比暴力的时间并没有快多少。
复制复制// Coded by hjxddl #include <bits/stdc++.h>//普通暴力写法 #define ll long long #define db double void solve() { int n; std::cin >> n; std::vector<int> a(n + 5); for (int i = 1; i <= n; i++) std::cin >> a[i]; int q; std::cin >> q; while (q--) { int opt; std::cin >> opt; if (opt == 1) { int k, x; std::cin >> k >> x; a[k] = x;//对于1操作直接修改 continue; } int k, l, r; std::cin >> k >> l >> r; std::vector<int> b{0}; for (int i = l; i <= r; i++) b.push_back(a[i]); int m = b.size(); std::sort(b.begin() + 1, b.begin() + m);//复制一份数组并排序 b.push_back(1e9 + 7); if (opt == 2)//2操作直接输出即可 std::cout << b[k] << '\n'; else { int rank; for (int i = 1; i <= m; i++) { if (k > b[i - 1] && k <= b[i]) { rank = i;//找出k的排名,可以使用二分 break; } } if (opt == 3) { std::cout << rank << '\n'; } if (opt == 4) { if (rank == 1) std::cout << (int)1e9 + 7 << '\n'; else std::cout << b[rank - 1] << '\n'; } if (opt == 5) { while (b[rank] == b[rank + 1]) rank++; if (rank == m) std::cout << (int)1e9 + 7 << '\n'; else if (b[rank] == k)//找出k的排名时,b[rank]可能等于k或第一个比k大的数 std::cout << b[rank + 1] << '\n';//因此要对后继进行特判. else std::cout << b[rank] << '\n'; } } } } int main() { std::ios::sync_with_stdio(0); std::cin.tie(0), std::cout.tie(0); int t=1; // std::cin>>t; while(t--){ solve(); } std::cout << std::flush; system("pause"); }
做法
因为这是一道允许各种方法过的签到,所以出题人写的题解有点潦草。注意一下实现即可做到所有询问的时间复杂度均为线性级别。
众所周知 STL 中有 nth_element
函数,可以在线性时间复杂度内求得给定数组的第 小值并将其放到数组的下标为 的位置上,同时将前 小值放在其左侧,其他值放在其右侧(但不保证这些值之间是有序的)。
则对于操作 2,直接调用 nth_element
求第 小即可;
对于操作 3,枚举求小于给定值的数的数量即可;
对于操作 4,枚举求小于给定值的数的最大值即可;
对于操作 5,枚举求大于给定值的数的最小值即可。
单次询问时间复杂度均为线性级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1010; const int kInf = 1000000007; //============================================================= int n, q, a[kN]; std::vector<int> b; //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; std::cin >> q; while (q --) { int opt; std::cin >> opt; if (opt == 1) { int x, k; std::cin >> x >> k; a[x] = k; } else if (opt == 2) { int k, l, r; std::cin >> k >> l >> r; b.clear(); for (int i = l; i <= r; ++ i) b.push_back(a[i]); std::nth_element(b.begin(), b.begin() + k - 1, b.end()); std::cout << b[k - 1] << "\n"; } else if (opt == 3) { int k, l, r, rk = 1; std::cin >> k >> l >> r; for (int i = l; i <= r; ++ i) if (a[i] < k) ++ rk; std::cout << rk << "\n"; } else if (opt == 4) { int k, l, r, ans = -kInf; std::cin >> k >> l >> r; for (int i = l; i <= r; ++ i) if (a[i] < k && a[i] > ans) ans = a[i]; if (ans == -kInf) ans = kInf; std::cout << ans << "\n"; } else { int k, l, r, ans = kInf; std::cin >> k >> l >> r; for (int i = l; i <= r; ++ i) if (a[i] > k && a[i] < ans) ans = a[i]; std::cout << ans << "\n"; } } return 0; }
线段树套平衡树
#include <algorithm> #include <iostream> #include <fstream> #include <cstring> #include <cstdlib> #include <complex> #include <string> #include <cstdio> #include <vector> #include <bitset> #include <cmath> #include <ctime> #include <queue> #include <stack> #include <map> #include <set> using namespace std; const int MAXN = 1e5 + 100; int n, m; int a[MAXN]; namespace Treap { struct balanced { int w; int sz; int num; int fix; int ch[2]; }; int tot; balanced tree[MAXN * 20]; int newnode(int w) { ++tot; tree[tot].w = w; tree[tot].fix = rand(); tree[tot].num = 1; tree[tot].ch[0] = tree[tot].ch[1] = 0; tree[tot].sz = 1; return tot; } void pushup(int p) { tree[p].sz = tree[tree[p].ch[0]].sz + tree[tree[p].ch[1]].sz + tree[p].num; } void rotate(int& p, int d) { int y = tree[p].ch[d]; tree[p].ch[d] = tree[y].ch[d ^ 1]; tree[y].ch[d ^ 1] = p; pushup(p); pushup(y); p = y; } void insert(int& p, int w) { if (!p) p = newnode(w); else if (tree[p].w == w) ++tree[p].num; else { if (tree[p].w > w) { insert(tree[p].ch[0], w); if (tree[tree[p].ch[0]].fix > tree[p].fix) rotate(p, 0); } else { insert(tree[p].ch[1], w); if (tree[tree[p].ch[1]].fix > tree[p].fix) rotate(p, 1); } } pushup(p); } void remove(int& p, int w) { if (tree[p].w > w) remove(tree[p].ch[0], w); else if (tree[p].w < w) remove(tree[p].ch[1], w); else { if (tree[p].num > 1) --tree[p].num; else { if (!tree[p].ch[0] && !tree[p].ch[1]) p = 0; else if (!tree[p].ch[0]) { rotate(p, 1); remove(tree[p].ch[0], w); } else if (!tree[p].ch[1]) { rotate(p, 0); remove(tree[p].ch[1], w); } else { if (tree[tree[p].ch[0]].fix > tree[tree[p].ch[1]].fix) { rotate(p, 0); remove(tree[p].ch[1], w); } else { rotate(p, 1); remove(tree[p].ch[0], w); } } } } if (p) pushup(p); } int queryrank(int p, int k) // return the highest rank of value 'k' { if (!p) return 0; if (tree[p].w > k) return queryrank(tree[p].ch[0], k); else if (tree[p].w == k) return tree[tree[p].ch[0]].sz; else return tree[tree[p].ch[0]].sz + tree[p].num + queryrank(tree[p].ch[1], k); } int querynum(int p, int k) // return the value of kth rank node { if (tree[tree[p].ch[0]].sz + 1 == k) return tree[p].w; else if (tree[tree[p].ch[0]].sz + 1 < k) return querynum(tree[p].ch[1], k - 1 - tree[tree[p].ch[0]].sz); else return querynum(tree[p].ch[0], k); } int querypre(int p, int k) // return the prefix of value k { if (!p) return -1000000007; if (tree[p].w >= k) return querypre(tree[p].ch[0], k); else return max(tree[p].w, querypre(tree[p].ch[1], k)); } int querysuf(int p, int k) // return the suffix of value k { if (!p) return 1000000007; if (tree[p].w <= k) return querysuf(tree[p].ch[1], k); else return min(tree[p].w, querysuf(tree[p].ch[0], k)); } void listall(int p) { if (tree[p].ch[0]) listall(tree[p].ch[0]); cerr << tree[p].w << ",sz=" << tree[p].num << " "; if (tree[p].ch[1]) listall(tree[p].ch[1]); } } using Treap::listall; namespace SEG { struct segment { int l; int r; int root; }; segment tree[MAXN * 8]; void build(int p, int l, int r) { tree[p].l = l; tree[p].r = r; for (int i = l; i < r + 1; ++i) Treap::insert(tree[p].root, a[i]); if (l != r) { int mid = (l + r) / 2; build(p * 2, l, mid); build(p * 2 + 1, mid + 1, r); } } void modify(int p, int x, int y) { Treap::remove(tree[p].root, a[x]); Treap::insert(tree[p].root, y); if (tree[p].l == tree[p].r) return; int mid = (tree[p].l + tree[p].r) / 2; if (x > mid) modify(p * 2 + 1, x, y); else modify(p * 2, x, y); } int queryrank(int p, int l, int r, int k) // query the highest rank of value 'k' { if (tree[p].l > r || tree[p].r < l) return 0; if (tree[p].l >= l && tree[p].r <= r) return Treap::queryrank(tree[p].root, k); else return queryrank(p * 2, l, r, k) + queryrank(p * 2 + 1, l, r, k); } int querynum(int u, int v, int k) // query the value of kth num { int l = 0, r = 1e8; while (l < r) { int mid = (l + r + 1) / 2; if (queryrank(1, u, v, mid) < k) l = mid; else r = mid - 1; } return r; } int querypre(int p, int l, int r, int k) { if (tree[p].l > r || tree[p].r < l) return -1000000007; if (tree[p].l >= l && tree[p].r <= r) return Treap::querypre(tree[p].root, k); else return max(querypre(p * 2, l, r, k), querypre(p * 2 + 1, l, r, k)); } int querysuf(int p, int l, int r, int k) { if (tree[p].l > r || tree[p].r < l) return 1000000007; if (tree[p].l >= l && tree[p].r <= r) return Treap::querysuf(tree[p].root, k); else return min(querysuf(p * 2, l, r, k), querysuf(p * 2 + 1, l, r, k)); } } int read() { char ch = getchar(); int x = 0, flag = 1; while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if (ch == '-') { ch = getchar(); flag = -1; } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * flag; } int main() { n = read(); for (int i = 1; i < n + 1; ++i) a[i] = read(); SEG::build(1, 1, n); m = read(); for (int i = 0; i < m; ++i) { int opt = read(); if (opt == 1) { int x = read(), y = read(); SEG::modify(1, x, y); a[x] = y; } else { int k = read(), l = read(), r = read(); if (opt == 3) printf("%d\n", SEG::queryrank(1, l, r, k) + 1); else if (opt == 2) printf("%d\n", SEG::querynum(l, r, k)); else if (opt == 4) printf("%d\n", abs(SEG::querypre(1, l, r, k))); else printf("%d\n", abs(SEG::querysuf(1, l, r, k))); } } return 0; }
B 我是一个一个一个一个签到题
发现在进行异或运算时,被操作的两个数的各个二进制位可以看做相互独立的。于是考虑对 拆位并依次尝试最小化答案的每一位,从而最小化最终的答案。
容易发现对于某一二进制位,当且仅当 该位上全为 1 或全为 0 时,答案中该位上可以取 0,否则最优情况下只能取 1。
于是直接枚举各位并构造答案即可,总时间复杂度 级别。
/* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long //============================================================= //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { int a, b, c, ans = 0; std::cin >> a >> b >> c; for (int i = 0; i <= 30; ++ i) { if ((a >> i & 1) && (b >> i & 1) && (c >> i & 1)) continue; if (!(a >> i & 1) && !(b >> i & 1) && !(c >> i & 1)) continue; ans += (1 << i); } std::cout << ans << "\n"; } return 0; }
C 不要再读假题了
是一个前缀和优化计数问题。
考虑暴力枚举每一行的 并用 数组记录下来对应的位置。
再考虑暴力枚举每一行的 , 加上对应 数组记录下来对应的位置的值。
每次枚举下一行时对 都不需要清空刚好进行了前缀和优化。
所以总复杂度为 。
#include <bits/stdc++.h> #define int long long #define endl '\n' using namespace std; char mp[409][409]; int sum[409][409]; signed main() { int n, m; cin >> n >> m; for (int i = 1;i <= n;i++) { for (int j = 1;j <= m;j++) { cin >> mp[i][j]; } } int ans = 0; for (int i = 1;i <= n;i++) { for (int l = 1;l <= m;l++) { for (int r = l + 1;r <= m;r++) { if (mp[i][l] == 'p' && mp[i][r] == 'c') { ans += sum[l][r]; } } } for (int l = 1;l <= m;l++) { for (int r = l + 1;r <= m;r++) { if (mp[i][l] == 'c' && mp[i][r] == 'c') { sum[l][r]++; } } } } cout << ans << endl; return 0; }
D A+B+C+D Problem
于是我们考虑优化,不难想到利用二分优化,我们可以进行两次二分。
第一次是思想上的,就是把 ,转化为 。
第二次是真正的二分,我们先用 存下来 的所有情况,用 存下来 的所有情况。枚举 中的每一个值,用二分查找在 中找所有满足和为 的数的个数,求和再输出就可以啦。
使用了来简化代码实现。
STL map 写法
#include <bits/stdc++.h> //#define int long long #define endl '\n' using namespace std; int a[5][1009]; map<int, int>z; signed main(){ int n; cin >> n; for (int i = 1;i <= 4;i++) { for (int j = 1;j <= n;j++) { cin >> a[i][j]; } } for (int i = 1;i <= n;i++) { for (int j = 1;j <= n;j++) { z[a[1][i] + a[2][j]]++; } } int ans = 0; for (int i = 1;i <= n;i++) { for (int j = 1;j <= n;j++) { ans += z[-a[3][i] - a[4][j]]; } } cout << ans << endl; return 0; }
二分写法
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e3 + 10; //============================================================= int n, a[kN], b[kN]; std::vector<int> c1, c2; std::map<int, int> cnt1, cnt2; //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; for (int i = 1; i <= n; ++ i) { std::cin >> b[i]; for (int j = 1; j <= n; ++ j) { c1.push_back(b[i] + a[j]); } } for (int i = 1; i <= n; ++ i) std::cin >> a[i]; for (int i = 1; i <= n; ++ i) { std::cin >> b[i]; for (int j = 1; j <= n; ++ j) { c2.push_back(b[i] + a[j]); } } std::sort(c1.begin(), c1.end()); std::sort(c2.begin(), c2.end()); LL ans = 0; for (auto v: c1) { auto it1 = std::lower_bound(c2.begin(), c2.end(), -v); if (*it1 != -v) continue; auto it2 = std::lower_bound(c2.begin(), c2.end(), -v + 1); ans += it2 - it1; } std::cout << ans << '\n'; return 0; }
E 喜欢加强数据是吧
题解
当 且 为合数时, 的两个因子必然存在于 到 中(因子 相同时, , 必然存在于 到 中)所以此时答案为 YES。
当 为质数时,很显然 到 不会出现 所以此时答案为 NO。
注意 需要特判。
证明见下。
于是仅需判断输入的数是否为 4,已经是否为素数即可。数据范围较小,可以每组数据均 地进行判断。
顺带一提 std 写的是 Miller-Rabin 算法,本质上是一种随机化算法,可以在 的时间复杂度下快速判断出一个数是否是素数,但具有一定的错误概率。不过在一定数据范围内,通过一些技巧可以使其不出错。因为太变态了所以不放在这里了,感兴趣的同学可以自行学习。
#include <bits/stdc++.h> using namespace std; void solve() { int n; cin >> n; if (n == 1) { cout << "YES" << endl; } else if (n == 4){ cout << "NO" << endl; } else { bool x = 0; for (int i = 2; i * i <= n; i++) { if (n % i == 0) { x = 1; break; } } if (x) cout << "YES" << endl; else cout << "NO" << endl; } } signed main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) solve(); } /* 5 1 2 4 6 20 */
证明
当 是素数时, 显然 NO.
当 且 不是素数时, 存在 使得
- 若 , 有:
- 若 , 则 , 于是有
然后只剩下 的情况, 手玩即可。
PS:这就是威尔逊定理:
F 努力の江队
题意分析
本题的数列为 $a_1=1,\ a_i\ge1(1\le i\le t),\quad \frac{a_{k+1}}{a_k}\in\left{
\right. $
其中,题目提供 条情报,每条情报提供 的信息。
最后,题目要求根据已有信息,构造出数列在满足给定条件下可能达到的最大值 ,输出 。
题目解法
判断情报真假
题目中,有默认的 信息,记得将其补充。然后将情报数据 按 从小到大排序。
根据题意,数列先是经历持平或倍增的不下降的第一阶段,然后再经历持平或减半的不上升的第二阶段。因此,当数列开始出现减小后,就无法再上升。于是,若给定情报出现减小后又增大的情况,则情报虚假。
此外,在给定的两组情报之间,两者的数值差距必须能被两者的距离容忍。若情报为 时, 没有给定。此时, 可以等于 或 。因此 ,而 ,最大为 ,故本情报虚假。第二阶段的不上升区间同理。于是,当 时,情报虚假。
综上,当满足以下条件之一,则情报虚假:
- 出现减小后又增大的情况;
- 存在 。
构造可能达到的最大值
首先,最终的答案值一定不会小于情报给定值的最大值。
因此,我们只需要关注情报给定的最大值,研究其周围的空位是否能让我们填入尽量大的数值。
在如图所示的数列中,给定的最大值是 ,共有三个(用红色字体标注)。于是,我们需要在 相邻的未知区域(用浅红底色填充)填数,构造最大值。
在左边的 与 之间的问号,与最大值 无关,无需关注。在红底填充的四段区域中,第一个 与第二个 中间隔着三个空,是空隙最大的。可以填充为 ,于是最大值为 ,输出值为 。
代码
由于数列元素只会比上一个元素增大两倍或减半,且最小为 ,则数列每个元素都是二的若干整数次方 。又因为题目要求输出数列最大值的对数,于是我们不需要记录数列原始值,只需要记录其幂值即可。
如数列 ,记录为 。这样能够保证数据能够被正常存储不越界。
#include<bits/stdc++.h> #define LL long long using namespace std; const int N = 1e5+6; LL t,m; struct D{ LL p,q; }dat[N]; bool cmp(D a,D b){ return a.p<b.p; } int main(){ ios::sync_with_stdio(false); cin>>t>>m; LL mx=0; dat[0].p=1,dat[0].q=0; // a[1]=2^0=1 for(LL i=1;i<=m;++i){ cin>>dat[i].p>>dat[i].q; mx=max(mx,dat[i].q); } sort(dat+1,dat+1+m,cmp); LL down=0, res=mx; for(LL i=1;i<=m;++i){ LL df=abs(dat[i].q-dat[i-1].q), len=dat[i].p-dat[i-1].p; if(df>len){ cout<<"I love Yuhan\n"; return 0; } if(dat[i].q>dat[i-1].q){ if(down){ cout<<"I love Yuhan\n"; return 0; } } if(dat[i].q<dat[i-1].q){ down=dat[i].p; // 记录开始下降 } if(dat[i].q==mx){ len-=df; // 将左右两数持平后再计算(类似倍增时的思路) LL r=dat[i].q+len/2; res=max(res,r); } else if(dat[i-1].q==mx){ len-=df; // 同上 LL r=dat[i-1].q+len/2; res=max(res,r); } } if(!down){ // 若最后一个情报也是最大值,补充判断 res=max(res,dat[m].q+(t-dat[m].p-1)); } cout<<res<<'\n'; }
G 我是一个动态规划?
分析
答案具有单调性。如果无法在 次修改内做到 ,那么肯定也无法在 次修改内做到。考虑二分答案。
如何判断当前答案 是否可行?考虑 。
状态:
表示前 个元素中,不修改 这个元素的条件下,使数列符合要求的最小修改次数。
答案:
由于不确定保留哪个元素更优,所以枚举保留位置 ,取 。即取前 个数的最优方案,然后将后面的数全部修改。
你可能想问:将后面所有数全改了肯定不优啊?
因为 后面的数不归 管,等我们枚举到后面的 值,它会帮我们保留后面的数的。这样就可以知道后面的数如果不全部更改,是否更优了。还不懂的话可以拿样例三试一试。
状态转移:
首先枚举当前状态 ,我们考虑枚举最后修改的那一段位置(可以不修改)。所以枚举上一个决策 (上一个保留的位置), 就是最后一段修改的位置。显然 和 都不能修改。但如果 的话, 和 必须修改,所以我们要判断 是否合法。
确定了 和 ,还是一样的,前 个数取最优方案,保留 。然后将 到 这一段全部修改,最后保留 。
初始化:
把 这一段全修改了一定能达成目标。
如果对于一个 ,没有合法的 ,那么也只能这样。
初始化即 。
#include <bits/stdc++.h> #define int long long #define endl '\n' using namespace std; const int N = 2005; int n, k, dp[N], a[N]; bool check(int x){ for (int i = 1; i <= n; i++) { dp[i] = i - 1; } for (int i = 1; i <= n; i++) { for (int j = 1; j < i; j++) { if (abs(a[i] - a[j]) <= (i - j) * x) { dp[i] = min(dp[i], dp[j] + i - j - 1); } } } for (int i = 1; i <= n; i++) { if (n - i + dp[i] <= k) { return 1; } } return 0; } signed main(){ cin >> n >> k; for (int i = 1; i <= n; i++) { cin >> a[i]; } int l = -1, r = 2e9 + 1; while (l + 1 < r){ int mid = l + r >> 1; if (check(mid)) r = mid; else l = mid; } cout << r << endl; return 0; }
写在最后
看完了第七卷没有败犬女主看我要死了啊啊啊啊
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧