AtCoder Beginner Contest 014
A
Problem
有 \(a\) 个点心,试图均分给 \(b\) 个人。点心不能切开,询问至少需要再买几个点心。
Solutions
每个人需要 \(\lceil \frac{a}{b} \rceil\) 个点心,于是总共需要点心 \(\lceil \frac{a}{b} \rceil b\) ,需要再买 \(\lceil \frac{a}{b} \rceil b - a\) 个。
int a, b; std::cin >> a >> b;
std::cout << (a + b - 1) / b * b - a << "\n";
B
Problem
给一个 \(n \ (1 \leq n \leq 20)\) ,给出 \(a_0, a_{1}, \cdots, a_{n - 1}\) 。
给一个 \(X\) ,若 \(X\) 的二进制下的第 \(i\) 位是 \(1\) ,则让 \(sum := sum + a_i\) 。
开始 \(sum = 0\) 。
Solutions
状压 \(DP\) 前置题?有符号整型移位出界取值是 \(0\) 。
view
int n; std::cin >> n;
int mask; std::cin >> mask;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i++) std::cin >> a[i];
i64 ans = 0;
for (int i = 1; i <= n; i++) if (mask >> i - 1 & 1) {
ans += a[i];
}
std::cout << ans << "\n";
C
Problem
给 \(n\) 个区间 \([l_i, r_i]\) 。询问被区间覆盖最多的某一段,被覆盖了多少次。
\(1 \leq n \leq 10^{5}, 0 \leq l_i, r_i \leq 10^{6}\)
Solutions
首先某段如果被覆盖最多次,会有某点被覆盖最多次。
对 \(10^{6}\) 的数轴做差分,前缀和回去。然后 \(T(10^{6})\) 检查贡献最多的点。
view
const int N = 1000005;
std::vector<int> a(N);
void solve() {
int n; std::cin >> n;
std::vector<int> l(n + 1), r(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> l[i] >> r[i];
a[l[i]]++; a[r[i] + 1]--;
}
int mx = 0;
for (int i = 0; i < N; i++) {
if (i > 0) a[i] += a[i - 1];
mx = std::max(mx, a[i]);
}
std::cout << mx << "\n";
for (int i = N - 1; i >= 1; --i) a[i] -= a[i - 1];
for (int i = 1; i <= n; i++) a[l[i]]--, a[r[i] + 1]++;
for (int i = N - 1; i >= 1; --i) assert(a[i] == 0);
}
实际上时间复杂度可以做到 \(O(n \log n)\) 。
先离散化,正确且比较稳妥的做法是:只把实际上会出现的点都加入离散化数组。只会出现 \(O(n)\) 个点。
然后差分,前缀和。查询获得最大贡献的点。
view
int n; std::cin >> n;
std::vector<int> l(n + 1), r(n + 1);
std::vector<int> numx(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> l[i] >> r[i];
numx.push_back(l[i]);
numx.push_back(r[i] + 1);
}
std::sort(numx.begin(), numx.end());
numx.erase(std::unique(numx.begin(), numx.end()), numx.end());
int m = numx.size();
std::vector<int> a(m + 2);
for (int i = 1; i <= n; i++) {
int p1 = std::lower_bound(numx.begin(), numx.end(), l[i]) - numx.begin();
int p2 = std::lower_bound(numx.begin(), numx.end(), r[i] + 1) - numx.begin();
a[p1]++;
a[p2]--;
}
int mx = 0;
for (int i = 1; i <= m; i++) a[i] += a[i - 1];
for (int i = 0; i <= m; i++) mx = std::max(mx, a[i]);
std::cout << mx << "\n";
这里的易错点是,\(0\) 号点不进前缀和,但需要贡献查询。显然 \(> 0\) 处的贡献可能不比 \(0\) 大。
D
Problem
给一棵 \(N\) 个点的无权树。\(Q\) 次独立询问两个点 \(x, y\) ,询问这两个点连接一条边,这条边所在的环包含多少边。数据保证这条边不会是重边。
\(1 \leq N, Q \leq 10^{5}\) 。
Solutions
有重边大不了特判而已。
考虑一棵树上任意两个点的简单路径,一定会是最短路径。
如果两个点没有直接连边的点 \(x, y\) 增加一条边,\(x \to y\) 路径上的点都会从桥变成环上的边。
然后这里显然可以直接倍增上去求 \(x \to y\) 的路径边数 \(K\) ,环上的边是 \(K + 1\) …… 时间复杂度为 \(Q \log N\) 。
代码如下:
倍增求边
事实上我就按照这个思路做了,可是直觉上比较傻逼,因为不需要预处理倍增数组。
多思考一会,注意树上任意两个点 \(x, y\) 的路径一定经过它们的最近公共祖先。
所以维护一下所有点的深度,然后 \(x \to y\) 的最短路径为 \(L = dep[x] + dep[y] - 2 LCA(x, y)\) ,路径边数为 \(L - 1\) ,加一条边的环长为 \(L - 1 + 1 = L\) 。
LCA 可以倍增求,dfs 序上二分求,tarjan + 并查集 求,重链剖分求。
时间复杂度都一样,数据比较随机的情况下重链剖分会快很多。而且重链剖分能维护更强的路径信息,其次是倍增和 dfn 。tarjan + 并查集似乎只是求了个 LCA 。
这里我打算都写一遍!!!(搞快点)
时间复杂度也是 \(O(N \log N)\) ,然后空间节省了一点。
代码如下:
倍增 LCA
tarjan + 并查集
重链剖分
如果 \(Q\) 很大呢怎么办呢。可以用 \(ST\) 表 + 欧拉序 \(O(n \log n + Q)\) 查出 \(LCA\) ,但是不能做路径维护。
欧拉序
要不要更快?不要吧。写欧拉序是因为欧拉序可以转括号序做树上莫队,不然根本不可能写的。
写 tarjan + 并查集 是因为 tarjan 很重要。
写 重链剖分是因为重链剖分很重要。
所以我们干脆学优化 log 。
树状数组优化 log 是因为简单且显然,线段树优化 log 是因为常数大。
不然根本不可能特意去费劲心思优化 log 的。
有 \(O(n)-O(1)\) \(\pm\) \(RMQ\) ,然后搞出 \(O(1)\) 求笛卡尔树上的 \(LCA\) ,于是搞出了 \(O(n)-O(1)\) \(RMQ\) 。
但是这些都不重要。
但是呢,我们甚至有更快的 \(LCA\) ,这个得问 lxlxl 。
但是呢。太沉迷于科技真是深渊,这些科技仅仅只是好玩而已。你根本没有一点点时间,和小学起步的 \(OIer\) 不一样。
及时醒悟,多刷点题才是重要的。
浙公网安备 33010602011771号