Loading

20230414 训练记录:前后缀

时间过得真快啊

去年这个时候打 ZUCC 校赛同步赛时还在牛客写了两个题解,里面有句话:

image

一年后的我已经学会了 lca,那现在算不算长大了呢?

这两篇题解也还挺有意思的,搬到这里好了!

Sum of Numerators

给定 \(N, K\),求解将序列 \(\left\{\dfrac{i}{2^K}\right\}\) 中元素全部约分后的分子和,其中 \(i\) 遍历 \(1 \sim N\)

\(N \in [1, 10^9], K \in [0, 10 ^ 9]\)

首先注意到 \(\gcd(2^K, \mathrm{odd}) = 1\),于是我们先将所有奇数和算入答案,我们知道:

\[\sum_{i = 1}^N[i \% 2 = 1] \times i = \bigg\lceil\dfrac{N}{2}\bigg\rceil ^2 \]

接下来考虑偶数与 \(2 ^ K\) 约分的过程,偶数可写作 \(X = x2 ^ t,\;\mathtt{where}\;\gcd(2, x) = 1\)。于是 \(2 ^ K\) 将会约分所有 \(t \le K\) 的部分,使得其变为一段奇数和,使用上述算式求解即可。

最后再加上没有被约分的偶数和即可,即

\[\displaystyle\sum_{i = 1}^N[i \% 2 = 0] \times i = \bigg\lfloor\dfrac{N}{2}\bigg\rfloor \times \bigg(\bigg\lfloor\dfrac{N}{2}\bigg\rfloor + 1\bigg) \]

展开代码
using i64 = long long;

void solve() {
    int n, k;
    std::cin >> n >> k;
 
    i64 ans = 0;
    for (k += 1; k -- && n; n -= (n + 1) / 2) {
        ans += 1LL * ((n + 1) / 2) * ((n + 1) / 2);
        (!k) && (ans += 1LL * (n / 2) * (n / 2 + 1));
    }
     
    std::cout << ans << '\n';
}

Disjoint Path On Tree

给定一棵 \(N\) 个节点的树,求解二元组 \((u, v)\) 的个数,其中 \(u, v\) 是俩不相交的简单路径。
路径 \((i, j)\)\((j, i), \,i \ne j\) 视作同一路径。

\(N \in [1, 2 \times 10 ^ 5]\)

显然,树上的路径均为简单路径。容易知道 \(N\) 个节点的树的简单路径条数为:

\[F(N) = N + \displaystyle{\binom{n}{2}} = \dfrac{N \times (N + 1)}{2} \]

\((i, j)\)\((j, i), \,i \ne j\) 视作一样的也没关系,求出所有二元组后除以二即可。

另一方面,所求也等价于 \(\Big((F(N) ^ 2 -\) 相交组数 \(\Big)\),设两条路径交于 \(u\),即选择

\[((u, x)_{cyc}, (u, y)_{cyc})_{cyc} \]

讨论 \(x, y\) 的来源:

  1. \(x \in u\),即 \(\mathrm{cnt}_1 = F(u) - \displaystyle \sum_{to \;\in\;u} F(to)\)
  2. \(x \notin u\),即 \(\mathrm{cnt}_2 = \mathrm{size}_u \times (n - \mathrm{size}_u)\)

注意不能是两点均来自 \(x \notin u\)。组合起来:

\[(\mathrm{cnt}_1 + \mathrm{cnt}_2) ^ 2 - (\mathrm{cnt}_2)^2 \]

展开代码
Z f(int n) { return 1LL * n * (n + 1) / 2; }

void solve() {
  int n;
  std::cin >> n;

  std::vector<std::vector<int>> G(n);

  for (int i = 1, u, v; i < n; ++i) {
    std::cin >> u >> v;
    -- u, -- v;
    G[u].push_back(v);
    G[v].push_back(u);
  }

  Z ans = f(n) * f(n);
  std::vector<int> size(n);

  std::function<void(int, int)> dfs = [&](int u, int p) {
    size[u] = 1;
    for (auto &&to : G[u]) if (to != p) {
      dfs(to, u);
      size[u] += size[to];
    }

    Z cnt1 = f(size[u]);
    Z cnt2 = 1LL * size[u] * (n - size[u]);

    for (auto &&to : G[u]) if (to != p) {
      cnt1 -= f(size[to]);
    }

    ans -= (cnt1 + cnt2) * (cnt1 + cnt2) - cnt2 * cnt2;
  };

  dfs(0, -1);

  std::cout << ans / 2 << '\n';
}

至多删除一/两段的最大子段和

集训队小伙伴问的面试题,挺有意思的。中途还断断续续地去请教了 tarjen 大佬,感激不尽。

至多一段

注意到,最终的答案是两段拼起来的:枚举 \(i\),求出 \(i\) 左侧的最大子段和、右侧的最大子段和,作为候选答案。实际上问题转换为求出前缀、后缀的最大子段和的最大值。这种前后缀拼起来贡献答案的题挺常见的。

展开代码
#include <bits/stdc++.h>

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n;
    std::cin >> n;
    std::vector<int> a(n + 1), fp(n + 1), fs(n + 2);
    for (int i = 1; i <= n; i++) std::cin >> a[i];
    for (int i = 1; i <= n; i++) fp[i] = std::max(fp[i - 1], 0) + a[i];
    for (int i = 1; i <= n; i++) fp[i] = std::max(fp[i], fp[i - 1]);
    for (int i = n; i >= 1; i--) fs[i] = std::max(fs[i + 1], 0) + a[i];
    for (int i = n; i >= 1; i--) fs[i] = std::max(fs[i], fs[i + 1]);

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        ans = std::max(ans, fp[i - 1] + fs[i + 1]);
    }
    
    std::cout << std::max(ans, fp[n]) << '\n';
    
    return 0;
}

