2024“钉耙编程”中国大学生算法设计超级联赛(3)
2024“钉耙编程”中国大学生算法设计超级联赛(3)
深度自同构
思路
不太会推,赛时队友出的,找到的规律就是 \(f_i\) 等于 \(i\) 的所有因子数的 \(f_d\)。
先考虑 \(n\) 个点的合法的树的个数,容易发现根据要求每个节点的所有 子树的形态必定完全相同。因此可以递推,令 \(f_i\) 表示 \(i\) 个点的合法的树的 个数,枚举根的儿子个数,有 \(f_i=\sum\limits_{d|(i-1)}f_d\)。这一转移可以用枚举倍数的 方法加速,复杂度为调和级数。
再考虑合法的森林个数,注意到森林中每棵树必定完全相同,因此 \(ans_i = ∑\limits_{ d|i} f_d\),再用调和级数的复杂度算一遍即可,复杂度 \(O(n log n)\) 。
代码
#include <bits/stdc++.h> using namespace std; using i64 = long long; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; const int mod = 998244353; n ++; vector<int> f(n + 1); f[1] = 1; for (int i = 1; i <= n; i ++) { for (int j = i + 1; j <= n; j += i) { (f[j] += f[i]) %= mod; } } for (int i = 2; i <= n; i ++) cout << f[i] << " \n"[i == n]; return 0; }
单峰数列
思路
难调的线段树。。
只需要维护区间最大值,最小值,以及升降序即可,判断相同用看区间最大与最小是否相同即可,判断单峰需要二分找到区间最大值的位置,从这个位置判断左边是否升,右边是否降即可。
代码
#include <bits/stdc++.h> using namespace std; using i64 = long long; template<class Node> struct SegmentTree { #define lc u<<1 #define rc u<<1|1 const int n, N; vector<Node> tr; SegmentTree(): n(0) {} SegmentTree(int n_): n(n_), N(n * 4 + 10) { tr.reserve(N); tr.resize(N); } SegmentTree(vector<int> init) : SegmentTree(init.size()) { function<void(int, int, int)> build = [&](int u, int l, int r) { tr[u].l = l, tr[u].r = r; init_lazy(tr[u]); if (l == r) { tr[u] = {l, r, 0, init[l], init[l], 1, 1}; return ; } i64 mid = (l + r) >> 1; build(lc, l, mid); build(rc, mid + 1, r); pushup(tr[u], tr[lc], tr[rc]); }; build(1, 1, n); } void cal_lazy(Node & fa, Node & ch) { i64 b = fa.add; ch.Max += b; ch.Min += b; } void tag_union(Node& fa, Node& ch) { i64 b = fa.add; ch.add += b; } void init_lazy(Node& u) { u.add = 0; } void pushdown(i64 u) { if (tr[u].add != 0) { cal_lazy(tr[u], tr[lc]); cal_lazy(tr[u], tr[rc]); tag_union(tr[u], tr[lc]); tag_union(tr[u], tr[rc]); init_lazy(tr[u]); } } void pushup(Node& U, Node& L, Node& R) { //上传 U.Max = max(L.Max, R.Max); U.Min = min(L.Min, R.Min); if (L.Max < R.Min && L.up && R.up) { U.up = 1; } else { U.up = 0; } if (L.Min > R.Max && L.down && R.down) { U.down = 1; } else { U.down = 0; } } void modify(int u, int l, int r, int k) { if (tr[u].l >= l && tr[u].r <= r) { tr[u].add += k; tr[u].Max += k; tr[u].Min += k; return ; } pushdown(u); int mid = (tr[u].l + tr[u].r) >> 1; if (l <= mid) modify(lc, l, r, k); if (r > mid) modify(rc, l, r, k); pushup(tr[u], tr[lc], tr[rc]); } Node query(int u, int l, int r) { //区查 if (l <= tr[u].l && tr[u].r <= r) return tr[u]; i64 mid = tr[u].l + tr[u].r >> 1; pushdown(u); i64 res = LLONG_MIN >> 1; if (r <= mid) return query(lc, l, r); if (l > mid) return query(rc, l, r); Node U; Node L = query(lc, l, r), R = query(rc, l, r); pushup(U, L, R); return U; } }; struct Node { //线段树定义 i64 l, r, add; i64 Max, Min; bool up, down; }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; i ++) cin >> a[i]; SegmentTree<Node> A(a); int m; cin >> m; while (m--) { int op, l, r; cin >> op >> l >> r; if (op == 1) { int x; cin >> x; A.modify(1, l, r, x); } else if (op == 2) { auto res = A.query(1, l, r); cout << (res.Max == res.Min) << '\n'; } else if (op == 3) { auto res = A.query(1, l, r); cout << res.up << '\n'; } else if (op == 4) { auto res = A.query(1, l, r); cout << res.down << '\n'; } else { int L = l + 1, R = r - 1, ans = -1; auto ma = A.query(1, l + 1, r - 1).Max; while (L <= R) { int mid = L + R >> 1; if (A.query(1, l, mid).Max >= ma) R = mid - 1, ans = mid; else L = mid + 1; } auto Lans = A.query(1, l, ans), Rans = A.query(1, ans, r); if (ans != -1 && Lans.up && Rans.down) { cout << 1 << '\n'; } else { cout << 0 << '\n'; } } } return 0; }
比特跳跃
思路
考虑到 \(x|y\) 的性质,即 \(x|y ≥\max(x,y)\),所以要使得权值最小,应该尽量从 \(y\) 的子集转移过来,这样就有 \(x|y = y[x\subseteq S_y]\) 其中 \(S\) 是 \(y\) 的子集集合,倘若不是连通图,那么只有从 \(1\) 转移过去时才能使权值最小。
这里 dijkstra 与普通的不一样,因为可能存在多个连通块,所以需要对多个连通块跑最短路,中间需要枚举子集优化,优化完后再跑一次 dijkstra 更新其他最短距离。
枚举子集的方法也有叫 \(SOSdp\),推荐博客:
枚举所有集合的子集(红皮) - pechpo - 博客园 (cnblogs.com)
「学习笔记」SOS DP - cyl06 - 博客园 (cnblogs.com)
代码
#include <bits/stdc++.h> using namespace std; using i64 = long long; struct DIJ { using i64 = long long; using PII = pair<i64, i64>; vector<i64> dis; vector<vector<PII>> G; DIJ() {} DIJ(int n) { dis.assign(n + 1, 1e18); G.resize(n + 1); } void add(int u, int v, int w) { G[u].emplace_back(v, w); } void dijkstra() { priority_queue<PII> que; for (int i = 1; i < dis.size(); i ++) { if (dis[i] != 1e18) { que.push({dis[i], i}); } } while (!que.empty()) { auto p = que.top(); que.pop(); int u = p.second; if (dis[u] < -p.first) continue; for (auto [v, w] : G[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; que.push({ -dis[v], v}); } } } } }; void solve() { int n, m, k; cin >> n >> m >> k; DIJ dij(n); for (int i = 0; i < m; i ++) { int u, v, w; cin >> u >> v >> w; dij.add(u, v, w); dij.add(v, u, w); } dij.dis[1] = 0; dij.dijkstra(); for (int i = 2; i <= n; i ++) { auto& d = dij.dis[i]; d = min(d, 1ll * k * (1 | i)); for (auto s = (i - 1)&i; s; s = (s - 1)&i) { d = min(d, dij.dis[s] + 1ll * i * k); } } dij.dijkstra(); for (int i = 2; i <= n; i ++) cout << dij.dis[i] << " \n"[i == n]; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
抓拍
思路
当存在有人往回走的时候,这个时候周长会变小,到达一定界限后,这个周长又会变大,所以用坐标轴表示 周长-时间 的曲线的话是个很显然的单峰函数,而对于单峰函数要找极值就需要用到三分了。
代码
#include <bits/stdc++.h> using namespace std; using i64 = long long; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<tuple<i64, i64, char>> a(n); for (auto &[x, y, fx] : a) { cin >> x >> y >> fx; } auto check = [&](int t)->i64{ array<i64, 2> l{LLONG_MAX >> 1, LLONG_MIN >> 1}, r{LLONG_MIN >> 1, LLONG_MAX >> 1}; auto A = a; for (int i = 0; i < n; i ++) { auto &[x, y, fx] = A[i]; if (fx == 'E') { x += t; } else if (fx == 'W') { x -= t; } else if (fx == 'S') { y -= t; } else { y += t; } l[0] = min(l[0], x), l[1] = max(l[1], y); r[0] = max(r[0], x), r[1] = min(r[1], y); } return 2 * (abs(l[0] - r[0]) + abs(l[1] - r[1])); }; i64 l = 0, r = 1e15; while (l < r) { i64 mid = l + (r - l) / 3; i64 midmid = r - (r - l) / 3; i64 val = check(mid), valval = check(midmid); if (valval > val) { r = midmid - 1; } else l = mid + 1; } cout << check(l) << "\n"; return 0; }
死亡之组
思路
分类讨论,首先把 \(a_1\) 从集合中去掉:
如果 \(a_1 ≥ L\),那么选最小的三个。
如果 \(a_1 < L\),那么选最大的,和最小的两个。
如果上述方案依然符合死亡之组的条件那么无解,否则有解。
代码
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, l, d; cin >> n >> l >> d; vector<int> a(n + 1); for (int i = 1; i <= n; i ++) cin >> a[i]; bool f = a[1] >= l; sort(a.begin() + 2, a.end()); if (f && (a[4] >= l || max({a[1], a[2], a[3], a[4]}) - min({a[1], a[2], a[3], a[4]}) <= d)) { cout << "No\n"; return ; } if (!f && (a[n] >= l && a[3] >= l || max({a[1], a[2], a[3], a[n]}) - min({a[1], a[2], a[3], a[n]}) <= d)) { cout << "No\n"; return ; } cout << "Yes\n"; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
本文作者:Ke_scholar
本文链接:https://www.cnblogs.com/Kescholar/p/18342103
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步