杂 D 选 P

[ARC168E] Subsegments with Large Sums

由于有恰好 \(k\) 段的限制,直接考虑 wqs 二分。

不加任何转化的话,可以发现 \(\geq s\) 的段数关于总分段数并不是凸的,因此进行一些转化是必要的。

由于最终分段一定可以表示为若干个 \(\geq s\) 的段和若干个长为 \(1\) 的段,记 \(f(i)\) 表示选出 \(i\) 个不交的和 \(\geq s\) 的段时最小的总长度,则答案可以为 \(i(i \leq k)\) 当且仅当 \(f(i) + k - i \leq n\)。如果能快速求出 \(f(i)\),就可以二分答案了。进一步观察可知,\(f\) 是下凸的,因此可以放心使用 wqs 二分求出 \(f(i)\)。时间复杂度为 \(\Theta(n \log n \log k)\)。可以进一步分析以去掉外层二分答案,复杂度 \(\Theta(n \log n)\),此处从略。

注意二分答案的上界为,在不考虑 \(k\) 段限制时,能够选出不交的 \(\geq s\) 的段的最大数量对 \(k\)\(\min\)。原因是 wqs 二分时为了规避共线情况会在每个合法的斜率处更新答案,而这样可能会让大于上界的 \(i\) 成为合法的。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

constexpr int N = 2.5e5 + 5;
int n, k; ll a[N], s[N], S;
int L[N]; ll f[N]; int g[N];

inline void Solve(int c) {
  for(int i = 1; i <= n; i++) {
    f[i] = g[i] = 0;
    if(L[i]) {
      ll w = f[L[i] - 1] + (i - L[i] + 1) - c;
      if(w < f[i]) f[i] = w, g[i] = g[L[i] - 1] + 1;
    }
    if(f[i - 1] < f[i] || (f[i - 1] == f[i] && g[i - 1] < g[i])) f[i] = f[i - 1], g[i] = g[i - 1];
  }
}

inline ll solve(int i) {
  int l = 1, r = n; ll res = -1;
  while(l <= r) {
    int mid = l + r >> 1;
    Solve(mid);
    if(g[n] > i) r = mid - 1;
    else l = mid + 1, res = 1ll * mid * i + f[n];
  }
  return res;
}

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr);
  
  cin >> n >> k >> S;
  for(int i = 1; i <= n; i++) {
    cin >> a[i];
    s[i] = s[i - 1] + a[i];
  }
  for(int i = 1, j = 1; i <= n; i++) {
    while(s[i] - s[j] >= S) j++;
    if(s[i] - s[j - 1] >= S) L[i] = j;
  }
  int upper = 0;
  for(int i = 1; i <= n; i++) {
    if(L[i]) f[i] = f[L[i] - 1] + 1;
    f[i] = max(f[i], f[i - 1]);
  }
  int l = 0, r = min((int)f[n], k), res = -1;
  while(l <= r) {
    int mid = l + r >> 1;
    if(solve(mid) - mid <= n - k) l = mid + 1, res = mid;
    else r = mid - 1;
  }
  cout << res << "\n";
}

[CF1930G] Prefix Max Set Counting

个人最初的想法是,记 \(f_{u, k}\) 表示考虑 \(u\) 子树,从 \(u\) 开始 dfs,只记录 \(\geq k\) 的值时能形成的本质不同序列数。记 \(M_u\)\(u\) 子树内的编号最大值,这里认为对于 \(i > M_u\)\(f_{u, i} = 0\)

首先特判掉 \(u = M_u\) 的情况,有 \(f_{u, *} = 1\)。由于依次遍历两棵子树 \(x, y\) 时,若 \(M_x > M_y\),则 \(y\) 子树不会再对 dfs 序列产生贡献,由此可以导出:任意一棵 \(M_v < M_u\) 的子树 \(v\) 都可以选择忽略,或在考虑上一棵未被忽略的子树的 \(M_v\) 的限制下记录贡献。因此就无需考虑遍历的顺序,只需将子树按照 \(M_v\) 从大到小排序,初始令 \(f_{u, M_u} \gets 1\),每当合并子树 \(v\) 时枚举 \(i \in [1, M_v]\),令 \(f_{u, M_{v}} f_{v, i} \to f_{u, i}\) 即可。最终 \(f_{1, 1}\) 即为答案。\(\Theta(n ^ 2)\) 的代码

这个 DP 过程可以用比较繁琐的线段树合并优化到 \(\Theta(n \log n)\):容易发现我们只在乎 \(i \in \text{subtree}(u)\) 位置上的 \(f_{u, i}\),其余项的值等同于它的最近后缀位置上的值。对 \(f_u\) 序列建立线段树,动态开点,每个结点维护最靠左的值是什么,查询单点的值是 trivial 的。考虑怎么执行 \(f_{v, i} \to f_{u, i}\) 这一操作。考虑线段树合并的过程,与本题的不同点在于,朴素的写法是 if(!a || !b) return;,而本题则需在此基础上实现一个区间加状物,可以用 lazytag 实现。最后还要实现一个区间赋值或删除操作。精细实现即可做到 \(\Theta(n \log n)\)

虽然这个做法的思维更加直接(可能大众做法也差不多?),但是实现起来较为繁琐,且 \(10 ^ 6\) 的数据范围对大常数时空都不太友好。因此还是学习了一下大众做法。以下为大众做法。


