杂 D 选 P

[ARC168E] Subsegments with Large Sums

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

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

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

注意二分答案的上界为,在不考虑 k 段限制时,能够选出不交的 s 的段的最大数量对 kmin。原因是 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

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

首先特判掉 u=Mu 的情况,有 fu,=1。由于依次遍历两棵子树 x,y 时,若 Mx>My,则 y 子树不会再对 dfs 序列产生贡献,由此可以导出:任意一棵 Mv<Mu 的子树 v 都可以选择忽略,或在考虑上一棵未被忽略的子树的 Mv 的限制下记录贡献。因此就无需考虑遍历的顺序,只需将子树按照 Mv 从大到小排序,初始令 fu,Mu1,每当合并子树 v 时枚举 i[1,Mv],令 fu,Mvfv,ifu,i 即可。最终 f1,1 即为答案。Θ(n2) 的代码

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

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


尝试直接对前缀 max 序列 DP。记 Muu 子树内的最大值。

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

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

  • ji 的祖先,即 j=d。此时需要 j1i 的链上除 i 外的最大值。
  • j 不是 i 的祖先,即 jd。此时需要 dj 方向的子树 vMv=j,且 j1i 的链上除 i 外的最大值。

故记 u 祖先中的最大值为 anc,维护一棵树状数组,每访问一个点时令 fut[anc,u),然后在树状数组上加入 fu,每 dfs 一棵子树后加入 fMv 即可。时间复杂度小常数 Θ(nlogn)

#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 @   ChroneZ  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示