2024“釘耙程式設計”中國大學生演算法設計超級聯賽(3)
深度自同構
HDU - 7457
思路
不太會推,賽時隊友出的,找到的規律就是 \(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;
}
單峰數列
HDU - 7463
思路
難調的線段樹。。
只需要維護區間最大值,最小值,以及升降序即可,判斷相同用看區間最大與最小是否相同即可,判斷單峰需要二分找到區間最大值,然後二分找到最大值的位置,從這個位置判斷左邊是否升,右邊是否降即可。
程式碼
#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;
}
位元跳躍
HDU - 7464
思路
考慮到 \(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;
}
抓拍
HDU - 7467
思路
當存在有人往回走的時候,這個時候周長會變小,到達一定界限後,這個周長又會變大,所有用座標軸表示 周長-時間 的曲線的話是個很顯然的單峰函式,而對於單峰函式要找極值就需要用到三分了。
程式碼
#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;
}
死亡之組
HDU - 7468
思路
分類討論,首先把 \(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;
}