尝试直接对前缀 \(\max\) 序列 DP。记 \(M_u\)\(u\) 子树内的最大值。

考虑一个上升序列可以作为答案的条件是什么。最直白的限制是,这个序列需要作为某个 dfs 序的子序列。但是 dfs 序太多了,尝试分析一些性质来简化限制。由于对于两子树 \(u, v(M_u < M_v)\)(称 \(u\) 为小子树,\(v\) 为大子树),先遍历 \(v\) 会导致 \(u\) 子树不可能出现在答案中,因此除最大子树外,每棵子树都可以选择被计入答案或忽略。所有被计入答案的子树需要按照 \(M\) 从小到大遍历。由此,将每个点的子结点按照 \(M_v\) 从小到大遍历,则任意一个合法的前缀 \(\max\) 序列都是该 dfs 序的一个子序列。

至此直接在这个特殊的 dfs 序列上 DP 即可。记 \(f_i\) 表示序列结尾为 \(i\),考虑所有在 \(i\) dfs 序列之前的点时的答案数。初始 \(f_1 \gets 1\)。首先判断 \(i\) 的祖先是否都小于 \(i\),若不满足则 \(f_i \gets 0\)。记 \(d = \text{LCA}(i, j)\),考虑满足 \(f_j \neq 0\)\(j(j < i)\) 可以转移到 \(i\) 的限制有:

  • \(j\)\(i\) 的祖先,即 \(j = d\)。此时需要 \(j\)\(1 \to i\) 的链上除 \(i\) 外的最大值。
  • \(j\) 不是 \(i\) 的祖先,即 \(j \neq d\)。此时需要 \(d\)\(j\) 方向的子树 \(v\)\(M_v = j\),且 \(j\)\(1 \to i\) 的链上除 \(i\) 外的最大值。

故记 \(u\) 祖先中的最大值为 \(\text{anc}\),维护一棵树状数组,每访问一个点时令 \(f_u \gets t[\text{anc}, u)\),然后在树状数组上加入 \(f_u\),每 dfs 一棵子树后加入 \(f_{M_v}\) 即可。时间复杂度小常数 \(\Theta(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

constexpr int mod = 998244353, N = 1e6 + 5;
namespace basic {
  inline int add(int x, int y) {return (x + y >= mod ? x + y - mod : x + y);}
  inline int dec(int x, int y) {return (x - y < 0 ? x - y + mod : x - y);}
  inline void ad(int &x, int y) {x = add(x, y);}
  inline void de(int &x, int y) {x = dec(x, y);}

  inline int qpow(int a, int b) {
    int r = 1;
    while(b) {
      if(b & 1) r = 1ll * r * a % mod;
      a = 1ll * a * a % mod; b >>= 1;
    }
    return r;
  }
  inline int inv(int x) {return qpow(x, mod - 2);}

  int fac[N], ifac[N];
  inline void fac_init(int n = N - 1) {
    fac[0] = 1;
    for(int i = 1; i <= n; i++)
      fac[i] = 1ll * fac[i - 1] * i % mod;
    ifac[n] = inv(fac[n]);
    for(int i = n - 1; i >= 0; i--)
      ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mod;
  }
  int invx[N];
  inline void inv_init(int n = N - 1) {
    invx[1] = 1;
    for(int i = 2; i <= n; i++)
      invx[i] = 1ll * (mod - mod / i) * invx[mod % i] % mod;
  }
  inline int binom(int n, int m) {
    if(n < m || m < 0) return 0;
    return 1ll * fac[n] * ifac[m] % mod * ifac[n - m] % mod;
  }
}
using namespace basic;

int n;
vector<int> G[N];
int M[N], f[N];

inline void pre_dfs(int u, int fa) {
  if(~fa) G[u].erase(find(G[u].begin(), G[u].end(), fa));
  M[u] = u;
  for(auto v : G[u]) {
    pre_dfs(v, u);
    M[u] = max(M[u], M[v]);
  }
  sort(G[u].begin(), G[u].end(), [](int x, int y) {
    return M[x] < M[y];
  });
}

struct BIT {
  int t[N];
  inline void add(int x, int v) {for(; x <= n; x += x & -x) ad(t[x], v);}
  inline int query(int x) {int r = 0; for(; x; x -= x & -x) ad(r, t[x]); return r;}
} bit;

inline void dfs(int u, int anc) {
  if(u == 1) f[u] = 1;
  else if(u < anc) f[u] = 0;
  else f[u] = dec(bit.query(u), bit.query(anc - 1));
  bit.add(u, f[u]);

  for(auto v : G[u]) {
    dfs(v, max(anc, u));
    bit.add(M[v], f[M[v]]);
  }
  for(auto v : G[u]) {
    bit.add(M[v], mod - f[M[v]]);
  }
  bit.add(u, mod - f[u]);
}

void Main() {
  cin >> n;
  for(int i = 1; i <= n; i++) {
    G[i].clear();
    f[i] = M[i] = 0;
  }
  for(int i = 1; i < n; i++) {
    int u, v; cin >> u >> v;
    G[u].push_back(v), G[v].push_back(u);
  }
  pre_dfs(1, -1), dfs(1, 1);
  cout << f[n] << "\n";
}

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr);
  
  int T; cin >> T;
  while(T--) {
    Main();
  }
}
posted @ 2024-06-06 11:11  ChroneZ  阅读(63)  评论(0)    收藏  举报