至多两段

我首先想的是,是否等价于做两次上面的那个问题。然后被群友 Hack 了:

这时候 tarjen 好哥哥说直接 \(\mathcal O(n \log n)\) 扫一遍就好了,我却没有理解到,接着加好友学了一波。答案实际上也是两段,不过这次是前区间和最小的两段,问题转换为求出所有前缀中,区间和最小的一段和为多少。区间和是两段前缀和之差。即对于当前遍历到的 \(i\),前缀和为 \(s_i\),找出所有前缀和 \(s_j\,(j \lt i)\) 中的最大值,和 \(s_i\) 的差就是前缀中最小的那段区间和了。

举一些例子来描述这个过程:
-1 -1 -1 -1 -1 [-1]

当前遍历到的前缀和为 \(-6\),前缀和集合为 \(\color{red}{0}\) \(-1, -2, -3, -4, -5\) 最大的是 \(0\),因此当前最小区间和为 \(-6\)

1 2 3 -6 5 [-6]

当前遍历到的前缀和为 \(-4\),前缀和集合为 \(0, 1, 3, 5, 6\),所以求出最小的区间和为 \(-1 - 6 = -7\)

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n;
    std::cin >> n;
    std::vector<int> a(n + 1);
    std::vector<ll> ps(n + 1), ss(n + 2);
    for (int i = 1; i <= n; i++) std::cin >> a[i];
    for (int i = 1; i <= n; i++) ps[i] = ps[i - 1] + a[i];
    for (int i = n; i >= 1; i--) ss[i] = ss[i + 1] + a[i];

    std::set<ll, std::greater<ll>> s;

    constexpr ll inf = 1e18;
    std::vector<ll> fp(n + 1), fs(n + 2);
    fp[0] = fs[n + 1] = inf;
    for (int i = 1; i <= n; i++) {
        s.insert(ps[i - 1]);
        fp[i] = std::min(fp[i - 1], ps[i] - *s.begin());
    }
    s.clear();
    for (int i = n; i >= 1; i--) {
        s.insert(ss[i + 1]);
        fs[i] = std::min(fs[i - 1], ss[i] - *s.begin());
    }

    fp[0] = fs[n + 1] = 0;

    ll ans = 0;
    for (int i = 1; i <= n; i++) {
        ans = std::min(ans, fp[i - 1] + fs[i + 1]);
    }

    std::cout << std::max<ll>(0, ps[n] - ans) << '\n';

    return 0;
}
闲话:学弟一天就写了个单调队列模板题然后十分得意,想说一通可是基本功实力不如他,讨厌自己。
posted @ 2023-04-15 02:26  PatrickyTau  阅读(14)  评论(0编辑  收藏  举报