2024CCPC哈尔滨 题解 ABCEGJKLM
A. 造计算机
构造题,注意到可以两点之间可以连不止一条边,所以我们可以创建一系列初始节点,让 \(i\) 向 \(i + 1\) 连一条权值为 \(0\) 和 一条权值为 \(1\) 的边,这样我们就能得到 \(0000... \sim 1111...\) 之间的所有值。
所以我们可以将目标区间 \([L, R]\) 划分成一系列子区间 \([L_i, R_i]\),使得 \(L_i = xxxx000...0, R_i=xxxx111...1\)。
具体实现可以枚举每一位,对于第 \(i\) 位,如果 \(L\) 的这一位为 \(0\),那么我们可以把这一位置为 \(1\),这样我们可以得到一段区间 \(xxx100...0 \sim xxx111...1\);如果 \(R\) 的这一位为 \(1\),那么我们可以把这一位置为 \(0\),这样我们可以得到一段区间 \(xxx00...0 \sim xxx011...1\),一直这样处理就好,注意分出来的区间不要跨过原来的区间,也不要划分出来相同的区间。(这一部分细节比较多)
现在我们可以把得到的前缀插入到字典树中,跑一遍 \(dfs\),如果跑到一段前缀的结尾,就把边连向对应的初始节点,否则就正常连向下一个节点。
由于每一段前缀都是 \(L\) 或 \(R\) 的前缀,所以最后的节点数量大概是 \(3logn\) 级别。
代码非常丑
#include <bits/stdc++.h>
constexpr int N = 22;
struct Trie
{
static constexpr int M = 2;
struct node
{
std::array<int, M> son;
bool isend;
std::set<int> len;
};
std::vector<node> tr;
int root = newnode();
int tot = 0;
int newnode()
{
tr.emplace_back();
return tr.size() - 1;
}
int insert(std::string &s, int len)
{
int p = root;
for(auto c : s)
{
int x = c - '0';
if(!tr[p].son[x]) tr[p].son[x] = newnode();
p = tr[p].son[x];
}
tr[p].isend = true;
tr[p].len.emplace(len);
return p;
}
};
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int l, r; std::cin >> l >> r;
std::vector<int> L, R;
bool ok = true;
if(l & 1) L.emplace_back(l), R.emplace_back(l);
for(int i = 0; i < N && (1 << i) < r; i++)
{
if(l >> i & 1) continue;
if(i == 0)
{
L.emplace_back(l);
R.emplace_back(l + 1);
continue;
}
int tl = (l >> i | 1) << i;
int tr = tl + (1 << i) - 1;
if(tr > r)
{
ok = false;
break;
}
L.emplace_back(tl), R.emplace_back(tr);
}
if(!ok) for(int i = 0; i < N && (1 << (i + 1)) < r; i++)
{
if(!(r >> i & 1)) continue;
int tl = (r >> (i + 1)) << (i + 1);
int tr = tl + (1 << i) - 1;
if(tl < l) break;
L.emplace_back(tl), R.emplace_back(tr | 1);
}
if(!std::ranges::count(R, r))
{
L.emplace_back(r);
R.emplace_back(r);
}
Trie trie;
int max = 0;
for(int i = 0; i < (int)R.size(); i++)
{
int len = 0;
std::string s = "";
while(L[i] || R[i])
{
if(L[i] % 2 != R[i] % 2) len++;
else s += std::to_string(L[i] % 2);
L[i] /= 2, R[i] /= 2;
}
std::ranges::reverse(s);
trie.insert(s, len);
max = std::max(max, len);
}
int cur = trie.tr.size();
std::vector<std::vector<std::pair<int, int>>> adj(cur + max + 1);
for(int i = 0; i < max; i++)
{
adj[cur + i].emplace_back(cur + i + 1, 0);
adj[cur + i].emplace_back(cur + i + 1, 1);
}
auto dfs = [&](auto dfs, int p) -> void
{
if(trie.tr[p].son[0])
{
int np = trie.tr[p].son[0];
if(trie.tr[np].isend) for(auto len : trie.tr[np].len)
adj[p].emplace_back(cur + max - len, 0);
adj[p].emplace_back(np, 0);
dfs(dfs, np);
}
if(trie.tr[p].son[1])
{
int np = trie.tr[p].son[1];
if(trie.tr[np].isend) for(auto len : trie.tr[np].len)
adj[p].emplace_back(cur + max - len, 1);
adj[p].emplace_back(np, 1);
dfs(dfs, np);
}
};
dfs(dfs, 0);
std::vector<std::vector<int>> ans;
std::vector<int> cnt(cur + max + 1);
for(int i = 0; i < cur + max + 1; i++)
{
if(adj[i].empty() && (i != cur + max)) cnt[i]++;
if(i) cnt[i] += cnt[i - 1];
}
for(int i = 0; i < cur + max + 1; i++)
{
if(adj[i].empty())
{
if(i == cur + max) ans.emplace_back(std::vector<int>{0});
continue;
}
int tot = adj[i].size();
for(auto [j, w] : adj[i]) if(adj[j].empty() && j != cur + max) tot--;
std::vector<int> tmp;
tmp.emplace_back(tot);
for(auto [j, w] : adj[i]) if(!adj[j].empty() || j == cur + max)
tmp.emplace_back(j - cnt[j] + 1), tmp.emplace_back(w);
ans.emplace_back(tmp);
}
std::cout << ans.size() << "\n";
for(auto vec : ans)
{
for(auto i : vec) std::cout << i << " ";
std::cout << "\n";
}
return 0;
}
B. 凹包
先对所有点求一次凸包,把这些点删除后再求一次凸包,那么只要最小化旧凸包上的一条边与新凸包上的一个点构成的三角形的面积集合,具体实现可以用双指针。
代码队友写的
#include <bits/stdc++.h>
using namespace std;
#define int long long
struct P{
int x, y;
P operator+(P p) { return {x + p.x, y + p.y}; }
P operator-(P p) { return {x - p.x, y - p.y}; }
bool operator<(P p) const{
if(x == p.x) return y < p.y;
return x < p.x;
}
bool operator==(P p)const{
return x == p.x && y == p.y;
}
int dot(P p){ return x * p.x + y * p.y; }
};
int cross(P p1, P p2, P p3){
return (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
}
vector<P> convexHull(vector<P> &ps){
sort(ps.begin(),ps.end());
ps.erase(unique(ps.begin(), ps.end()), ps.end());
int n = ps.size();
if(n <= 1) return ps;
vector<P> qs(n << 1);
int top = 0;
for(int i = 0; i < n; qs[top++] = ps[i++])
while(top > 1 && cross(qs[top - 2], qs[top - 1], ps[i]) <= 0) top--;
for(int i = n - 2, t = top; i >= 0; qs[top++] = ps[i--])
while(top > t && cross(qs[top - 2], qs[top - 1], ps[i]) < 0) top--;
qs.resize(top - 1);
return qs;
}
void solve(){
int n;
cin >> n;
vector<P> a(n);
for(int i = 0; i < n; i++){
cin >> a[i].x >> a[i].y;
}
map<P, int> vis;
auto h1 = convexHull(a);
for(auto i : h1) vis[i] = 1;
vector<P> b;
for(auto i : a) if(!vis.contains(i)) b.push_back(i);
auto h2 = convexHull(b);
if(!h2.size()){
cout << -1 << "\n";
return;
}
int u = h1.size(), v = h2.size(), ans = 0;
for(int i = 2; i < u; i++){
ans += cross(h1[0], h1[i - 1], h1[i]);
}
int d = 9e18;
if(h2.size() == 1){
for(int i = 0; i < n; i++){
d = min(d, cross(h2[0], h1[i], h1[(i + 1) % u]));
}
cout << ans - d << "\n";
return;
}
for(int i = 0, j = 0; i < u; i++){
P nomral = {h1[i].y - h1[(i + 1) % u].y, h1[(i + 1) % u].x - h1[i].x};
while(nomral.dot(h2[(j + 1) % v] - h2[j]) < 0) j = (j + 1) % v;
while(nomral.dot(h2[(j + v - 1) % v] - h2[j]) < 0) j = (j + v - 1) % v;
d = min(d, cross(h2[j], h1[i], h1[(i + 1) % u]));
}
cout << ans - d << "\n";
}
signed main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t; cin >> t;
while(t--) solve();
return 0;
}
C. 在哈尔滨指路
签到题,模拟即可
#include <bits/stdc++.h>
void solve()
{
int n; std::cin >> n;
std::map<std::pair<char, char>, char> mp;
mp[{'N', 'E'}] = 'R', mp[{'N', 'W'}] = 'L';
mp[{'S', 'E'}] = 'L', mp[{'S', 'W'}] = 'R';
mp[{'E', 'N'}] = 'L', mp[{'E', 'S'}] = 'R';
mp[{'W', 'N'}] = 'R', mp[{'W', 'S'}] = 'L';
char s; std::cin >> s;
int d; std::cin >> d;
std::vector<std::pair<char, int>> ans;
ans.emplace_back('Z', d);
char lst = s;
for(int i = 1; i < n; i++)
{
char c; std::cin >> c >> d;
ans.emplace_back(mp[{lst, c}], 0);
ans.emplace_back('Z', d);
lst = c;
}
std::cout << ans.size() << " " << s << "\n";
for(auto [dir, dis] : ans)
{
if(dis) std::cout << dir << " " << dis << "\n";
else std::cout << dir << "\n";
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t; std::cin >> t;
while(t--) solve();
return 0;
}
E. 弹珠赛跑
首先我们可以有一个朴素的想法,枚举所有存在小球位置可能为 \(0\) 的时间:对于第 \(i\) 个小球,存在时间 \(t = \frac{x_{c_i}}{v_i}\) 使得它存在位置为 \(0\) 的起点 \(c_i\);知道时间之后,我们就可以知道在这个时间下,对于小球 \(j\ (j \neq i)\) 有多少种起点使得经过 \(t\) 时间后位置在 \(0\) 之前,不妨记为 \(cnt_j\)。然后我们就可以对这些小球做一遍背包,记 \(f[i][j]\) 为 \(i\) 个小球恰有 \(j\) 个小球的最终位置在 \(0\) 之前的方案数,则有:
如果我们每次都这么做一遍的话,时间复杂度显然为 \(O(n^4)\),无法通过。
这个时候我们可以思考一下,\(cnt\) 数组一定要每次都重新求吗?这显然是不需要的,因为假如我们枚举到一个 \(t\),他是属于小球 \(i\) 的,那么只有\(cnt[i]\) 会改变,所以我们只需要把小球 \(i\) 的贡献从背包中删除,更新 \(cnt[i]\),再加入回背包中就可以了,这样单次复杂度是 \(O(n)\) 的。具体删除的过程,只需要把正着转移的方程改写,即:
其中 \(f[i - 1][0] = \frac{f[i][0]}{cnt[i]}\),空间可以用滚动数组优化。
对于计算答案,在删除第 \(i\) 个小球的时候,\(f[\lfloor \frac{m}{2} \rfloor] \times t\) 即为对答案的贡献,因为此时第 \(i\) 个小球的位置就是 \(0\)。最后的答案再除以 \(n^m\),因为我们前面计算的是每个方案贡献的总和,所以最后要除以总的可能的方案。
*\(Extra\) :
对于背包的操作,我们也可以从多项式的角度理解,我们可以把每个球看成一个多项式 \(cnt[i] + (n - cnt[i])x\),最终对答案的贡献就是 \(x^{\lfloor \frac{m}{2} \rfloor}\) 的系数。
记 \(a = cnt[i], b = n - cnt[i]\),那么每次加入物品的操作就是乘多项式 \(a + bx\),删除就是乘 \(\frac{1}{a + bx}\)。对于乘 \(\frac{1}{a + bx}\),我们可以把这个式子稍微整理一下:
相当于让原来的数组乘 \(\frac{1}{a}\) 后再以 \(-\frac{b}{a}\) 为系数跑一次完全背包。
可能还有其他理解方式,但是笔者太菜了只会这么化简
#include <bits/stdc++.h>
using i64 = long long;
constexpr int P = 1e9 + 7;
i64 power(i64 a, i64 b)
{
i64 res = 1;
for( ; b; b >>= 1, a = a * a % P)
if(b & 1) res = res * a % P;
return res;
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m; std::cin >> n >> m;
std::vector<i64> x(n), v(m);
for(int i = 0; i < n; i++) std::cin >> x[i];
for(int i = 0; i < m; i++) std::cin >> v[i];
std::vector<std::array<i64, 3>> t(n * m);
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
t[i * m + j] = {-x[i], v[j], j};
std::ranges::sort(t, [&](auto a, auto b)
{
return a[0] * b[1] < a[1] * b[0];
});
std::vector<int> cnt(m, n);
std::vector<i64> f(m + 1);
f[0] = 1;
auto mul = [&](i64 a, i64 b) -> void // a + bx;
{
auto g = f;
for(int i = 0; i <= m; i++) f[i] = g[i] * a % P;
for(int i = 1; i <= m; i++) f[i] = (f[i] + g[i - 1] * b % P) % P;
};
auto div = [&](i64 a, i64 b) -> void
{
i64 inva = power(a, P - 2);
for(auto &i : f) i = i * inva % P;
b = b * inva % P;
b = P - b;
for(int i = 0; i < m; i++) f[i + 1] = (f[i + 1] + f[i] * b) % P;
};
i64 ans = 0;
for(int i = 0; i < m; i++) mul(cnt[i], 0);
for(int i = 0; i < n * m; i++)
{
auto [a, b, id] = t[i];
div(cnt[id], n - cnt[id]);
cnt[id]--;
ans = (ans + a * power(b, P - 2) % P * f[m / 2] % P) % P;
mul(cnt[id], n - cnt[id]);
}
ans = ans * power(power(n, m), P - 2) % P;
std::cout << ans << "\n";
return 0;
}
G. 欢迎加入线上会议!
签到题。
随便选一个不忙的人开始跑 \(bfs\),如果搜到一个忙的人,就不让他相邻的人入队,最后判断一下是不是 \(n\) 个人都被搜到即可。
输出答案的时候可以重新跑一次 \(bfs\)。
#include <bits/stdc++.h>
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, k; std::cin >> n >> m >> k;
std::vector<int> tag(n), vis(n);
for(int i = 0; i < k; i++)
{
int x; std::cin >> x;
x--;
tag[x] = 1;
}
std::queue<int> q;
int st = 0;
for(int i = 0; i < n; i++)
{
if(!tag[i])
{
st = i;
q.emplace(i);
break;
}
}
std::vector<std::vector<int>> adj(n);
for(int i = 0; i < m; i++)
{
int u, v; std::cin >> u >> v;
u--, v--;
adj[u].emplace_back(v);
adj[v].emplace_back(u);
}
std::vector<std::vector<int>> ans(n);
int cnt = 1;
vis[st] = 1;
while(!q.empty())
{
int x = q.front(); q.pop();
for(auto y : adj[x])
{
if(vis[y]) continue;
ans[x].emplace_back(y);
vis[y] = 1;
cnt++;
if(!tag[y]) q.emplace(y);
}
}
if(cnt == n)
{
std::cout << "Yes\n";
int tot = 0;
for(int i = 0; i < n; i++) if(!ans[i].empty()) tot++;
std::cout << tot << "\n";
q.emplace(st);
while(!q.empty())
{
int x = q.front(); q.pop();
if(ans[x].empty()) continue;
std::cout << x + 1 << " " << ans[x].size() << " ";
for(auto y : ans[x])
{
std::cout << y + 1 << " ";
q.emplace(y);
}
std::cout << "\n";
}
}
else std::cout << "No\n";
return 0;
}
J. 新能源汽车
贪心的考虑,我们每次肯定用最近的充电桩的电池,所以可以记录每个电池对应的充电桩的位置,然后用一个小根堆存储每次备选的电池,枚举每个充电桩的位置,即时更新即可。
值得注意的是,一个电池必须还有点才能放入堆中,并且没有充电桩的电池也是可以使用一次的,所以我们可以给每个电池都虚设一个无限远处的充电桩。
#include <bits/stdc++.h>
using i64 = long long;
void solve()
{
int n, m; std::cin >> n >> m;
std::vector<int> a(n);
for(int i = 0; i < n; i++) std::cin >> a[i];
std::vector<int> aa(a);
std::vector<int> X(m), T(m);
for(int i = 0; i < m; i++) std::cin >> X[i] >> T[i], T[i]--;
std::vector<std::vector<int>> pos(n);
for(int i = 0; i < m; i++) pos[T[i]].emplace_back(X[i]);
for(int i = 0; i < n; i++)
{
pos[i].emplace_back(X[m - 1] + 1);
std::ranges::reverse(pos[i]);
}
i64 cur = 0;
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<>> q;
for(int i = 0; i < n; i++)
{
q.emplace(pos[i].back(), i);
pos[i].pop_back();
}
for(int i = 0; i < m; i++)
{
int tmp = X[i] - cur;
while(!q.empty() && tmp)
{
auto [x, t] = q.top(); q.pop();
if(tmp >= a[t]) tmp -= a[t], a[t] = 0;
else
{
a[t] -= tmp;
tmp = 0;
if(x > X[i]) q.emplace(x, t); // 不是当前充电桩的电池,并且还能接着用,就重新入队
}
}
if(tmp)
{
std::cout << X[i] - tmp << "\n";
return;
}
cur = X[i];
a[T[i]] = aa[T[i]];
if(!pos[T[i]].empty()) // 又能用了,入队
{
q.emplace(pos[T[i]].back(), T[i]);
pos[T[i]].pop_back();
}
}
for(int i = 0; i < n; i++) cur += a[i];
std::cout << cur << "\n";
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t; std::cin >> t;
while(t--) solve();
return 0;
}
K. 农场经营
我们先对每种作物按 \(w_i\) 从大到小排序,初始让所有作物的处理时间都为 \(l_i\),现在我们考虑删除一个作物的时间限制。
假如我们删除了作物 \(i\) 的时间限制,那么我们获得的可分配时间为 \(m-\sum l + l_i\),我们可以贪心的分配这些时间,按从 \(w_i\) 大到小来分配,最后把剩余的时间全部分配给 \(i\)。
对每个 \(i\) 都这么操作,取最大值即可。
#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;
i64 m; std::cin >> m;
std::vector<i64> w(n), l(n), r(n);
for(int i = 0; i < n; i++) std::cin >> w[i] >> l[i] >> r[i], m -= l[i];
std::vector<int> p(n);
std::iota(p.begin(), p.end(), 0);
std::ranges::sort(p, [&](int i, int j)
{
return w[i] > w[j];
});
std::vector<i64> pre(n + 1), suf(n + 1), res(n + 1);
for(int i = 0; i < n; i++)
{
res[i + 1] = res[i] + r[p[i]] - l[p[i]];
pre[i + 1] = pre[i] + w[p[i]] * r[p[i]];
}
for(int i = n - 1; i >= 0; i--)
suf[i] = suf[i + 1] + w[p[i]] * l[p[i]];
i64 ans = 0;
for(int i = 0; i < n; i++)
{
i64 ti = l[p[i]] + m;
if(ti >= res[i]) // 分的够直接分
{
ans = std::max(ans, pre[i] + w[p[i]] * (ti - res[i]) + suf[i + 1]);
continue;
}
int pos = *std::ranges::partition_point(std::views::iota(0, i), [&](int mid) // 找到第一个分不够的位置
{
return ti >= res[mid + 1];
});
ans = std::max(ans, pre[pos] + w[p[pos]] * (l[p[pos]] + ti - res[pos]) + suf[pos + 1] - w[p[i]] * l[p[i]]);
}
std::cout << ans << "\n";
return 0;
}
L. 树上游戏
详细题解
首先把期望转成统计方案数,最后除以 \((\frac{n(n-1)}{2})^2\)。
看到平方,套路地考虑维护 \(0/1/2\) 次项和,考虑dp,记 \(f[x][0/1/2]\) 为 \(x\) 子树内到 \(x\) 的所有路径的 \(0/1/2\) 次项和。
记 \(x\) 其中一个儿子为 \(y\),在 \(y\) 向 \(x\) 合并的过程中考虑对答案的贡献,有两种情况:
- \(y\) 中的路径直接向答案贡献,这种情况要考虑 \(x\) 侧端点的方案数
- \(y\) 中的路径和 \(x\) 中的路径拼起来,可以简单的计算
#include <bits/stdc++.h>
using i64 = long long;
void solve()
{
int n; std::cin >> n;
std::vector<int> fac;
for(int i = 1; i * i <= n; i++)
{
if(n % i) continue;
fac.emplace_back(i);
if(i * i != n) fac.emplace_back(n / i);
}
i64 ans = 0;
std::ranges::sort(fac);
for(int i = 0; i + 1 < (int)fac.size(); i++)
ans += 1LL * n / fac[i] * (fac[i + 1] - fac[i]);
std::cout << ans + 1 << "\n";
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t; std::cin >> t;
while(t--) solve();
return 0;
}
M. 奇怪的上取整
签到题,注意到 \(f(n,i)\) 的结果只可能是 \(n\) 除以 \(n\) 的因数。
如果我们将 \(n\) 的因数按小到大排序,得到 \(d_1, d_2, d_3, ... , d_m\),那么对于 \(f(n, j)\),如果存在 \(d_i \leq j < d_{i + 1}\),那么 \(f(n, j) = \frac{n}{d_i}\)。
同时,\(f(n, n) = 1\),故答案为:
分解因数求解即可
#include <bits/stdc++.h>
using i64 = long long;
void solve()
{
int n; std::cin >> n;
std::vector<int> fac;
for(int i = 1; i * i <= n; i++)
{
if(n % i) continue;
fac.emplace_back(i);
if(i * i != n) fac.emplace_back(n / i);
}
i64 ans = 0;
std::ranges::sort(fac);
for(int i = 0; i + 1 < (int)fac.size(); i++)
ans += 1LL * n / fac[i] * (fac[i + 1] - fac[i]);
std::cout << ans + 1 << "\n";
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t; std::cin >> t;
while(t--) solve();
return 0;
}
都看到这了,点个赞支持一下笔者呗,这篇题解写了挺久的