CF1905E One-X 题解
题目链接:https://codeforces.com/contest/1905/problem/E
题意简述
有线段树的构建函数 \(build(id, l, r)\):
-
若 \(l = r\),函数返回单个编号为 \(id\) 的节点。
-
否则,函数返回一棵树,根节点编号为 \(id\),分别以 \(build(2 \times id, l, mid)\) 和 \(build(2 \times id + 1, mid + 1, r)\) 作为左右子树。(\(mid = \lfloor \dfrac{l + r}{2}\rfloor\))
对函数 \(build(1, 1, n)\) 构造的线段树,求所有非空叶节点子集的 LCA 编号的和。
\(n \le 10^{18}\),最多 \(10^3\) 组测试。
题解
由于线段树采用递归构建,自然想到采用递归的方式求答案。首先考虑 \(O(n)\) 的树形 DP。注意到只与叶结点个数有关,因此可以采用 \(dp(id, n)\) 的形式。LCA 为 \(id\) 的叶节点子集的个数为 \(cnt = 2^{n} - 1 - (2^{\lceil \dfrac n 2\rceil} - 1 + 2^{\lfloor \dfrac n 2\rfloor} - 1)\),再加上子树内部的答案,因此有:
读者应该能够证明 \(dp(id, n)\) 是关于 \(id\) 的线性函数,即 \(dp(id, n) = k(n) \cdot id + b(n)\)。则由上面的方程可以推导出从 \(\lceil \dfrac n 2\rceil\) 及 \(\lfloor \dfrac n 2\rfloor\) 到 \(n\) 的转移。使用记忆化搜索,状态数是 \(O(\log n)\) 的,加上快速幂总复杂度 \(O(\log ^2 n)\)。
代码实现
unordered_map<i64, tuple<Z, Z>> cache;
Z pow_set(i64 n) { return Z(2).pow(n) - 1; }
tuple<Z, Z> get(i64 n) {
if (cache.contains(n)) { return cache[n]; }
Z k, b;
k += pow_set(n) - pow_set((n + 1) / 2) - pow_set(n / 2);
k += 2 * get<0>(get((n + 1) / 2));
k += 2 * get<0>(get(n / 2));
b += get<1>(get((n + 1) / 2)) + get<0>(get(n / 2)) + get<1>(get(n / 2));
return cache[n] = {k, b};
}
void solve() {
i64 n;
cin >> n;
auto [k, b] = get(n);
cout << k + b << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cache[1] = {1, 0};
int t = 1;
cin >> t;
while (t--) { solve(); }
}
浙公网安备 33010602011771号