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;
}