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) = dp(2 \times id, \lceil \dfrac n 2\rceil) + dp(2 \times id + 1, \lfloor \dfrac n 2\rfloor) + id \times cnt \]

读者应该能够证明 \(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(); }
}
posted @ 2024-01-03 23:42  cccpchenpi  阅读(52)  评论(0)    收藏  举报