AtCoder Beginner Contest 070
A
题意
输入一个整数,判读是否回文。
题解
不妨以字符串形式输入,可以 \(O(n)\) 判断是否回文。
另外的,\(O(1)\) 判断回文有两种方法。一种是正向反向分别哈希,没推广性。
更好用的是以上升幂哈希一次,下降幂哈希一次。时间复杂度分别为 \(O(n)\) 和 \(O(n + \log n)\) 。
const int MAXN = 2E5+5;
const int HA = 2;
const int BB[HA] = {1234, 5678};
const int PP[HA] = {int(1E9) + 123, int(1E9) + 403};
i64 pw[HA][MAXN], ipw[HA][MAXN];
i64 hdw[HA][MAXN], hup[HA][MAXN];
i64 ksm(i64 a, i64 n, int P) {
i64 res = 1;
for (;n;n>>=1, a=a*a%P) if (n&1)
res=res*a%P;
return res;
}
void init_hash() {
for (int h = 0; h < HA; h++) {
pw[h][0] = 1;
for (int i = 1; i < MAXN; i++) {
pw[h][i] = pw[h][i - 1] * BB[h] % PP[h];
}
ipw[h][MAXN - 1] = ksm(pw[h][MAXN - 1], PP[h] - 2, PP[h]);
for (int i = MAXN - 1; i; --i) {
ipw[h][i - 1] = ipw[h][i] * BB[h] % PP[h];
}
assert(ipw[h][0] = 1);
}
}
void make_hash(std::string s) { // s[0] == ' '
int n = s.size();
for (int h = 0; h < HA; h++) {
for (int i = 1; i <= n; i++) {
hdw[h][i] = (hdw[h][i - 1] * BB[h] % PP[h] + s[i]) % PP[h];
hup[h][i] = (hup[h][i - 1] + s[i] * pw[h][i - 1] % PP[h]) % PP[h];
}
}
}
std::array<int, 2> get_hdw(int l, int r) {
assert(l <= r);
std::array<int, 2> res;
for (int h = 0; h < HA; h++) {
res[h] = (hdw[h][r] - hdw[h][l - 1] * pw[h][r - l + 1] % PP[h] + PP[h]) % PP[h];
}
return res;
}
std::array<int, 2> get_hup(int l, int r) {
assert(l <= r);
std::array<int, 2> res;
for (int h = 0; h < HA; h++) {
res[h] = (hup[h][r] - hup[h][l - 1] + PP[h]) % PP[h] * ipw[h][l - 1] % PP[h];
}
return res;
}
void solve() {
init_hash();
std::string s; std::cin >> s;
int n = s.size(); s = " " + s;
make_hash(s);
int ok = 1;
for (int h = 0; h < HA; h++) {
ok &= get_hdw(1, n) == get_hup(1, n);
}
std::cout << (ok ? "Yes" : "No") << "\n";
}
B
题意
给一个区间 \([A, B]\) 和一个区间 \([C, D]\) ,询问这两个区间的交集大小。
题解
结论: 问题可以扩展到 \(n\) 个区间,询问这 \(n\) 个区间的交集的位置。只需对所有左区间取 \(max\) ,所有右区间取 \(min\) ,交集即 \([max\{l_i\}, min\{r_i\}]\) 。
时间复杂度 \(O(n)\) 。
证明:
\(n\) 个区间如果有交,把左区间看作左括号,右区间看作右括号,按坐标优先,否则左括号优先排序。
则括号序列一定是 "[[[[[]]]]]" 的形式。如果存在一个 "][" ,至少这两个括号对应的区间交集是空集。
\(\square\)
view
const int N = 2;
std::vector<int> l(N + 1), r(N + 1);
int L = -(1 << 30), R = 1 << 30;
for (int i = 1; i <= N; i++) {
std::cin >> l[i] >> r[i];
L = std::max(L, l[i]);
R = std::min(R, r[i]);
}
std::cout << std::max<int>(R - L, 0) << "\n";
相似问题。
结论: 询问 \(n\) 个区间的并集,询问它们的位置。只需将括号序列排序,然后贪心选出合法的括号序列。
时间复杂度 \(O(n)\) 。
证明:
\(n\) 个区间如果有交,把左区间看作左括号,右区间看作右括号,按坐标优先,否则左括号优先排序。
首先一定形成合法的括号序列,可能是 "[[[][[]]]" 的形式。任意一个并集一定是一个不是两个合法括号序列之和的极大的合法括号序列。
若这个合法的括号序列是并集,且是两个合法括号序列之和,则这两个合法括号序列之间的一部分区间不属于并集。矛盾。
若这个合法的括号序列是并集,且不极小。则它显然属于并集而不是并集。
\(\square\)
C
题意
有 \(N\) 个时钟,第 \(i\) 个时钟指针转一圈的周期是 \(T_i\) 秒,所有时钟指针一开始都指向零刻度线。
现在这 \(N\) 个时钟同时启动,询问下一次它们指针都指向零刻度线是多少秒后。
保证答案不超过 i64 。
题解
对正整数 \(X\) 和 \(m_i\) :
i64 gcd(i64 a, i64 b) { return b ? gcd(b, a % b) : a; }
i64 lcm(i64 a, i64 b) { return a / gcd(a, b) * b; }
void solve() {
int N; std::cin >> N;
i64 lc = 1;
for (int i = 1; i <= N; i++) {
i64 x; std::cin >> x;
lc = lcm(lc, x);
}
std::cout << lc << "\n";
}
D
题意
给一棵大小为 \(N\) 的带权树,节点编号为 \(1, 2, 3, \cdots, N(1 \leq N \leq 10^{5})\) 。
给一个正整数 \(K(1 \leq K \leq N)\) 。
给 \(Q\) 个询问,\(1 \leq Q \leq 10^{5}\) 。
第 \(i\) 个询问。询问经过 \(K\) , \(x_i\) 到 \(y_i\) 的最短通路。
题解
首先经过 \(K\) , \(x_i\) 到 \(y_i\) 的最短通路,一定是 \(x_i\) 到 \(K\) 的最短通路加上 \(K\) 到 \(y_i\) 的最短通路。
树上两个点的最短通路(不经过特定点)有且仅有一条。(这时候可能会条件反射,树上不带修路径信息维护用倍增……别急)
且树上 dfs 的路径也一定是最短路径,理论上 \(Q\) 个询问中给的所有点开始 dfs ,都能找到到 \(K\) 的最短路径。
这里是一个常见 trick :树上、图上或区间的多组询问问题,且需要经过特定的常数个点(或与之关联)。我们可以不从询问给的点处理,而是用特定点处理。
这里我们可以以 \(K\) 为根,搜索出 \(K\) 到每个点的路径权值,问题可以解决。
view
类似 trick
问题一:
给一个带权有向图,给定学校在 \(x\) 点,每次询问一个点 \(y\) 到学校的距离。
思路:显然我们不打算每个询问都跑一遍单源最短路计算答案。我们建立一个反向图,然后从 \(y\) 跑一遍单源最短路。
于是问题显然,反向图中 \(x\) 点到所有点的最短路即正向图中所有点到 \(x\) 点的最短路。
问题二:
给定一个长度为 \(n\) 的数列 \(a_1, a_2, \cdots, a_n\) 和一个正整数 \(x\) 。给 \(m\) 个询问,第 \(i(1 \leq m)\) 个询问给出 \(l_i, r_i\) ,询问能否在 \(a_{l_i}, a_{l_i + 1}, \cdots, a_{r_i}\) 找到两个数 \(h, w\) 满足 \(h \oplus w = x\) 。
思路:从询问计算很麻烦,但是 \(x\) 是特定常数,所以可以从 \(x\) 计算。很容易想到能否在 \(\{a_{l_i}, a_{l_i + 1}, \cdots, a_{r_i}\}\) 找到一个数 \(k\) 满足 \(k \oplus x\) 也在该集合内。貌似拥有了一个 \(O(m n)\) 的做法。
实际上我们需要设计 \(last[v]\) 为权值 \(v\) 上一次出现的位置。
设计 \(dp[i]\) 表示 \(1\) 到 \(i\) 的前缀 \([1, i]\) 中,\(last[a[j] \oplus x](1 \leq j \leq i)\) 的最大值。
设计完状态后转移方程显然:
还需要动态维护 \(last[a[i]] = i\) 。
于是对每个询问,若 \(dp[r_i] \geq l_i\) ,则区间内存在一个数 \(k\) 使得 \(k \oplus x\) 依旧在该区间中。
本题加强版(非 trick )。
如果每次询问给出不同的 \(K_i\) ,这放到很多问题上都不太好处理。
但是本题依旧存在经典的处理。
依旧使用一开始的 trick 。经过 \(K\) ,从 \(x_i\) 到 \(y_i\) 的最短通路是 \(x_i\) 到 \(K\) 的最短通路拼上 \(K\) 到 \(y_i\) 的最短通路。
那么一棵树上两个点的最短通路有且仅有一条,这就转化为了树上路径问题。
使用倍增预处理,并利用 \(LCA\) 获得路径并计算答案。
时间复杂度 \(O(m \log n)\) 。
view
int n; std::cin >> n;
std::vector<std::vector<std::pair<int, int> > > adj(n + 1);
for (int i = 1; i < n; i++) {
int u, v, w; std::cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
std::vector<int> dep(n + 1);
const int LGN = 20;
std::vector<std::vector<int> > p(LGN + 1, std::vector<int>(n + 1, 0));
std::vector<std::vector<i64> > val(LGN + 1, std::vector<i64>(n + 1, 0));
std::function<void(int, int)> dfs = [&] (int u, int par) {
for (auto vw : adj[u]) {
int v = vw.first, w = vw.second;
if (v == par) continue;
dep[v] = dep[u] + 1;
p[0][v] = u;
val[0][v] = w;
dfs(v, u);
}
};
dfs(1, -1);
for (int j = 1; j <= LGN; j++) {
for (int i = 1; i <= n; i++) {
p[j][i] = p[j - 1][p[j - 1][i]];
val[j][i] = val[j - 1][i] + val[j - 1][p[j - 1][i]];
}
}
int Q; std::cin >> Q;
int K; std::cin >> K;
auto query = [&] (int u, int v) -> i64 {
i64 ans = 0;
if (dep[u] < dep[v]) std::swap(u, v);
int L = dep[u] - dep[v];
for (int i = LGN; ~i; --i) if (L >> i & 1) {
ans += val[i][u];
u = p[i][u];
}
if (u == v) {
return ans;
}
for (int i = LGN; ~i; --i) if (p[i][u] != p[i][v]) {
ans += val[i][u] + val[i][v];
u = p[i][u];
v = p[i][v];
}
return ans + val[0][u] + val[0][v];
};
for (int i = 1; i <= Q; i++) {
int x, y; std::cin >> x >> y;
std::cout << i64(query(x, K) + query(y, K)) << "\n";
}
浙公网安备 33010602011771号