2024“钉耙编程”中国大学生算法设计超级联赛(3)§
深度自同构§
显然,深度自同构的森林的各个树应该完全相同,各个树的子树同理。
可以使用
DP。设 \(dp_i\) 为有
i个节点的深度自同构森林的数量。考虑如何状态转移。
对于一棵有跟树来说,假如删去根节点,这颗树就自然转化成了一个森林。
对于一个森林来说,加上一个根节点,这个森林就转化成了一棵树。
由于深度自同构必须满足子树完全相同,这就意味着对于一个 \(dp_i\) ,可以给这个森林加上一个根节点。
用 \(dp_i\) 来递推所有大小是
i + 1倍数的新的 \(dp_{k(i+1)},(k\in (1,2,...))\) 。相当于说用这个森林构成一棵树,用不同数量的这种树再构成新的森林。
显然这会涵盖所有情况。
注意,\(dp_i\) 要从
0开始, \(dp_0=1\)。#include <bits/stdc++.h> constexpr int mod = 998244353; int main() { int n; std::cin >> n; std::vector<int> dp(n + 1, 0); dp[0] = 1; for (int i = 0; i <= n; i++) { for (int j = i + 1; j <= n; j += (i + 1)) { dp[j] += dp[i]; dp[j] %= mod; } if (i) std::cout << dp[i] << ' '; } return 0; }
旅行§
使用了启发式合并来优化树形
DP。这里我们需要设计两个
DP数组。\(f_{u,c}\) 表示以
u节点为根的子树假如祖先有类型为c的节点和它配对的话能提供的最大价值。\(g_u\) 表示以
u节点为根的子树该配对的都配对好了的最大价值,也就是答案数组。那么如何进行状态转移呢?
首先可以想到一个 \(O(n^2)\) 的转移方式。
\(f_{u,v} \gets \sum_{v \in son_u}^{} g_v + \max_{v\in son_u}(f_{v,c}-g_v)\)
\(\begin{cases} g_u \gets \sum_{v \in son_u} g_v \\ g_u \gets \max_{1\le c\le n} \max_{v_1,v_2\in son_u,v_1\ne v_2} (f_{v_1,c} - g_{v_1}) + (f_{v_2,c} - g_{v_2}) \end{cases}\)
不过在具体实现中会发现
f数组不需要存那么多状态,对于节点u来说,最多有以u节点为根的子树的大小个类型,所以可以考虑用map维护。在合并子树之间的情况时,可以使用启发式合并来优化。
这里再解释一下
help数组。这个数组的功能是用于辅助更改
f数组的,有了它就更容易合并不同子树的f数组。所以实际的 \(f_{u,c} - g_v = f[u][col] + help[u]\) 。
具体合并时的
f[u][col] = (val + help[v]) - help[u];就能使得之后f[u][col] + help[u] = f[v][col] + help[v]。至于为什么这么做,这是因为在合并完后需要对所有的 \(f_u\) 再更新,也就是都加上 \(\sum_{v \in son_u}^{} g_v\) ,并减去 \(g_u\) 。
从而便于之后回溯的时候对各个 \(f_{u,c} - g_u\) ,开箱即用。
只需要简单的用 \(f[u][col] + help[u]\) 就能求得该值。
总的时间复杂度为 \(O(nlog_2(n))\) ,如果用线段树合并能减少一个 \(log\) 。
#include <bits/stdc++.h> using i64 = long long; constexpr int N = 2e5 + 1; int n, c[N], w[N], e[N << 1], ne[N << 1], h[N], idx; i64 g[N], help[N]; std::map<int, i64> f[N]; void add(int u, int v) { e[idx] = v, ne[idx] = h[u], h[u] = idx++; } void dfs(int u, int fa) { int sum = g[u] = help[u] = 0; for (int i = h[u]; i != -1; i = ne[i]) { int v = e[i]; if (v == fa) continue; dfs(v, u); sum += g[v]; } g[u] = sum; for (int i = h[u]; i != -1; i = ne[i]) { int v = e[i]; if (v == fa) continue; if (f[v].size() > f[u].size()) { std::swap(f[u], f[v]); std::swap(help[u], help[v]); } for (auto [col, val] : f[v]) { if (f[u].count(col)) { g[u] = std::max( g[u], sum + (f[u][col] + help[u]) + (f[v][col] + help[v])); } if (!f[u].count(col) || (f[u][col] + help[u]) < (val + help[v])) { f[u][col] = (val + help[v]) - help[u]; } } f[v].clear(); } if (f[u].count(c[u])) { g[u] = std::max(g[u], sum + (f[u][c[u]] + help[u]) + w[u]); } if (!f[u].count(c[u]) || (f[u][c[u]] + help[u] < w[u])) { f[u][c[u]] = w[u] - help[u]; } help[u] += sum - g[u]; } void solve() { memset(h, -1, sizeof(h)); idx = 0; std::cin >> n; for (int i = 0; i < n; i++) { std::cin >> c[i]; } for (int i = 0; i < n; i++) { std::cin >> w[i]; } for (int i = 0; i < n - 1; i++) { int u, v; std::cin >> u >> v; u--, v--; add(u, v); add(v, u); } dfs(0, -1); std::cout << *std::max_element(g, g + n) << '\n'; f[0].clear(); } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int t; std::cin >> t; while (t--) { solve(); } return 0; }
单峰数列§
显然要用线段树来维护。
不过我们只需要维护原数组的差分数组。
更改操作是区间加上某个数,差分数组具有先天优势,具体实现不用多说。
而对于某个查询无论询问区间是升序,降序还是全相等,只需要看这个区间的差分数组是否全正,全负,全
0即可。接下来看如何判断单峰数列。
对于一个单峰数列,其差分数组左侧应全为正数右侧应全为负数。
在线段树上合并两个区间时其实也很好判断,满足条件的仅仅有三种情况:
左正右负
左峰右负
左正右峰
具体实现这里不再细讲,注意,用结构体来维护节点信息更方便并且易于理解。
#include <bits/stdc++.h> constexpr int N = 2e5 + 10; int n, a[N]; namespace seg { struct Node { bool positive, negative, zero, mountain; } t[N << 2]; Node merge(Node l, Node r) { return Node{l.positive && r.positive, l.negative && r.negative, l.zero && r.zero, (l.positive && r.negative) || (l.mountain && r.negative) || (l.positive && r.mountain && r.mountain)}; }; void push_up(int u) { t[u] = merge(t[u << 1], t[u << 1 | 1]); } void build(int u, int L, int R) { if (L == R) { t[u] = Node{a[L] > 0, a[L] < 0, a[L] == 0, false}; return; } int mid = (L + R) >> 1; build(u << 1, L, mid); build(u << 1 | 1, mid + 1, R); push_up(u); } void modify(int u, int L, int R, int pos, int val) { if (L == R) { t[u] = Node{val > 0, val < 0, val == 0, false}; return; } int mid = (L + R) >> 1; if (pos <= mid) { modify(u << 1, L, mid, pos, val); } else { modify(u << 1 | 1, mid + 1, R, pos, val); } push_up(u); } Node query(int u, int L, int R, int l, int r) { if (L >= l && R <= r) { return t[u]; } int mid = (L + R) >> 1; if (mid >= l && mid < r) { return merge(query(u << 1, L, mid, l, r), query(u << 1 | 1, mid + 1, R, l, r)); } else if (mid >= l) { return query(u << 1, L, mid, l, r); } else { return query(u << 1 | 1, mid + 1, R, l, r); } } bool query(int q, int l, int r) { if (l == r) { return q != 5; } Node result = query(1, 1, n, l + 1, r); if (q == 2) { return result.zero; } else if (q == 3) { return result.positive; } else if (q == 4) { return result.negative; } else { return result.mountain; } } } // namespace seg int main() { std::cin >> n; for (int i = 1; i <= n; i++) { std::cin >> a[i]; } for (int i = n; i >= 2; i--) { a[i] -= a[i - 1]; } seg::build(1, 1, n); int q; std::cin >> q; while (q--) { int op, l, r; std::cin >> op >> l >> r; if (op == 1) { int x; std::cin >> x; a[l] += x, a[r + 1] -= x; seg::modify(1, 1, n, l, a[l]); if (r + 1 <= n) seg::modify(1, 1, n, r + 1, a[r + 1]); } else { std::cout << seg::query(op, l, r) << '\n'; } } return 0; }
抓拍§
满足凹函数性质,可以直接三分找答案。
#include <bits/stdc++.h> using i64 = long long; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int n; std::cin >> n; std::vector<std::array<int, 4>> a(n); for (auto &[x, y, dx, dy] : a) { std::cin >> x >> y; char c; std::cin >> c; dx = dy = 0; if (c == 'E') { dx++; } else if (c == 'W') { dx--; } else if (c == 'N') { dy++; } else { dy--; } } i64 l = 0, r = 2e9; auto check = [&](int u) -> i64 { i64 minx = 1e9, miny = 1e9, maxx = -1e9, maxy = -1e9; for (auto [x, y, dx, dy] : a) { i64 nx = x + u * dx, ny = y + u * dy; minx = std::min(minx, nx); miny = std::min(miny, ny); maxx = std::max(maxx, nx); maxy = std::max(maxy, ny); } return std::abs(maxx - minx) + std::abs(maxy - miny); }; while (l < r) { int lmid = l + (r - l) / 3, rmid = r - (r - l) / 3; if (check(lmid) <= check(rmid)) r = rmid - 1; else { l = lmid + 1; } } std::cout << 2LL * check(l) << '\n'; return 0; }
死亡之组§
先把除
1号队伍外按照实力排序。根据
1号队伍实力大小讨论,小于L就选两个最小的再选一个最大的。否则就全选最小的。
#include <bits/stdc++.h> void solve() { int n, L, D; std::cin >> n >> L >> D; std::vector<int> a(n); for (auto &it : a) { std::cin >> it; } std::sort(a.begin() + 1, a.end()); std::vector<int> v{a[0]}; v.push_back(a[1]); v.push_back(a[2]); if (a[0] < L) { v.push_back(a[n - 1]); } else { v.push_back(a[3]); } std::sort(v.begin(), v.end()); if (v[2] < L && v[3] - v[0] > D) { std::cout << "Yes\n"; } else { std::cout << "No\n"; } } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int t; std::cin >> t; while (t--) { solve(); } return 0; }