DS 练习

P9058 [Ynoi2004] rpmtdq

树上支配对模板题。

题解讲得很详细

P10834 [COTS 2023] 题 Zadatak

首先,不难发现由于原来正方形的边长为偶数,所以所有的正方形都可以划分成四个对称的边长为 \(\frac {a_i} 2\) 的小正方形。

由下图可知:

image

第 \(i\) 个 \(L\) 形颜色完全一样,且大小为 \(2\times i - 1\)。

原问题转化为 \(n\) 个长度为 \(a_i\) 的序列初始全为 \(1\),每次选两个序列异或合并,代价是两个序列按位与为 \(1\) 的位置对应的代价。

差分操作后,转化为有 \(n\) 个序列初始 \(a_n\) 为 \(1\),每次选两个序列异或合并,代价是两个序列的后缀异或和的按位与为 \(1\) 的位置对应的代价。

可以用线段树合并维护。

线段树维护当前区间的异或和,为 \(0\) 的位置的总代价,为 \(1\) 的位置的总代价。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("P10834_4.in", "r", stdin);
  freopen("1.out", "w", stdout);
}
using ll = long long;
const int kN = 1e5 + 7;
int n, a[kN], rt[kN << 1], tot;
ll ans;
struct SGT{
  struct node{
    int x, ls, rs;
    ll s[2]; 
  }tree[kN * 21];
  ll Get(int l, int r){
    return 1ll * r * r - 1ll * (l - 1) * (l - 1);
  }
  void pushup(int x, int l, int r){
    int mid = l + r >> 1;
    tree[x].x = tree[tree[x].ls].x ^ tree[tree[x].rs].x;
    tree[x].s[0] = (tree[x].rs ? tree[tree[x].rs].s[0] : Get(mid + 1, r)) + (tree[x].ls ? tree[tree[x].ls].s[tree[tree[x].rs].x] : (!tree[tree[x].rs].x ? Get(l, mid) : 0ll));
    tree[x].s[1] = (tree[x].rs ? tree[tree[x].rs].s[1] : 0ll) + (tree[x].ls ? tree[tree[x].ls].s[tree[tree[x].rs].x ^ 1] : (tree[tree[x].rs].x ? Get(l, mid) : 0ll));
    return ;
  }
  void update(int &x, int l, int r, int pos){
    if(!x) x = ++tot;
    if(l == r){
      tree[x].x = 1;
      tree[x].s[0] = 0, tree[x].s[1] = 2ll * pos - 1;
      return ;
    }
    int mid = l + r >> 1;
    if(pos <= mid) update(tree[x].ls, l, mid, pos);
    else update(tree[x].rs, mid + 1, r, pos);
    pushup(x, l, r);
    return ;
  }
  void merge(int &x, int xa, int xb, int taga, int tagb, int l, int r){
    if(!xa && !xb){
      if(taga && tagb) ans += Get(l, r);
      return ;
    }
    if(!xa){
      if(taga) ans += tree[xb].s[tagb ^ 1];
      x = xb;
      return ;
    }
    if(!xb){
      if(tagb) ans += tree[xa].s[taga ^ 1];
      x = xa;
      return ;
    }
    if(l == r){
      if((tree[xa].x ^ taga) & (tree[xb].x ^ tagb)) ans += 2ll * l - 1;
      x = xa;
      tree[x].x ^= tree[xb].x;
      tree[x].s[tree[x].x] = 2ll * l - 1;
      tree[x].s[tree[x].x ^ 1] = 0;
      return ;
    }
    x = xa;
    int mid = l + r >> 1;
    merge(tree[x].ls, tree[xa].ls, tree[xb].ls, taga ^ tree[tree[xa].rs].x, tagb ^ tree[tree[xb].rs].x, l, mid);
    merge(tree[x].rs, tree[xa].rs, tree[xb].rs, taga, tagb, mid + 1, r);
    pushup(x, l, r);
  }
}sgt;
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  for(int i = 1; i <= n; i++){
    cin >> a[i];
    a[i] /= 2;
    sgt.update(rt[i], 1, 1e6, a[i]);
  }
  int m = n - 1;
  while(m--){
    int x, y;
    cin >> x >> y;
    ans = 0;
    sgt.merge(rt[++n], rt[x], rt[y], 0, 0, 1, 1e6);
    cout << ans * 4ll << '\n'; 
  }

  return 0;
}

P14061 【MX-X21-T6】[IAMOI R5] 希望有羽毛和翅膀

挖掘性质:只会选长度为 \(k\) 或 \(k + 1\) 的子串,答案至多为 \(\frac n k\)。

发现讨论 \(k\) 有点复杂,使用 manacher 算法后,只需考虑回文中心即可,且对于一个回文中心,所有的回文串长度固定为奇数或偶数,所以不需要讨论 \(k\) 和 \(k + 1\)。

由于答案至多为 \(\frac n k\),考虑根号分治。

设 \(nxt\) 为下一个符合题意的回文中心。

当 \(k < B\) 时,考虑套用弹飞绵羊的 \(trick\),设 \(jump_i\) 表示从 \(i\) 跳出当前块跳到的位置,\(f_i\) 表示此过程中跳跃的次数。这样预处理时空复杂度为 \(\Theta(nB)\) ,询问的复杂度为 \(\Theta(q\times \frac n B)\)。

但是预处理的空间复杂度 \(\Theta(nB)\) 很不牛,发现问题是对于所有 \(k<B\) ,每个位置都存储了其对应的 \(nxt\)。如果每个位置都只存储了同一种 \(k\),那空间复杂度就变成线性的了。所以可以想到把询问离线,将 \(k\) 从小到大排序,每次遇到新的 \(k\),再把原来位置的 \(nxt\) 给覆盖,这样预处理的空间复杂度就做到 \(\Theta(n)\) 了。

当 \(k \ge B\) 时,由于答案很小,可以直接暴力跳 \(nxt\),问题在于如何快速求出 \(nxt\)。这个 \(nxt\) 由于量级较大,是无法预处理的。

记 \(p_i\) 表示以 \(i\) 为回文中心的最长回文半径,那么 \(nxt_i\) 实际上表示 \(i\) 右边第一个满足 \(p_j \ge k\) 的 \(j\)。这可以 st 表加二分做到 \(\Theta(q\times \frac n B \times log n)\)。考虑如何优化。

有一种思考方式是按 \(p\) 的值域从小到大扫描线,原问题转化为删除一个位置,查询一个位置的后继,可以用序列并查集维护。具体为将 \(i\) 和 \(i+1\) 合并,查询 \(i\) 所在的祖先。这样在均摊 \(\Theta(1)\) 的时间内可以查找到 \(nxt\),因为每次询问最多跳 \(\frac n B\) 次,所以时间复杂度为 \(\Theta((n+q)\times \frac n B)\)。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
  freopen("1.out", "w", stdout);
}
const int kN = 4e5 + 7;
int B, p[kN], n, Q, ans[kN];
struct node{
  int l, r, k, id;
}q1[kN], q2[kN];
char s[kN];
string tmp;
static inline int To(int pos, int k) {
    return pos + k + (((pos ^ k) & 1) ^ 1);
}
namespace Sol1{
  int m, t, jump[kN], f[kN], pos[kN], L[kN], R[kN], nxt[kN], nxtm[kN];
  void Ins(int l, int r, int k, int id){
    q1[++m] = {l, r, k, id};
  }
  void main(){ 
    t = n / B;
    for(int i = 1; i <= t; i++) L[i] = R[i - 1] + 1, R[i] = i * B;
    if(R[t] < n){
      ++t; L[t] = R[t - 1] + 1; R[t] = n;
    }
    for(int i = 1; i <= t; i++){
      for(int j = L[i]; j <= R[i]; j++){
        pos[j] = i;
      }
    }
    sort(q1 + 1, q1 + m + 1, [&](node x, node y){return x.k < y.k;});
    int lst = 0;
    for(int i = 1; i <= m; i++){
      int k = q1[i].k;
      if(lst != k){
        nxt[n + 1] = jump[n + 1] = n + 1;
        for(int i = n, j = n + 1; i >= 1; i--){
          if(p[i] >= k) j = i;
          nxtm[i] = j;
          if(p[i] >= k){
            f[i] = 1;
            if(To(i, k) + k <= n) jump[i] = nxt[i] = nxtm[To(i, k) + k];
            else jump[i] = nxt[i] = n + 1;
            if(jump[i] <= R[pos[i]]) f[i] += f[jump[i]], jump[i] = jump[jump[i]];
          }
        }
        lst = k;
      }
      int l = q1[i].l, r = q1[i].r, id = q1[i].id;
      if(l + k > r) continue;
      int x = nxtm[l + k], res = 0;
      while(To(jump[x], k) <= r) res += f[x], x = jump[x];
      while(To(nxt[x], k) <= r) res++, x = nxt[x];
      if(To(x, k) <= r) res++;
      ans[id] = res;
    }
    return ;
  }
}
namespace Sol2{
  int m;
  void Ins(int l, int r, int k, int id){
    q2[++m] = {l, r, k, id};
  }
  struct DSU{
    int fa[kN];
    void init(){
      for(int i = 1; i <= n + 1; i++) fa[i] = i;
    }
    int find(int x){
      if(fa[x] != x) fa[x] = find(fa[x]);
      return fa[x];
    }
    void Del(int x){
      fa[x] = find(x + 1);
    }
  }dsu;
  int tp[kN];
  void main(){
    for(int i = 1; i <= n; i++) tp[i] = i;
    sort(tp + 1, tp + n + 1, [&](int x, int y){return p[x] < p[y];});
    sort(q2 + 1, q2 + m + 1, [&](node x, node y){return x.k < y.k;});
    int lst = 0;
    dsu.init();
    for(int i = 1, j = 1; i <= m; i++){
      int l = q2[i].l, r = q2[i].r, k = q2[i].k, id = q2[i].id;
      while(j <= n && p[tp[j]] < k) dsu.Del(tp[j]), ++j;
      int now, cnt = 0;
      if(l + k <= n) now = dsu.find(l + k);
      else continue;
      while(To(now, k) <= r){
        ++cnt;
        if(To(now, k) + k <= r) now = dsu.find(To(now, k) + k);
        else break;
      }
      ans[id] = cnt;
    }
    return ;
  }
}
void init(){
  cin >> n >> Q;
  cin >> tmp;
  n = 0;
  s[++n] = '#';
  for(int i = 0; i < tmp.size(); i++){
    s[++n] = tmp[i];
    s[++n] = '#';
  }
  B = 700;
  int j = 0, r = 0;
  for(int i = 1; i <= n; i++){
    if(r >= i) p[i] = min(p[2 * j - i], r - i + 1);
    else p[i] = 1;
    while(i + p[i] <= n && s[i + p[i]] == s[i - p[i]]) ++p[i];
    if(i + p[i] - 1 > r){
      r = i + p[i] - 1;
      j = i;
    }
  }
  for(int i = 1; i <= n; i++) p[i]--;
  for(int i = 1; i <= Q; i++){
    int l, r, k;
    cin >> l >> r >> k;
    l = 2 * l - 1, r = 2 * r + 1;
    if(k < B) Sol1::Ins(l, r, k, i);
    else Sol2::Ins(l, r, k, i);
  }
  return ;
}
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  init();
  Sol1::main();
  Sol2::main();
  for(int i = 1; i <= Q; i++) cout << ans[i] << '\n';
  return 0;
}

P7230 [COCI 2015/2016 #3] NEKAMELEONI

此题可以加强到与 \(k\) 无关的做法,设 \(f_i\) 表示以 \(i\) 为左端点时最小的有端点。

发现修改一个位置的值很难维护 \(f_i\),但是删除一个数是比较容易维护的。设删除了一个位置 \(p\),记 \(l\),\(r\) 为上一个和下一个与 \(a_p\) 相等的下标,那么 \(\forall i \in (l,p]\),\(f_i=max(f_i,r)\)。这可以用吉司机线段树维护,当然容易发现 \(f_i\) 是单调不降的,所以可以线段树二分加区间赋值解决。

于是我们得到了最后的做法,一开始把所有的数包括修改后的数加入原序列,然后对删除操作线段树分治即可。时间复杂度 \(\Theta(q\log q \log n)\)

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kN = 1e5 + 7;
const int INF = 2e9;
int f[kN], n, k, m, ok[kN], a[kN], ans[kN];
multiset<int> s[kN];
vector<pii> q[kN << 2];
struct Stack{
  int x;
  int mx, mn;
  int tag;
}stk[kN * 20 * 20];
int top;
struct SGT{
  struct node{
    int mx, mn, tag;
  }tree[kN * 4];
  void pushup(int x){
    stk[++top] = {x, tree[x].mx, tree[x].mn, tree[x].tag};
    tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
    tree[x].mn = min(tree[x << 1].mn, tree[x << 1 | 1].mn);
    return ;
  }
  void maketag(int x, int l, int r, int v){
    stk[++top] = {x, tree[x].mx, tree[x].mn, tree[x].tag};
    tree[x].mx = v;
    tree[x].mn = v - r + 1;
    tree[x].tag = v;
    return ;
  }
  void pushdown(int x, int l, int r){
    if(tree[x].tag){
      int mid = l + r >> 1;
      stk[++top] = {x, tree[x].mx, tree[x].mn, tree[x].tag};
      maketag(x << 1, l, mid, tree[x].tag);
      maketag(x << 1 | 1, mid + 1, r, tree[x].tag);
      tree[x].tag = 0;
    }
    return ;
  }
  void update(int x, int l, int r, int L, int R, int v){
    if(L <= l && r <= R){
      maketag(x, l, r, v);
      return ;
    } pushdown(x, l, r);
    int mid = (l + r) / 2;
    if(L <= mid) update(x << 1, l, mid, L, R, v);
    if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
    pushup(x);
    return ;
  }
  int query(int x, int l, int r, int k){
    if(l == r){
      if(tree[x].mx >= k) return l;
      return n + 1;
    } pushdown(x, l, r);
    int mid = l + r >> 1;
    if(tree[x << 1].mx < k) return query(x << 1 | 1, mid + 1, r, k);
    return query(x << 1, l, mid, k);
  }
}sgt;
void Upd(int x, int l, int r, int L, int R, pii v){
  if(L > R) return ;
  if(L <= l && r <= R){
    q[x].push_back(v);
    return ;
  }
  int mid = l + r >> 1;
  if(L <= mid) Upd(x << 1, l, mid, L, R, v);
  if(mid < R) Upd(x << 1 | 1, mid + 1, r, L, R, v);
  return ;
}
void Add(int x, int y){
  s[y].erase(s[y].lower_bound(x));
  auto it = s[y].lower_bound(x);
  if(*it == x) return ;
  int l = *prev(it) + 1, r = *it, v;
  l = max(l, 1), v = r;
  r = min(r, sgt.query(1, 1, n, v) - 1);
  if(l > r) return ;
  sgt.update(1, 1, n, l, r, v);
  return ;
}
void Sol(int x, int l, int r){
  int ltop = top;
  for(auto &[i, j] : q[x]) Add(i, j);
  if(l == r){
    ans[l] = sgt.tree[1].mn;
    if(ans[l] > n) ans[l] = -1;
  }
  else{
    int mid = l + r >> 1;
    Sol(x << 1, l, mid);
    Sol(x << 1 | 1, mid + 1, r);
  }
  while(top > ltop){
    Stack tmp = stk[top];
    sgt.tree[tmp.x].mx = tmp.mx;
    sgt.tree[tmp.x].mn = tmp.mn;
    sgt.tree[tmp.x].tag = tmp.tag;
    top--;
  }
  for(auto &[i, j] : q[x]) s[j].insert(i);
  return ;
}
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> k >> m;
  for(int i = 1; i <= n; i++){
    cin >> a[i];
    s[a[i]].insert(i);
  }
  for(int i = 1; i <= m; i++){
    int op, p, v;
    cin >> op;
    if(op == 1){
      cin >> p >> v;
      s[v].insert(p);
      Upd(1, 1, m, 1, i - 1, mp(p, v));
      Upd(1, 1, m, i, m, mp(p, a[p]));
      a[p] = v;
    }
    else ok[i] = 1;
  }
  for(int i = 1; i <= k; i++) s[i].insert(-INF), s[i].insert(INF);
  for(int i = 1; i <= k; i++) f[1] = max(f[1], *s[i].lower_bound(1)), sgt.update(1, 1, n, 1, 1, f[1]);
  for(int i = 2; i <= n; i++){
    f[i] = f[i - 1];
    for(int j = 1; j <= k; j++){
      f[i] = max(f[i], *s[j].lower_bound(i));
    }
    sgt.update(1, 1, n, i, i, f[i]);
  }
  Sol(1, 1, m);
  for(int i = 1; i <= m; i++){
    if(ok[i]) cout << ans[i] << '\n';
  }
  return 0;
}

P16458 [UOI 2026] mex plus

像这种二选一问题可以考虑图论建模,转化成边的定向的问题。假设有边 \(x\to y\),表示 \(x\) 放入 \(A\) 序列,\(y\) 放入 \(B\) 序列。那么最后答案转化为编号最小的入度为 \(0\) 的点和编号最小的出度为 \(0\) 的点的编号之和。对于每个连通块单独考虑,最后取 min 即可。

如果当前联通块有环,环上的结点入度出度都不为 \(0\)。度数为 \(1\) 的叶子肯定会影响答案,我们不妨把这些结点放入同一个序列中,这样对答案取 min 的影响最小。假设这个序列为 \(A\),那所有叶子结点都有边指向父结点,接着只需每个结点依次指向父结点,那剩下的结点入度和出度都不为 \(0\),不会影响答案。

如果当前连通块无环,和有环联通块做法类似。唯一有问题的是到根结点 \(rt\),\(rt\) 会被放入 \(B\) 序列影响最后的答案,如何解决 \(?\) 令编号最大的结点为根即可。

最后 \(mex(A)\) 就是所有联通块中度数 \(\le 1\) 的结点的编号最小值,\(mex(B)\) 就是所有无环联通块中编号最大值的最小值。

剩下的用线段树分治维护动态图即可,注意可撤销并查集不能路径压缩,要维护 \(height\) 保证期望树高为 \(\log n\),否则复杂度容易退化。

时间复杂度为 \(\Theta(q \log q \log n)\)。

下面我的代码 \(A\),\(B\) 序列表示的含义相反。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("4.in", "r", stdin);
  freopen("1.out", "w", stdout);
}
const int kN = 2e5 + 7;
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
map<pii, vector<int> > ma;
int n, Q, tim[kN << 1], m, ans[kN], M;
vector<int> lsh;
struct Query{
  int op, x, y;
}q[kN << 1];
vector<Query> tree[kN << 2];
struct Heap{
  priority_queue<int, vector<int>, greater<int> > que1, que2;
  void Ins(int x){
    que1.push(x);
  }
  void Del(int x){
    que2.push(x);
  }
  int query(){
    while(!que2.empty() && que1.top() == que2.top()){
      que1.pop(), que2.pop();
    }
    assert(que1.size());
    return que1.top();
  }
}A, B;
struct Stack{
  int x, y;
  int fx, fy;
  int hx, hy;
  int mxx, mxy;
}stk[kN * 40];
int top, deg[kN << 3];
struct DSU{
  int fa[kN << 3], siz[kN << 3], ecnt[kN << 3], mx[kN << 3], height[kN << 3];
  void init(){
    for(int i = 0; i <= M; i++) fa[i] = i, siz[i] = 1, ecnt[i] = 0, height[i] = 1, mx[i] = i;
  }
  int find(int x){
    return fa[x] == x ? fa[x] : find(fa[x]); 
  }
  void merge(int x, int y){
    int fx = find(x), fy = find(y);
    if(fx == fy){
      stk[++top] = {x, y, fx, fy, height[fx], height[fy], mx[fx], mx[fy]};
      if(ecnt[fx] < siz[fx]) A.Del(mx[fx]);
      ecnt[fx]++;
      if(ecnt[fx] < siz[fx]) A.Ins(mx[fx]);
      return ;
    }
    if(height[fx] > height[fy]) swap(fx, fy), swap(x, y);
    stk[++top] = {x, y, fx, fy, height[fx], height[fy], mx[fx], mx[fy]};
    if(ecnt[fx] < siz[fx]) A.Del(mx[fx]);
    if(ecnt[fy] < siz[fy]) A.Del(mx[fy]);
    fa[fx] = fy, siz[fy] += siz[fx], ecnt[fy] += ecnt[fx] + 1, height[fy] += (height[fx] == height[fy]), mx[fy] = max(mx[fy], mx[fx]);
    if(ecnt[fy] < siz[fy]) A.Ins(mx[fy]);
    return ;
  }
  void Imerge(int x, int y, int fx, int fy, int hx, int hy, int mxx, int mxy){
    if(fx == fy){
      if(ecnt[fx] < siz[fx]) A.Del(mx[fx]);
      ecnt[fx]--;
      if(ecnt[fx] < siz[fx]) A.Ins(mx[fx]);
      return ;
    }
    if(ecnt[fy] < siz[fy]) A.Del(mx[fy]);
    fa[fx] = fx;
    height[fy] = hy;
    siz[fy] -= siz[fx];
    ecnt[fy] -= (ecnt[fx] + 1);
    mx[fy] = mxy;
    if(ecnt[fx] < siz[fx]) A.Ins(mx[fx]);
    if(ecnt[fy] < siz[fy]) A.Ins(mx[fy]);
  }
}dsu;
void Upd(int x, int l, int r, int L, int R, Query v){
  if(L <= l && r <= R){
    tree[x].push_back(v);
    return ;
  }
  int mid = (l + r) / 2;
  if(L <= mid) Upd(x << 1, l, mid, L, R, v);
  if(mid < R) Upd(x << 1 | 1, mid + 1, r, L, R, v);
  return ;
}
void Add(int x, int y){
  if(deg[x] == 1) B.Del(x);
  ++deg[x];
  if(deg[y] == 1) B.Del(y);
  ++deg[y];
  dsu.merge(x, y);
}
void Withdraw(int x, int y, int fx, int fy, int hx, int hy, int mxx, int mxy){
  if(deg[x] == 2) B.Ins(x);
  --deg[x];
  if(deg[y] == 2) B.Ins(y);
  --deg[y];
  dsu.Imerge(x, y, fx, fy, hx, hy, mxx, mxy);
}
void Solve(int x, int l, int r){
  int lsttop = top;
  for(auto i : tree[x]) Add(i.x, i. y);
  if(l == r){
    ans[l] = A.query() + B.query(); 
  }
  if(l != r){
    int mid = l + r >> 1;
    Solve(x << 1, l, mid);
    Solve(x << 1 | 1, mid + 1, r);    
  }
  while(top > lsttop){
    int x = stk[top].x, y = stk[top].y;
    int fx = stk[top].fx, fy = stk[top].fy;
    int hx = stk[top].hx, hy = stk[top].hy;
    int mxx = stk[top].mxx, mxy = stk[top].mxy;
    Withdraw(x, y, fx, fy, hx, hy, mxx, mxy);
    top--;
  }
  return ;
}
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> Q;
  for(int i = 1; i <= n; i++){
    cin >> q[i].x >> q[i].y;
    q[i].op = 1;
  }
  for(int i = 1; i <= Q; i++){
    char op;
    cin >> op >> q[n + i].x >> q[n + i].y;
    if(op == '+') q[n + i].op = 1;
    else q[n + i].op = -1;
  }
  for(int i = 1; i <= n + Q; i++){
    if(q[i].x > 2 * (n + Q)) lsh.push_back(q[i].x);
    if(q[i].y > 2 * (n + Q)) lsh.push_back(q[i].y);
  }
  sort(lsh.begin(), lsh.end());
  auto lshcnt = unique(lsh.begin(), lsh.end());
  lsh.erase(lshcnt, lsh.end());
  int tmp = lshcnt - lsh.begin();
  for(int i = 1; i <= n + Q; i++){
    if(q[i].x > 2 * (n + Q)) q[i].x = 2 * (n + Q) + lower_bound(lsh.begin(), lsh.end(), q[i].x) - lsh.begin() + 1;
    if(q[i].y > 2 * (n + Q)) q[i].y = 2 * (n + Q) + lower_bound(lsh.begin(), lsh.end(), q[i].y) - lsh.begin() + 1;
  }
  M = 2 * (n + Q) + tmp;
  for(int i = 0; i <= M; i++) A.Ins(i), B.Ins(i);
  for(int i = 1; i <= n + Q; i++) tim[i] = -1;
  for(int i = 1; i <= n; i++){
    ma[mp(q[i].x, q[i].y)].push_back(i);
    tim[i] = 0;
  }
  for(int i = n + 1; i <= n + Q; i++){
    if(q[i].op == 1){
      ma[mp(q[i].x, q[i].y)].push_back(i);
      tim[i] = i - n;
    }
    else{
      int j = ma[mp(q[i].x, q[i].y)].back();
      ma[mp(q[i].x, q[i].y)].pop_back();
      Upd(1, 0, Q, tim[j], i - n - 1, q[j]);
      tim[j] = -1;
    }
  }
  for(int i = 1; i <= n + Q; i++){
    if(tim[i] != -1){
      Upd(1, 0, Q, tim[i], Q, q[i]);
      tim[i] = -1;
    }
  }
  dsu.init();
  Solve(1, 0, Q);
  for(int i = 0; i <= Q; i++) cout << ans[i] << '\n';
  return 0;
}

P7560 [JOISC 2021] フードコート (Day1)

学习了 这篇文章 的整体二分 \(\Theta(q\log n)\) 做法,本质上就是将区间修改、单点查询问题转化成先把询问和修改按下标排序,然后差分将内部转化成线性。

这道题思考了很久都觉得无法二分,因为既有加也有减。实际上我们可以忽略 离开事件,把它看作 白嫖事件 中 \(B\) 的增量即可,这一部分可以先预处理。这样就有单调性,可以二分了。

有个小细节,如果有无解的情况,可以插入一个 \(Q+1\) 的策略,使其强制有解,这样写很方便,只需要最后输出答案的时候判断即可。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("21.in", "r", stdin);
  freopen("1.out", "w", stdout);
}
using ll = long long;
const ll INF = 1e18;
const int kN = 3e5 + 7;
int n, m, Q, cnt;
struct BIT{
  ll tree[kN];
  void update(int x, ll v){
    for(int i = x; i <= n; i += i & -i) tree[i] += v;
  }
  ll query(int x){
    ll res = 0;
    for(int i = x; i; i -= i & -i) res += tree[i];
    return res;
  }
}bit;
struct SGT{
  struct node{
    ll mx;
    ll taga, tagb;
  }tree[kN << 2];
  void pushup(int x){
    tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
    return ;
  }
  void maketag(int x, ll v1, ll v2){
    tree[x].mx += v1, tree[x].taga += v1;
    if(tree[x].tagb > -INF) tree[x].tagb += v1;
    tree[x].mx = max(tree[x].mx, v2);
    tree[x].tagb = max(tree[x].tagb, v2);
  }
  void pushdown(int x){
    maketag(x << 1, tree[x].taga, tree[x].tagb);
    maketag(x << 1 | 1, tree[x].taga, tree[x].tagb);
    tree[x].taga = 0, tree[x].tagb = -INF;
  }
  void build(int x, int l, int r){
    tree[x].mx = tree[x].taga = 0, tree[x].tagb = -INF;
    if(l == r) return ;
    int mid = l + r >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    pushup(x);
    return ;
  }
  void update(int x, int l, int r, int L, int R, ll v1, ll v2){
    if(L <= l && r <= R){
      maketag(x, v1, v2);
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(L <= mid) update(x << 1, l, mid, L, R, v1, v2);
    if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v1, v2);
    pushup(x);
    return ;
  }
  ll Qmax(int x, int l, int r, int pos){
    if(l == r) return tree[x].mx;
    pushdown(x);
    int mid = l + r >> 1;
    if(pos <= mid) return Qmax(x << 1, l, mid, pos);
    else return Qmax(x << 1 | 1, mid + 1, r, pos);
  }
}sgt;
struct Query{
  int op, x, id;
  ll v;
}q[kN << 2], p[kN << 2];
int col[kN], ans[kN];
void Sol(int l, int r, int L, int R){
  int ql = L - 1, qr = R + 1;
  int mid = l + r >> 1;
  ll sum = 0;
  for(int i = L; i <= R; i++){
    if(q[i].op == 1){
      if(q[i].id <= mid) sum += q[i].v, p[++ql] = q[i];
      else p[--qr] = q[i];
    }
    else{
      if(q[i].v <= sum) p[++ql] = q[i];
      else q[i].v -= sum, p[--qr] = q[i];
    }
  }
  for(int i = L; i <= ql; i++) q[i] = p[i];
  for(int i = ql + 1; i <= R; i++) q[i] = p[R - (i - ql) + 1];
  if(l == r){
    for(int i = L; i <= R; i++){
      if(q[i].op == 3) ans[q[i].id] = l;
    }
    return ;
  }
  Sol(l, mid, L, ql);
  Sol(mid + 1, r, qr, R);
  return ;
}
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m >> Q;
  sgt.build(1, 1, n);
  for(int i = 1; i <= Q; i++){
    int op, l, r, c, k, a;
    ll b;
    cin >> op;
    if(op == 1){
      cin >> l >> r >> c >> k;
      q[++cnt] = {op, l, i, k};
      q[++cnt] = {op, r + 1, i, -k};
      col[i] = c;
      bit.update(l, k); bit.update(r + 1, -k);
      sgt.update(1, 1, n, l, r, k, -INF);
    }
    else if(op == 2){
      cin >> l >> r >> k;
      sgt.update(1, 1, n, l, r, -k, 0);
    }
    else{
      cin >> a >> b;
      q[++cnt] = {op, a, i, bit.query(a) - sgt.Qmax(1, 1, n, a) + b};
    }
  }
  q[++cnt] = {1, 1, Q + 1, INF};
  sort(q + 1, q + cnt + 1, [&](Query x, Query y){return x.x == y.x ? x.op < y.op : x.x < y.x;});
  Sol(1, Q + 1, 1, cnt);
  for(int i = 1; i <= Q; i++){
    if(ans[i]){
      if(ans[i] < i) cout << col[ans[i]] << '\n';
      else cout << 0 << '\n';
    }    
  }
  return 0;
}

P9340 [JOIST 2023] 旅行 / Tourism

答案为虚树的边数 \(+1\),如何求虚树的边数?

经典结论:虚树的边数 $\times 2 = $ 关键点按 \(dfs\) 序排序后,相邻两点的距离之和。发现删除操作可以直接用链表维护,加入操作必须要带 \(\log\);又因为区间询问无修改,可以离线。所以直接考虑回滚莫队,只删除不增加。复杂度 \(\Theta(n\sqrt n)\)。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
const int kN = 1e5 + 7;
int n, m, Q, c[kN], Log2[kN], ans[kN], B, pos[kN], L[kN], R[kN], t, idx, dfn[kN], id[kN], st[kN][21], dep[kN];
vector<int> ve[kN];
struct Query{
  int x, y, id;
}q[kN];
void dfs(int x, int fa){
  dfn[x] = ++idx, id[idx] = x;
  st[dfn[x]][0] = dfn[fa];
  dep[x] = dep[fa] + 1;
  for(int j : ve[x]){
    if(j == fa) continue;
    dfs(j, x);
  }
  return ;
}
int lca(int x, int y){
  if(x == y) return x;
  x = dfn[x], y = dfn[y];
  if(x > y) swap(x, y);
  ++x;
  int k = Log2[y - x + 1];
  return id[min(st[x][k], st[y - (1 << k) + 1][k])];
}
int dis(int x, int y){
  if(x == 0  || y == 0) return 0;
  return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
int pre[kN], nxt[kN], cnt[kN], res;
void Del(int x){
  x = dfn[c[x]];
  --cnt[x];
  if(cnt[x] == 0){
    res -= dis(id[pre[x]], id[x]);
    res -= dis(id[nxt[x]], id[x]);
    res += dis(id[pre[x]], id[nxt[x]]);
    pre[nxt[x]] = pre[x];
    nxt[pre[x]] = nxt[x];    
  }
  return ;
}
void Add(int x){
  x = dfn[c[x]];
  if(cnt[x] == 0){
    res -= dis(id[pre[x]], id[nxt[x]]);
    res += dis(id[pre[x]], id[x]);
    res += dis(id[nxt[x]], id[x]);
    pre[nxt[x]] = x;
    nxt[pre[x]] = x;
  }
  ++cnt[x];
  return ;
}
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n >> m >> Q;
  for(int i = 2; i <= n; i++) Log2[i] = Log2[i >> 1] + 1;
  B = sqrt(m);
  t = m / B;
  for(int i = 1; i <= t; i++) L[i] = R[i - 1] + 1, R[i] = i * B;
  if(R[t] < m){
    ++t;
    L[t] = R[t - 1] + 1, R[t] = m;
  }
  for(int i = 1; i <= t; i++){
    for(int j = L[i]; j <= R[i]; j++) pos[j] = i;
  }
  for(int i = 1; i < n; i++){
    int u, v; cin >> u >> v;
    ve[u].push_back(v), ve[v].push_back(u);
  }
  dfs(1, 0);
  for(int j = 1; j <= 20; j++){
    for(int i = 1; i + (1 << j) - 1 <= n; i++) st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
  }
  for(int i = 1; i <= m; i++) cin >> c[i];
  for(int i = 1; i <= Q; i++){
    cin >> q[i].x >> q[i].y, q[i].id = i;
  }
  sort(q + 1, q + Q + 1, [&](Query x, Query y){return pos[x.x] != pos[y.x] ? x.x < y.x : x.y > y.y;});
  int p = 0;
  for(int i = 1; i <= t; i++){
    for(int j = 1; j <= n; j++) cnt[j] = pre[j] = nxt[j] = 0;
    for(int j = L[i]; j <= m; j++) ++cnt[dfn[c[j]]];
    int lst = 0;
    for(int j = 1; j <= n; j++){
      pre[j] = lst;
      if(cnt[j]) lst = j;
    }
    for(int j = 1; j <= n; j++){
      if(!pre[j]) pre[j] = lst;
    }
    lst = 0;
    for(int j = n; j >= 1; j--){
      nxt[j] = lst;
      if(cnt[j]) lst = j;
    }
    for(int j = 1; j <= n; j++){
      if(!nxt[j]) nxt[j] = lst;
    }
    res = 0;
    for(int j = 1; j <= n; j++){
      if(cnt[j]) res += dis(id[pre[j]], id[j]);
    }
    int l = L[i], r = m;
    while(p < Q && pos[q[p + 1].x] == i){
      ++p;
      while(r > q[p].y) Del(r--);
      while(l < q[p].x) Del(l++);
      ans[q[p].id] = res / 2 + 1;
      while(l > L[i]) Add(--l);
    }
  }
  for(int i = 1; i <= Q; i++) cout << ans[i] << '\n';
  return 0;
}

P9596 [JOI Open 2018] 冒泡排序 2 / Bubble Sort 2

设 \(f_i\) 表示 \([1,i]\) 内大于 \(a_i\) 的数的个数,\(g_i\) 为 \([1,i]\) 内 \(\le a_i\) 的数的个数,有 \(f_i=i-g_i\)。

直接用树套树维护是 \(\Theta(n \log^2 n)\) 的。

有冒泡排序的性质可知 \(ans=\max_{i=1}^n{f_i}\)。

最 关键的一步 来了,注意到当 \(g_i\) 表示 \([1,n]\) 内 \(\le a_i\) 的数的个数时,\(ans\) 保持不变。

所以我们用权值线段树维护全局 \(\le v\) 的个数,全局 \(f_i\) 的最大值即可。

复杂度 \(\Theta(n \log n)\)。

很多这种用树套树维护的题,可以发现有一维的限制是多余的,从而转化为一维限制。

当然这题还有很多代码细节值得学习。

如有 \(a_i\) 相同的值,我们视左边的小于右边的。

思考一下,如果不这么做的话,那么每次删除 \(a_i\) 时,我们还需要用 \(\text{set}\) 来维护值为 \(a_i\) 的 \(f_i\) 的最大值,复杂度又退化到 \(\Theta(n \log^2 n)\) 了。

而如果这么做的话,每次可以直接删除 \(a_i\) 的影响,且由于答案是取 \(\max\),\(a_i\) 相同的,只有可能最右边的 \(a_i\) 成为答案,所以最终不会影响答案。

如何处理,令所有的 \(a_i=n\times a_i+i\) 即可。

将所有的 \(a_i\) 和修改的值离散化后在插入线段树,避免动态开点的内存过大。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
const ll INF = 1e18;
const int kN = 5e5 + 7;
int n, Q, b[kN], c[kN];
ll a[kN];
struct Query{
  int x;
  ll v;
}q[kN];
vector<ll> lsh;
int cnt;
struct SGT{
  struct node{
    int s, mx, tag;
  }tree[kN << 3];
  void pushup(int x){
    tree[x].s = tree[x << 1].s + tree[x << 1 | 1].s;
    tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
    return ;
  }
  void maketag(int x, int v){
    tree[x].mx += v;
    tree[x].tag += v;
    return ;
  }
  void pushdown(int x){
    maketag(x << 1, tree[x].tag);
    maketag(x << 1 | 1, tree[x].tag);
    tree[x].tag = 0;
    return ;
  }
  void Ins(int x, int l, int r, int pos, int v, int v2){
    if(l == r){
      tree[x].s += v;
      tree[x].mx = v2;
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(pos <= mid) Ins(x << 1, l, mid, pos, v, v2);
    else Ins(x << 1 | 1, mid + 1, r, pos, v, v2);
    pushup(x);
    return ;
  }
  void Del(int x, int l, int r, int pos){
    if(l == r){
      tree[x].s = 0, tree[x].mx = 0;
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(pos <= mid) Del(x << 1, l, mid, pos);
    else Del(x << 1 | 1, mid + 1, r, pos);
    pushup(x);
    return ;
  }
  int query(int x, int l, int r, int L, int R){
    if(L <= l && r <= R) return tree[x].s;
    pushdown(x);
    int mid = l + r >> 1, ans = 0;
    if(L <= mid) ans += query(x << 1, l, mid, L, R);
    if(mid < R) ans += query(x << 1 | 1, mid + 1, r, L, R);
    pushup(x);
    return ans;
  }
  void update(int x, int l, int r, int L, int R, int v){
    if(L <= l && r <= R){
      maketag(x, v);
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(L <= mid) update(x << 1, l, mid, L, R, v);
    if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
    pushup(x);
    return ;
  }
}sgt;
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> Q;
  for(int i = 1; i <= n; i++){
    cin >> a[i];
    a[i] = 1ll * n * a[i] + 1ll * i;
    b[i] = i;
    lsh.push_back(a[i]);
  }
  sort(b + 1, b + n + 1, [&](int x, int y){return a[x] < a[y];});
  for(int i = 1; i <= n; i++) c[b[i]] = i;
  for(int i = 1; i <= Q; i++){
    cin >> q[i].x >> q[i].v;
    ++q[i].x;
    q[i].v = 1ll * n * q[i].v + 1ll * q[i].x;
    lsh.push_back(q[i].v);
  }
  sort(lsh.begin(), lsh.end());
  auto it = unique(lsh.begin(), lsh.end());
  lsh.erase(it, lsh.end());
  cnt = lsh.size();
  for(int i = 1; i <= n; i++) a[i] = lower_bound(lsh.begin(), lsh.end(), a[i]) - lsh.begin() + 1;
  for(int i = 1; i <= Q; i++) q[i].v = lower_bound(lsh.begin(), lsh.end(), q[i].v) - lsh.begin() + 1;
  for(int i = 1; i <= 4 * cnt; i++) sgt.tree[i].mx = -Q;
  for(int i = 1; i <= n; i++) sgt.Ins(1, 1, cnt, a[i], 1, i - c[i]);
  for(int i = 1; i <= Q; i++){
    sgt.update(1, 1, cnt, a[q[i].x], cnt, 1);
    sgt.update(1, 1, cnt, q[i].v, cnt, -1);
    sgt.Del(1, 1, cnt, a[q[i].x]);
    a[q[i].x] = q[i].v;
    int val = sgt.query(1, 1, cnt, 1, a[q[i].x]) + 1;
    sgt.Ins(1, 1, cnt, a[q[i].x], 1, q[i].x - val);
    cout << sgt.tree[1].mx << '\n';
  }
  return 0;
}

P14311 【MX-S8-T4】平衡三元组

性质 \(1\):设 \(y\) 为 \(a_i\),则 \(x\) 是 \([l,i)\) 的最大值,\(z\) 是 \((i,r]\) 的最大值。

性质 \(2\):由性质 \(1\) 可得:\(x\) 和 \(z\) 中必有一个为 \([l,r]\) 的最大值,否则不合法。

性质 \(2\) 我没有想出来。

我们钦定 \(x\) 为 \([l,r]\) 的最大值,下标记为 \(a\)。我们找到 \((a,r]\) 的最大值下标 \(a_0\)。

有以下几种情况:

当 \(y \in(a,a_0)\) 时,三元组 \((a,y,a_0)\) 都合法,取 \(y\) 的最大值加入答案。
当 \(y = a_0\) 时,令 \(a_1\) 为 \((a_0,r]\) 的最大值的下标,若三元组 \((a, a_0, a_1)\) 合法,则加入答案。
当 \(y \in(a_0,r)\) 时,若三元组 \((a, y, a_1)\) 合法,则此情况必定不比三元组 \((a, a_0, a_1)\) 更优。否则我们需要继续分讨 \(y \in(a_0,r)\) 的情况。注意到此情况与第一种情况有相似的结构,于是我们可以递归处理。

复杂度为什么是正确的呢?

若继续递归,满足:

  • \(2\times A_{a_0}>A_a+A_{a_1}\),移项得:\(A_{a_1}<2\times A_{a_0}-A_a\);
  • \(2\times A_{a_1}>A_a+A_{a_2}\),移项得:\(A_{a_2}<2\times A_{a_1}-A_a\);
    与第一式联立,得:\(A_{a_2}<4\times A_{a_0}-3\times A_a\);

归纳一下 \(A_{a_i}<2^i \times A_{a_0} - (2^i-1) \times A_a\)。

即 \(A_{a_i}<A_a - 2^i \times( A_a - A_{a_0})\)

若 \(A_a \ne A_{a_0}\) 则递归 \(\log V\) 次;
若 \(A_a=A_{a_0}\) 则从 \(A_{a_1}\) 开始归纳;
若 \(A_a=A_{a_0}=A_{a_1}\) 则 \((A_a,A_{a_0},A_{a_1})\) 就为最终答案,和我们刚才讨论的第二种情况一致。

因为有区间修改、区间求 \(\max\),用线段树维护,时间复杂度 \(\Theta(q\log n \log v)\)

归纳法还是很重要的,需要掌握。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 1e6 + 7;
const ll INF = 1e17;
#define pli pair<ll, int>
#define mp(a, b) make_pair(a, b)
int n, Q;
ll a[kN], mx1, mx2;
struct SGT{
  struct node{
    pli mx;
    ll tag;
  }tree[kN << 2];
  void pushup(int x){
    tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
    return ;
  }
  void maketag(int x, ll v){
    tree[x].mx.first += v;
    tree[x].tag += v;
    return ;
  }
  void pushdown(int x){
    if(tree[x].tag){
      maketag(x << 1, tree[x].tag);
      maketag(x << 1 | 1, tree[x].tag);
      tree[x].tag = 0;
    }
    return ;
  }
  void build(int x, int l, int r){
    tree[x].mx = mp(-INF, 0);
    tree[x].tag = 0;
    if(l == r){
      tree[x].mx = mp(a[l], l);
      return ;
    }
    int mid = l + r >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    pushup(x);
    return ;
  }
  void update(int x, int l, int r, int L, int R, ll v){
    if(L > R) return ;
    if(L <= l && r <= R){
      maketag(x, v);
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(L <= mid) update(x << 1, l, mid, L, R, v);
    if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
    pushup(x);
    return ;
  }
  pli query(int x, int l, int r, int L, int R){
    if(L > R) return mp(-INF, 0);
    if(L <= l && r <= R) return tree[x].mx;
    pushdown(x);
    int mid = l + r >> 1;
    pli ans = mp(-INF, 0);
    if(L <= mid) ans = max(ans, query(x << 1, l, mid, L, R));
    if(mid < R) ans = max(ans, query(x << 1 | 1, mid + 1, r, L, R));
    pushup(x);
    return ans;
  }
}sgt;
void dfs1(int x, ll v, int l, int r){
  if(l >= r) return ;
  pli p = sgt.query(1, 1, n, l + 1, r);
  mx1 = max(mx1, v + p.first + sgt.query(1, 1, n, l + 1, p.second - 1).first);
  int y = p.second;
  pli q = sgt.query(1, 1, n, y + 1, r);
  if(p.first * 2ll <= v + q.first) mx1 = max(mx1, v + p.first + q.first);
  else dfs1(x, v, p.second, r);
  return ;
} 
void dfs2(int x, ll v, int l, int r){
  if(l >= r) return ;
  pli p = sgt.query(1, 1, n, l, r - 1);
  mx2 = max(mx2, sgt.query(1, 1, n, p.second + 1, r - 1).first + p.first + v);
  int y = p.second;
  pli q = sgt.query(1, 1, n, l, y - 1);
  if(p.first * 2ll <= v + q.first) mx2 = max(mx2, q.first + p.first + v);
  else dfs2(x, v, l, p.second);
  return ;
}
static inline void Out(int l, int r){
  mx1 = -INF, mx2 = -INF;
  pli p = sgt.query(1, 1, n, l, r);
  dfs1(p.second, p.first, p.second, r);
  dfs2(p.second, p.first, l, p.second);
  if(max(mx1, mx2) <= -10000000000) cout << "No\n";
  else cout << max(mx1, mx2) << '\n';
  return ;
}
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> Q;
  for(int i = 1; i <= n; i++) cin >> a[i];
  sgt.build(1, 1, n);
  for(int i = 1; i <= Q; i++){
    int o, l, r; ll v;
    cin >> o;
    if(o == 1){
      cin >> l >> r;
      Out(l, r);
    }
    else{
      cin >> l >> r >> v;
      sgt.update(1, 1, n, l, r, v);
    }
  }
  return 0;
}

P15455 [JOI 2026 SemiFinal] 新的桥梁 / New Bridge

发现题目给定的操作本质上就是 \(\text{prim}\) 求 \(\text{MST}\) 的过程。

由于题目保证了边权各不相同,所以原图的 \(\text{MST}\) 唯一。

\(D_{s,i}\) 是从 \(s\) 开始 \(\text{prim}\) 到 \(i\) 被加入 \(s\) 所在的点集之前所执行的操作次数。

这 等价于 \(\text{MST}\) 中点 \(j\to s\) 的路径上的最大边权小于 \(i\to s\) 这条路径上的路径上的最大边权 \(<w\) 的数量减一。(因为不考虑 \(j=s\))

我们看到最大边权 \(<w\) 可以想到 \(\text{Kruskal}\) 重构树,但事实上,我们并不需要真的把重构树建出来。我们把 \(\text{MST}\) 先拉出来,模拟 \(\text{Kruskal}\) 合并的过程,按边权从小到大枚举 \(i\to s\) 的最大边权,算这条边对跨两侧点对的贡献。

\(\text{MST}\) 默认以 \(1\) 为根。

记 \(Sub_u\) 为 \(\text{MST}\) 中 \(u\) 的子树,\(siz_u\) 为 \(u\) 这棵子树的大小,\(A\) 为合并前 \(u\) 所在的连通块,\(B\) 为合并前 \(v\) 所在的连通块。

考虑这条边为 \((u,v,w)\),\(s\) 和 \(i\) 分别位于 \(Sub_u\) 和 \(Sub_v\) 中,

以下假设 \(\text{MST}\) 中,\(fa_u=v\)。

当 \(s\in Sub(u)\) 时,\(i\in Sub(v)\)。\(s\) 可以为 \(Sub(u)\) 中的任意一个点,由于 \(\text{Kruskal}\) 的过程保证了边权从小到大合并到连通块中,所以 \(j\in A\),给 \(B\) 中所有点答案加上 \(|A|\times siz_u\)。

当 \(s\in Sub(v)\) 时,同理可得,给 \(A\) 中所有点答案加上 \(|B|\times (n-siz_u)\)。

如果 \(fa_v=u\) 讨论类似。

发现只需维护连通块的加法 \(tag\)。由于合并后原来连通块的 \(tag\) 与新加入连通块的点的 \(tag\) 不一致,考虑启发式合并暴力删除点以及维护每个点的 \(tag\) 修正量即可。

时间复杂度 \(\Theta(n\log n)\)。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 3e5 + 7;
int n, m, Q, f[kN], siz[kN];
struct edge{
  int u, v, w;
}e[kN << 1];
struct DSU{
  int fa[kN];
  void init(){
    for(int i = 1; i <= n; i++) fa[i] = i;
    return ;
  }
  int find(int x){
    if(fa[x] != x) fa[x] = find(fa[x]);
    return fa[x];
  }
}dsu;
vector<int> ve[kN], g[kN];
void dfs(int x, int fa){
  siz[x] = 1, f[x] = fa;
  for(int j : ve[x]){
    if(j == fa) continue;
    dfs(j, x);
    siz[x] += siz[j];
  }
  return ;
} 
ll sub[kN], add[kN];
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m >> Q;
  for(int i = 1; i <= m; i++){
    cin >> e[i].u >> e[i].v >> e[i].w;
  }
  dsu.init();
  sort(e + 1, e + m + 1, [&](edge x, edge y){return x.w < y.w;});
  for(int i = 1; i <= m; i++){
    int u = dsu.find(e[i].u), v = dsu.find(e[i].v);
    if(u != v) dsu.fa[u] = v, ve[e[i].u].push_back(e[i].v), ve[e[i].v].push_back(e[i].u);
  }
  dfs(1, 0);
  dsu.init();
  for(int i = 1; i <= n; i++) g[i].push_back(i);
  for(int i = 1; i <= m; i++){
    int u = dsu.find(e[i].u), v = dsu.find(e[i].v);
    if(u != v){
      if(f[e[i].v] == e[i].u){
        add[u] += 1ll * g[v].size() * siz[e[i].v];
        add[v] += 1ll * g[u].size() * (n - siz[e[i].v]);
      } 
      else{
        add[u] += 1ll * g[v].size() * (n - siz[e[i].u]);
        add[v] += 1ll * g[u].size() * siz[e[i].u];
      }
      if(g[u].size() < g[v].size()) swap(u, v);
      while(g[v].size()){
        sub[g[v].back()] += add[v] - add[u];
        g[u].push_back(g[v].back());
        g[v].pop_back();
      }
      dsu.fa[v] = u;
    }
  }
  for(int i = 1; i <= Q; i++){
    int x; cin >> x;
    cout << sub[x] + add[dsu.find(x)] << '\n';
  } 
  return 0;
}

P13693 [CEOI 2025] Equal Mex

本题中 \(\text{Mex}\) 表示最小未出现的正整数。

考虑 \(\Theta(nq)\) 的暴力怎么写?

假设询问区间的 \(\text{Mex}\) 为 \(M\),那么这个区间内不出现 \(M\),且 \(1,\cdots, M-1\) 全部出现。由于每一个划分段的 \(\text{Mex}\) 要相等,大胆猜测每段的 \(\text{Mex}\) 都为 \(M\),这很好证明。

然后我们可以发现最大的划分数量 \(ans\) 其实就是答案,因为 \(k\in [1,ans]\) 都是可以取到的。

如何让划分数量最大,每段尽可能小即可,最后剩余的补到末尾一段即可。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kM = 4e5 + 3;
bool ma[kM];
vector<int> ans;
vector<int> solve(int n, vector<int> &a, int Q, vector<pii> &q){
  for(int i = 0; i < Q; i++) q[i].first--, q[i].second--;
  for(int k = 0; k < Q; k++){
    int l = q[k].first, r = q[k].second;
    for(int i = 0; i < n; i++) ma[a[i]] = 0;
    for(int i = l; i <= r; i++) ma[a[i]] = 1;
    int m = 1;
    while(ma[m]) ++m;
    int cnt = 0, t = 0;
    for(int i = l; i <= r; i++) ma[a[i]] = 0;
    for(int i = l; i <= r; i++){
      if(!ma[a[i]] && a[i] < m){
        ++t;
        ma[a[i]] = 1;
      }
      if(t == m - 1){
        ++cnt;
        t = 0;
        for(int j = 1; j < m; j++) ma[j] = 0;
      }
    }
    ans.push_back(cnt);
  }
  return ans;
}

思考特殊性值 \(v_i\le 2\) 怎么做?

当 \(i\in [l,r]\),\(a_i\) 全部相同时,\(ans=r-l+1\)。
否则,我们思考上面的暴力能不能优化。

本质上,我们要求每一段都包含 \(1\) 和 \(2\),并且使这个段的长度最小,我们可以把这个过程看作一个不断向后跳的过程。

可以用倍增优化,记 \(jump_{i,0}\) 表示最小的 \(j\) 满足 \(i<j\) 且 \([i,j]\) 内既出现了 \(1\) 也出现了 \(2\),这里用二分求解。
时间复杂度 \(\Theta((n+q)\log n)\)。

每次询问直接倍增跳即可。

int s[kN][3], jump[kN][21];
vector<int> ans;
vector<int> solve(int n, vector<int> &a, int Q, vector<pii> &q){
  s[0][1] = (a[0] == 1);
  s[0][2] = (a[0] == 2);
  for(int i = 1; i < n; i++){
    s[i][1] = s[i - 1][1], s[i][2] = s[i - 1][2];
    s[i][a[i]]++;
  }
  for(int i = 0; i <= n; i++)
    for(int j = 0; j <= 20; j++)
      jump[i][j] = INF;
  for(int i = 0; i < n; i++){
    int l = i, r = n - 1, mid, p = n;
    while(l <= r){
      mid = l + r >> 1;
      if(s[mid][1] - (i ? s[i - 1][1] : 0) > 0 && s[mid][2] - (i ? s[i - 1][2] : 0) > 0){
        r = mid - 1, p = mid;
      }
      else l = mid + 1;
    }
    if(p != n) jump[i][0] = p + 1;
  }
  for(int j = 1; j <= 20; j++){
    for(int i = 0; i <= n; i++){
      if(jump[i][j - 1] != INF) jump[i][j] = jump[jump[i][j - 1]][j - 1];
    }
  }
  for(int i = 0; i < Q; i++){
    int l = q[i].first, r = q[i].second;
    l--; r--;
    if(s[r][1] - (l ? s[l - 1][1] : 0) == 0 || s[r][2] - (l ? s[l - 1][2] : 0) == 0) ans.push_back(r - l + 1);
    else{
      int res = 0;
      for(int j = 20; j >= 0; j--){
        if(l < n && jump[l][j] <= r + 1) res += (1 << j), l = jump[l][j];
      }
      ans.push_back(res);
    }
  }
  return ans;
}

考虑正解,我们知道极小 \(\text{Mex}\) 区间不超过 \(2\times n\) 个。证明:点击这里

科普:如何求区间 \(\text{Mex}\)?

用主席树维护前 \([1,i]\) 个数中,值域 \(j\in [1,V]\) 内的数字 \(j\) 出现的最大的位置的最小值。

查询区间 \([l,r]\) 时,在主席树上二分,若 \(tree[tree[x].l].mn<l\),则答案在左半区间;否则,答案在右半区间。

如何求出极小 \(\text{Mex}\) 区间?

一般我们有两种做法。

法一:假设当前我们已经找到了 \(\text{Mex}=k\) 的极小区间 \([l,r]\),现在我们要将其拓展,每次找到左边第一个等于 \(k\) 的 \(a_i\) 和右边第一个等于 \(k\) 的 \(a_j\),这可以用 \(\text{set}\) 维护。然后我们把这个区间拓展到 \([i,r]\) 和 \([l,j]\)。

有两个要注意的地方:

  • 区间 \([i,r]\) 和 \([l,j]\) 的 \(\text{Mex}\) 不一定等于 \(k+1\),所以我们要用上文讲到的求区间 \(\text{Mex}\) 的方法。

  • 刚拓展到 \(\text{Mex}=k\) 的区间,我们要把不符合极小 \(\text{Mex}\) 区间定义的去掉。极小 \(\text{Mex}\) 区间可以有交,但是不能包含。

法二:考虑从极小 \(\text{Mex}\) 区间数量的证明过程出发。对于极小 \(\text{Mex}\) 区间 \([l,r]\),我们钦定 \(a_l\) 为 \([l,r]\) 中的最大值。按 \(l\) 从大到小扫描线,维护 \(p_i\) 表示数字 \(i\) 出现的最靠左的位置。\(l\) 对应的 \(\text{Mex}\) 区间的右端点为 \(\max(p_1,\cdots,p_{a_l-1})\),\(\text{Mex}=a_l+1\)。对于 \(a_r\) 为 \([l,r]\) 中的最大值,同理。

显然法一的码量小于法二,故我们采用法一。

对于每一组询问 \([l,r]\),我们先找到区间 \([l,r]\) 的 \(\text{Mex}\),记为 \(m\)。我们把所有 \(\text{Mex}=m\) 的极小 \(\text{Mex}\) 区间拉出。沿用特殊性质的做法,我们倍增跳即可。

时间复杂度 \(\Theta(n\log V+q \log n)\)

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("33.in", "r", stdin);
  freopen("1.out", "w", stdout);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
#define fi first
#define se second
const int INF = 1e8;
const int kN = 6e5 + 3;
const int kV = 4e5 + 3;
struct SGT{
  int tot = 0;  
  struct node{
    int l, r, mn;
  }tree[kN * 20];
  void pushup(int x){
    tree[x].mn = min(tree[tree[x].l].mn, tree[tree[x].r].mn);
    return ;
  }
  void update(int &x, int lstx, int l, int r, int pos, int v){
    x = ++tot;
    tree[x] = tree[lstx];
    if(l == r){
      tree[x].mn = v;
      return ;
    } 
    int mid = l + r >> 1;
    if(pos <= mid) update(tree[x].l, tree[lstx].l, l, mid, pos, v);
    else update(tree[x].r, tree[lstx].r, mid + 1, r, pos, v);
    pushup(x);
    return ;
  }
  int query(int x, int l, int r, int v){
    if(l == r) return l;
    int mid = l + r >> 1;
    if(tree[tree[x].l].mn < v) return query(tree[x].l, l, mid, v);
    else return query(tree[x].r, mid + 1, r, v);
  }
}sgt;
struct Query{
  int l, r, id;
};
set<int> s[kV]; // 维护左边右边第一个值为 v 的下标
vector<pii> Mex[kV];
vector<Query> qry[kN];
vector<int> ans;
int rt[kN], a[kN];
int jump[kN][21], Nxt[kN], as[kN];
vector<int> solve(int n, vector<int> &v, int Q, vector<pii> &q){
  for(int i = 1; i <= n; i++) a[i] = v[i - 1];
  for(int i = 1; i <= n; i++){
    if(a[i] == 1) Mex[2].push_back({i, i});
    else Mex[1].push_back({i, i});
    s[a[i]].insert(i);
    sgt.update(rt[i], rt[i - 1], 1, 400001, a[i], i);
  }
  if(Mex[1].size()) sort(Mex[1].begin(), Mex[1].end(), [&](pii x, pii y){return (x.first == y.first) ? x.second < y.second : x.first > y.first;});
  for(int i = 2; i <= 400001; i++){
    sort(Mex[i].begin(), Mex[i].end(), [&](pii x, pii y){return (x.first == y.first) ? x.second < y.second : x.first > y.first;});
    vector<pii> tmp;
    int mn = n + 1;
    for(auto [l, r] : Mex[i]){
      if(r < mn) mn = r, tmp.push_back(mp(l, r)); 
    }
    swap(Mex[i], tmp);
    for(auto [l, r] : Mex[i]){
      auto it = s[i].lower_bound(l);
      int nl = 0, nr = 0;
      if(it != s[i].begin()) --it, nl = *it;
      it = s[i].upper_bound(r);
      if(it != s[i].end()) nr = *it;
      if(nl) Mex[sgt.query(rt[r], 1, 400001, nl)].push_back(mp(nl, r));
      if(nr) Mex[sgt.query(rt[nr], 1, 400001, l)].push_back(mp(l, nr));
    }
  }
  for(int i = 0; i < Q; i++){
    int l = q[i].fi, r = q[i].se;
    int v = sgt.query(rt[r], 1, 400001, l);
    qry[v].push_back({l, r, i});
  }
  for(int i = 1; i <= 400001; i++){
    int j = -1;
    for(int k = 0; k < Mex[i].size(); k++){
      int l = Mex[i][k].fi, r = Mex[i][k].se;
      jump[l][0] = r + 1;
      while(j + 1 < k && Mex[i][j + 1].fi > r) ++j;
      if(j != -1) Nxt[r + 1] = Mex[i][j].fi;
      for(int t = 1; t <= 20; t++) jump[l][t] = jump[Nxt[jump[l][t - 1]]][t - 1];
    }
    sort(qry[i].begin(), qry[i].end(), [&](Query x, Query y){return x.l > y.l;});
    j = -1;
    for(auto [l, r, id] : qry[i]){
      int res = 0;
      while(j + 1 < Mex[i].size() && Mex[i][j + 1].fi >= l) ++j;
      if(j == -1) continue;
      l = Mex[i][j].fi; 
      for(int t = 20; t >= 0; t--){
        if(jump[l][t] && jump[l][t] <= r + 1) l = Nxt[jump[l][t]], res += 1 << t;
      }
      as[id] = res;
    }
    for(auto [l, r] : Mex[i]){
      for(int t = 0; t <= 20; t++) jump[l][t] = 0;
      Nxt[r + 1] = 0;
    }
  }
  for(int i = 0; i < Q; i++) ans.push_back(as[i]);
  return ans;
}
int n, Q;
vector<int> A;
vector<pii> q;
signed main(){
  File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> Q;
  for(int i = 1, x; i <= n; i++) cin >> x, A.push_back(x);
  for(int i = 1, l, r; i <= Q; i++){
    cin >> l >> r;
    q.push_back(mp(l, r));
  }
  vector<int> tmp =  solve(n, A, Q, q);
  for(int i = 0; i < Q; i++) cout << tmp[i] << '\n';
  return 0;
}

P5311 [Ynoi2011] 成都七中

连通块问题应该要想到点分治。设 \(s_{x,[l,r]}\) 表示点 \(x\) 通过点 \(j\in [l, r]\) 能到达的点的集合,显然有 \(y\in S\),\(s_{x,[l,r]}=s_{y,[l,r]}\)。

考虑当前分治重心为 \(rt\),若 \(rt\in s_{x,[l,r]}\),则 \(s_{x,[l,r]}=s_{rt,[l,r]}\)。若 \(rt\notin s_{x,[l,r]}\),\(s_{x,[l,r]}\) 全部位于 \(rt\) 的一棵子树中,所以点分治的正确性是对的。

记 \(mn_u\) 和 \(mx_u\) 表示 \(u\) 到 \(rt\) 路径上编号最小和最大的点。对于询问 \((l,r,x)\),若 \(\exists u\) 满足 \(l\le mn_u \le mx_u \le r\),则 \(a_u\) 可以被算入答案。

说句题外话,推导到这,我们发现原询问的 \(x\) 为干扰变量。

如何高效计算答案?

将 \([mu_u,mx_u]\) 视作一个区间,若 \([mu_u,mx_u]\subseteq [l,r]\),\(a_u\) 会被算入答案,这是经典的离线扫描线的数颜色问题。

将贡献和询问区间按左端点从大到小排序,记录 \(lst_i\) 表示颜色 \(i\) 所在区间的最小的右端点。

用树状数组维护区间内的颜色数量,每次更新 \(lst_i\) 时,删除原来位置的贡献,加入新位置的贡献,这样保证了每个颜色最多被算入一次答案。

每次查询就查前缀 \([1,r]\) 的和即可。

时间复杂度 \(\Theta(n\log^2 n+q\log n)\)。

题外话,这题还可以用 \(\text{Kruskal}\) 重构树来解决。

具体而言,发现限制为最小的点的编号大于等于 \(l\),最大的点的编号小于等于 \(r\),于是我们可以建一棵最大生成树和一棵最小生成树的 \(\text{Kruskal}\) 重构树,边权视作两节点编号的 \(\max\) 或 \(\min\),于是问题转化为了求两棵树中的子树的叶子节点颜色并。记点 \(i\) 在第一棵子树上的 \(dfs\) 序为 \(dfs1_i\),在第一棵树上的限制为 \(l1_i \le dfs1_i \le r1_i\),在第二棵子树上的 \(dfs\) 序为 \(dfs2_i\),限制为 \(l2_i \le dfs2_i \le r2_i\)。将 \((dfs1_i,dfs2_i)\) 视作一个点,于是问题又转化为了 二维数颜色问题。

于是发现做法假了。

好像又没假。

image

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
const int kN = 1e5 + 7;
int a[kN], rt, n, m, siz[kN], ban[kN], ans[kN], del[kN], mn[kN], mx[kN], lst[kN], N;
vector<int> ve[kN];
struct Query{
  int l, r, id, o;
};
vector<Query> q[kN], opt;
struct BIT{
  int tree[kN];
  void update(int x, int v){
    for(int i = x; i <= N; i += i & -i) tree[i] += v;
    return ;
  }
  int query(int x){
    int res = 0;
    for(int i = x; i; i -= i & -i) res += tree[i];
    return res;
  }
}bit;
void dfs(int x, int fa){
  siz[x] = 1;
  for(int j : ve[x]){
    if(j == fa || del[j]) continue;
    dfs(j, x);
    siz[x] += siz[j];
  }
  return ;
}
void find_rt(int x, int fa){
  int mx = 0;
  for(int j : ve[x]){
    if(j == fa || del[j]) continue;
    find_rt(j, x);
    mx = max(mx, siz[j]);
  }
  mx = max(mx, n - siz[x]);
  if(mx <= n / 2){
    rt = x;
  }
  return ;
}
void dfs2(int x, int fa){
  opt.push_back({mn[x], mx[x], a[x], 1});
  for(auto j : q[x]){
    if(!ban[j.id] && j.l <= mn[x] && mx[x] <= j.r){
      ban[j.id] = 1;
      opt.push_back(j);
    }
  }
  for(int j : ve[x]){
    if(j == fa || del[j]) continue;
    mn[j] = min(j, mn[x]);
    mx[j] = max(j, mx[x]);
    dfs2(j, x);
  }
  return ; 
}
void solve(int x){
  del[x] = 1;
  mn[x] = mx[x] = x;
  dfs2(x, 0);
  sort(opt.begin(), opt.end(), [&](Query x, Query y){return x.l != y.l ? x.l > y.l : x.o > y.o;});
  for(auto j : opt){
    if(j.o == 1){
      if(j.r < lst[j.id]){
        bit.update(lst[j.id], -1);
        lst[j.id] = j.r;
        bit.update(lst[j.id], 1);
      }
    }
    else ans[j.id] = bit.query(j.r);
  }
  for(auto j : opt) if(j.o) bit.update(lst[j.id], -1), lst[j.id] = N + 1;
  opt.clear();
  for(int j : ve[x]){
    if(del[j]) continue;
    n = siz[j];
    rt = 0;
    find_rt(j, 0);
    dfs(rt, 0);
    solve(rt);
  }
  return ;
}
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m; N = n;
  for(int i = 1; i <= n; i++) cin >> a[i];
  for(int i = 1; i <= 100000; i++) lst[i] = N + 1;
  for(int i = 1, u, v; i < n; i++){
    cin >> u >> v; ve[u].push_back(v), ve[v].push_back(u);
  }
  for(int i = 1, l, r, x; i <= m; i++){
    cin >> l >> r >> x; 
    q[x].push_back({l, r, i, 0});
  }
  dfs(1, 0);
  find_rt(1, 0);
  solve(rt);
  for(int i = 1; i <= m; i++) cout << ans[i] << '\n';
  return 0;
}

P11118 [ROI 2024] 无人机比赛 (Day 2)

好题。

令 \(d_i=s_i-s_{i-1}\),发现若 \(d_i\le d_{i-1}\),则某一轮的结果和上一轮一模一样,所以只需要保留 \(d_i\) 的严格递增序列,长度 \(m\le \sqrt{2\times n}\),令 \(c_i\) 表示每一段在原序列中有多少个门。

对于第 \(i\) 个无人机到达第 \(k\) 个门时的贡献等于 \((\sum_{j=1}^n[t_j\times d_m\ge t_i \times d_k])-m\),因为只要还没有到达终点的无人机会回到存档点。

总答案 \(ans=-n\times m + \sum_{i=1}^n\sum_{j=1}^n\sum_{k=1}^m[t_j\times d_m\ge t_i \times d_k]\)。

问题来了,如何处理 \([t_j\times d_m\ge t_i \times d_k]\)?

首先,移项得:\(\frac{t_j\times d_m}{d_k}\ge t_i\)。

记 \(t_i\) 的排名为 \(rk_i\),\(rk\) 的逆变换为 \(id_i\)。

发现 \(\frac{t_j\times d_m}{d_k}\) 只与 \(j\) 和 \(k\) 有关,令 \(R_{j,k}\) 表示有多少个 \(t_i\le \frac{t_j\times d_m}{d_k}\),这可以预处理。

\([t_j\times d_m\ge t_i \times d_k]\) 被转化成 \([R_{j,k}\ge rk_i]\)。

重新写一遍答案的式子 \(ans=-n\times m + \sum_{i=1}^n\sum_{j=1}^n\sum_{k=1}^m[R_{j,k}\ge rk_i]\)。

现在我们要做的就是维护答案的增量。

分两种情况:

  • 第 \(x+1\) 个无人机使其他无人机返回存档点的次数 \(-m+\sum_{j=1}^{x+1}\sum_{k=1}^m [R_{j,k}\ge rk_{x+1}]\times c_k\)

    发现有 \(\Theta(n)\) 次查询 \(\ge rk_{x+1}\),\(\Theta(n\sqrt n)\) 次插入 \(R_{x+1,k}\),根号平衡一下。

  • 其他无人机使第 \(x+1\) 个无人机的返回存档点的次数 \(\sum_{i=1}^x\sum_{k=1}^m[R_{x+1,k}\ge rk_i]\times c_k\)。

    类似地,发现有 \(\Theta(n)\) 次插入 \(rk_{x+1}\),\(\Theta(n\sqrt n)\) 次查询 \(\le R_{x+1,k}\),根号平衡一下。

总复杂度 \(\Theta(n\sqrt n)\)。

感觉这题难点在于把答案的形式写出来,并对它化简。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("52.in", "r", stdin);
}
using ll = long long;
#define pll pair<ll, ll>
#define mp(a, b) make_pair(a, b)
const int kN = 2e5 + 7;
int n, m, rk[kN], id[kN], r[kN][550];
ll ans, t[kN], s[kN], d[kN], c[kN];
int cnt, pos[kN], L[kN], R[kN];
struct DS1{
  ll sum[403], val[kN];
  void update(int x, ll v){
    val[x] += v;
    sum[pos[x]] += v;
  }
  ll query(int x){
    ll ans = 0;
    for(int i = x; i <= R[pos[x]]; i++) ans += val[i];
    for(int i = pos[x] + 1; i <= cnt; i++) ans += sum[i];
    return ans;
  }
}ds1;
struct DS2{
  ll sum[403], val[kN];
  void update(int x, ll v){
    for(int i = x; i <= R[pos[x]]; i++) val[i] += v;
    for(int i = pos[x]; i <= cnt; i++) sum[i] += v;
    return ;
  }
  ll query(int x){
    return sum[pos[x] - 1] + val[x];  
  }
}ds2;
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  int B = sqrt(n);
  cnt = n / B;
  for(int i = 1; i <= cnt; i++){
    L[i] = R[i - 1] + 1, R[i] = i * B;
  }
  if(R[cnt] < n){
    L[++cnt] = R[cnt - 1] + 1;
    R[cnt] = n;
  }
  for(int i = 1; i <= cnt; i++){
    for(int j = L[i]; j <= R[i]; j++) pos[j] = i;
  }
  for(int i = 1; i <= n; i++) cin >> t[i];
  for(int i = 1; i <= m; i++) cin >> s[i];
  for(int i = m; i >= 1; i--) s[i] = s[i] - s[i - 1];
  int M = 0;
  for(int i = 1; i <= m; i++){
    d[++M] = s[i];
    c[M] = 1;
    while(i + 1 <= m && s[i + 1] <= d[M]) ++i, c[M]++;
  }
  swap(m, M);
  for(int i = 1; i <= n; i++) id[i] = i; 
  sort(id + 1, id + n + 1, [&](int x, int y){return (t[x] == t[y]) ? x < y : t[x] < t[y];});
  for(int i = 1; i <= n; i++) rk[id[i]] = i;
  for(int k = 1; k <= m; k++){
    for(int j = 1, i = 0; j <= n; j++){
      while(i + 1 <= n && mp(t[id[j]] * d[m], id[j]) >= mp(t[id[i + 1]] * d[k], id[i + 1])) ++i;
      r[id[j]][k] = i;
    }
  }
  for(int x = 0; x < n; x++){
    for(int k = 1; k <= m; k++) ds1.update(r[x + 1][k], c[k]);
    ans += ds1.query(rk[x + 1]);
    for(int k = 1; k <= m; k++) ans += ds2.query(r[x + 1][k]) * c[k];
    ds2.update(rk[x + 1], 1);
    cout << ans - 1ll * (x + 1) * M << '\n';
  }
  return 0;
}

P11947 [KTSC 2025] 可爱区间 / maxsum

想要区间 \([l,r]\) 是可爱的,我们肯定会让 \(i\in [l,r]\) 填 \(B_i\),\(i\notin [l,r]\) 填 \(A_i\)。

下标先都平移一位。

考虑对 \([l,r]\) 有哪些限制?

记 \(s1_i\) 为 \(a\) 的前缀和,\(s2_i\) 为 \(b\) 的前缀和。

  • 不存在与 \([l,r]\) 不相交的区间的 \(A_i\) 之和 \(\ge s2_r-s2_{l-1}\)。这个限制可以强化到 \(A\) 中的最大子段和 \(\le s2_r-s2_{l-1}\)。

  • 区间 \([l,r]\) 内不存在一段 真后缀 的和 \(\le 0\),即 \(s2_r\) 是 \(i\in [l,r]\) 中 \(s2_i\) 中的最大值。

  • 区间 \([l,r]\) 内不存在一段 真前缀 的和 \(\le 0\),即 \(s2_{l-1}\) 是 \(i\in [l-1,r-1]\) 中 \(s2_i\) 的最小值。

  • 区间 \([1,l-1]\) 内所有 后缀 的最大的 \(A\) 之和(可以为空)为 \(0\),区间 \([r+1,n]\) 内所有 前缀 的最大的 \(A\) 之和(可以为空)为 \(0\)。

考虑扫描线,对 \(r\) 从小到大扫描。

对于限制二,我们可以预处理出每个 \(r\) 所对应的最大的 \(l-1\)。(为什么是 \(l-1\),我们发现上面的四个限制基本上都与 \(l-1\) 有关)。

对于限制三,我们想到维护一个单调不降的单调栈取存合法的 \(l-1\)。

对于限制一,可以发现 \(l - 1\) 是一段前缀,在单调栈上二分即可。

对于限制四,预处理出 \(pre_i\) 和 \(suf_i\) 分别表示 \([1,i]\) 和 \([i,n]\) 的最大后缀/前缀和即可,只有在 \(pre_{i}=0\) 的时候入栈,在 \(suf_{i+1}=0\) 的时候算答案。

对于答案差分一下,变成右端点在 \([1,r2]\) 内,左端点在 \([l1,r1]\) 内的答案,这可以用单点修改,区间历史和的线段树来维护。

总复杂度 \(\Theta(n\log n)\)。

本题难点在于想到单调栈。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 250007;
int qcnt, n, Q;
ll a[kN], b[kN], pre[kN], suf[kN], mxA, s1[kN], s2[kN];
int stk[kN], top, lim[kN];
struct SGT{
  struct node{
    ll s, s2, tag;
  }tree[kN << 2];
  void pushup(int x){
    tree[x].s = tree[x << 1].s + tree[x << 1 | 1].s;
    tree[x].s2 = tree[x << 1].s2 + tree[x << 1 | 1].s2;
    return ;
  }
  void maketag(int x, ll v){
    tree[x].s2 += tree[x].s * v;
    tree[x].tag += v;
    return ;
  }
  void pushdown(int x){
    maketag(x << 1, tree[x].tag);
    maketag(x << 1 | 1, tree[x].tag);
    tree[x].tag = 0;
    return ;
  }
  void update(int x, int l, int r, int pos, ll v){
    if(l == r){
      tree[x].s += v;
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(pos <= mid) update(x << 1, l, mid, pos, v);
    else update(x << 1 | 1, mid + 1, r, pos, v);
    pushup(x);
    return ;
  }
  void update2(int x, int l, int r, int L, int R){
    if(L <= l && r <= R){
      maketag(x, 1ll);
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(L <= mid) update2(x << 1, l, mid, L, R);
    if(mid < R) update2(x << 1 | 1, mid + 1, r, L, R);
    pushup(x);
    return ;
  }
  ll query(int x, int l, int r, int L, int R){
    if(L <= l && r <= R) return tree[x].s2;
    pushdown(x);
    int mid = l + r >> 1; ll res = 0;
    if(L <= mid) res += query(x << 1, l, mid, L, R);
    if(mid < R) res += query(x << 1 | 1, mid + 1, r, L, R);
    pushup(x);
    return res;
  }
}sgt;
ll ans[kN];
struct Query{
  int l, r, id;
  ll op;
};
vector<Query> opt[kN];
vector<ll> maxsum(vector<int> A, vector<int> B,  vector<int> L1, vector<int> R1, vector<int> L2, vector<int> R2){
  n = A.size(), Q = L1.size();
  for(int i = 1; i <= n; i++) a[i] = A[i - 1], b[i] = B[i - 1];
  for(int i = 1; i <= n; i++) s1[i] = s1[i - 1] + a[i], s2[i] = s2[i - 1] + b[i];
  for(int i = 1; i <= Q; i++){
    L1[i - 1]++, L2[i - 1]++, R1[i - 1]++, R2[i - 1]++;
    opt[R2[i - 1]].push_back({L1[i - 1] - 1, R1[i - 1] - 1, i, 1ll});
    opt[L2[i - 1] - 1].push_back({L1[i - 1] - 1, R1[i - 1] - 1, i, -1ll});
  }
  for(int i = 1; i <= n; i++) pre[i] = max(pre[i - 1] + a[i], 0ll);
  for(int i = n; i >= 1; i--) suf[i] = max(suf[i + 1] + a[i], 0ll);
  for(int i = 1; i <= n; i++){
    while(top && s2[stk[top]] <= s2[i]) top--;
    lim[i] = stk[top]; stk[++top] = i;
  } top = 0;
  mxA = -1e18;
  for(int i = 1; i <= n; i++) mxA = max(mxA, pre[i - 1] + a[i]);
  stk[++top] = 0;
  sgt.update(1, 0, n, 0, 1ll);
  for(int i = 1; i <= n; i++){
    if(suf[i + 1] == 0){
      int l = 1, r = top, mid, Ed = 0, Fi = top + 1;
      while(l <= r){
        mid = l + r >> 1;
        if(s2[i] - s2[stk[mid]] >= mxA){
          l = mid + 1;
          Ed = mid;
        }
        else r = mid - 1;
      }
      if(lim[i] <= stk[Ed] && Ed) sgt.update2(1, 0, n, lim[i], stk[Ed]);
    }
   for(auto j : opt[i]){
      ans[j.id] += j.op * sgt.query(1, 0, n, j.l, j.r);
    }    
    while(top && s2[stk[top]] > s2[i]){
      sgt.update(1, 0, n, stk[top], -1ll);
      top--;
    }
    if(pre[i] == 0) sgt.update(1, 0, n, i, 1ll), stk[++top] = i;
  }
  vector<ll> tmp;
  for(int i = 1; i <= Q; i++) tmp.push_back(ans[i]);
  return tmp;
}
signed main(){
  File();
  vector<ll> A, B;
  vector<int> L1, R1, L2, R2;
  cin >> n >> Q;
  for(int i =  1; i <= n; i++){
    ll x, y; cin >> x >> y;
    A.push_back(x), B.push_back(y);
  }
  for(int i = 1; i <= Q; i++){
    int x, y, xx, yy;
    cin >> x >> y >> xx >> yy;
    L1.push_back(x), R1.push_back(y);
    L2.push_back(xx), R2.push_back(yy);
  }
  vector<ll> tmp = maxsum(A, B, L1, R1, L2, R2);
  for(auto i : tmp) cout << i << ' ';
  cout << '\n';
  return 0;
}

P11945 [KTSC 2025] 军事基地 / safezone

看到这道题,有个显然的想法是扫描线。

我们扫描 \(x\) 轴,维护 \(y\) 轴上的点所属的集合,对于一个矩形,在左端点时加入 \([l,r]\),和 \([l,r]\) 所代表的集合内的点连边,然后在矩形右端点的时候删除,这样做,复杂度显然爆炸。

考虑给纵轴上的每个点设一个代表元,表示当前这个点属于哪个矩形,那么每次向这个点所代表的矩形合并,合并可以用并查集维护。

再仔细思考,发现上面的操作本质上是给 \([l,r]\) 的点打上颜色,可以想到用 \(\text{ODT}\) 来维护。

那接下来我们有两个问题:

  • 如何保证 \(\text{ODT}\) 的复杂度?

  • 如何保证高效的删除?

先考虑第二个问题。

由于 \(\text{ODT}\) 无法支持删除,我们可以想到对于区间 \([l,r]\) 内的每个颜色段,如果当前颜色段内还有点,那么这个代表元肯定是存在的。

这个只需要用线段树维护区间加和区间查即可。

再考虑第一个问题。

每次访问完 \([l,r]\) 内的所有颜色段后,将 \([l,r]\) 改为一个整体的颜色段,这样是对的。

简单证明:每次询问一个区间的数量约等于删除区间的数量,每次操作至多加入 \(\Theta(1)\) 个区间,可看作询问颜色段的数量约等于操作的数量,所以复杂度为 \(\Theta(n\log ^2 n)\) ,不要忘记乘上线段树的复杂度。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("13.in", "r", stdin);
  return ;
}
const int kN = 5e5 + 7;
vector<int> A, B, C, D, ans;
int n, cx, cy;
using ll = long long;
struct SGT{
  struct node{
    ll s, tag;
  }tree[kN << 3];
  void pushup(int x){
    tree[x].s = tree[x << 1].s + tree[x << 1 | 1].s;
    return ;
  }
  void maketag(int x, int l, int r, ll v){
    tree[x].s += 1ll * (r - l + 1) * v;
    tree[x].tag += 1ll * v;
    return ;
  }
  void pushdown(int x, int l, int r){
    int mid = l + r >> 1;
    maketag(x << 1, l, mid, tree[x].tag);
    maketag(x << 1 | 1, mid + 1, r, tree[x].tag);
    tree[x].tag = 0;
    return ;
  }
  void build(int x, int l, int r){
    tree[x].s = tree[x].tag = 0;
    if(l == r) return ;
    int mid = l + r >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    pushup(x);
    return ;
  }
  void update(int x, int l, int r, int L, int R, ll v){
    if(L <= l && r <= R){
      maketag(x, l, r, v);
      return ;
    } pushdown(x, l, r);
    int mid = l + r >> 1;
    if(L <= mid) update(x << 1, l, mid, L, R, v);
    if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
    pushup(x);
    return ;
  }
  ll query(int x, int l, int r, int L, int R){
    if(L <= l && r <= R) return tree[x].s;
    pushdown(x, l, r);
    int mid = l + r >> 1;
    ll ans = 0;
    if(L <= mid) ans += query(x << 1, l, mid, L, R);
    if(mid < R) ans += query(x << 1 | 1, mid + 1, r, L, R);
    pushup(x);
    return ans; 
  }
}sgt;
struct DSU{
  int fa[kN], siz[kN];
  void init(){
    for(int i = 1; i <= n; i++) siz[i] = 1, fa[i] = i;
    return ;
  }
  int find(int x){
    if(fa[x] != x) fa[x] = find(fa[x]);
    return fa[x];
  }
  void merge(int x, int y){
    x = find(x), y = find(y);
    if(x != y){
      if(siz[x] > siz[y]) swap(x, y);
      fa[x] = y, siz[y] += siz[x];
    }
    return ;
  }
}dsu;
struct node{
  int l, r, id;
  bool friend operator < (node a, node b){
    return a.l < b.l;
  }
};
struct ODT{
  set<node> s;
  void insert(node x){
    s.insert(x);
    return ;
  }
  void split(int l, int r){
    auto it = --s.upper_bound({l, l, -1});
    if((*it).l != l){
      node tmp = *it;
      s.erase(it);
      s.insert({tmp.l, l - 1, tmp.id});
      s.insert({l, tmp.r, tmp.id});  
    }
    it = --s.upper_bound({r, r, -1});
    if((*it).r != r){
      node tmp = *it;
      s.erase(it);
      s.insert({tmp.l, r, tmp.id});
      s.insert({r + 1, tmp.r, tmp.id});
    }
    return ;
  }
  void assign(node x){
    int l = x.l, r = x.r, v = x.id + 1;
    split(l, r);
    auto itr = s.upper_bound({r, r, -1});
    auto itl = s.lower_bound({l, l, -1});
    for(auto it = itl; it != itr; ++it){
      if(sgt.query(1, 1, cy, (*it).l, (*it).r) > 0){
        dsu.merge(v, (*it).id);
      }
    }
    s.erase(itl, itr);
    s.insert({l, r, v});
    return ;
  }
}odt;
int lshx[kN << 1], lshy[kN << 1], vis[kN];
vector<node> Add[kN << 1], Del[kN << 1];
vector<int> find_union(int _n, vector<int> A, vector<int> B, vector<int> C, vector<int> D){
  cx = 0, cy = 0; n = _n;
  for(int i = 0; i < n; i++) lshx[++cx] = A[i], lshx[++cx] = C[i];
  for(int i = 0; i < n; i++) lshy[++cy] = B[i], lshy[++cy] = D[i];
  sort(lshx + 1, lshx + cx + 1);
  sort(lshy + 1, lshy + cy + 1);
  cx = unique(lshx + 1, lshx + cx + 1) - (lshx + 1);
  cy = unique(lshy + 1, lshy + cy + 1) - (lshy + 1);
  for(int i = 0; i < n; i++){
    A[i] = lower_bound(lshx + 1, lshx + cx + 1, A[i]) - lshx;
    C[i] = lower_bound(lshx + 1, lshx + cx + 1, C[i]) - lshx;
    B[i] = lower_bound(lshy + 1, lshy + cy + 1, B[i]) - lshy;
    D[i] = lower_bound(lshy + 1, lshy + cy + 1, D[i]) - lshy;
    Add[A[i]].push_back({B[i], D[i], i});
    Del[C[i] + 1].push_back({B[i], D[i], i});
  }
  dsu.init();
  odt.insert({1, cy, 0});
  sgt.build(1, 1, cy);
  for(int i = 1; i <= cx; i++){
    for(auto j : Del[i]) sgt.update(1, 1, cy, j.l, j.r, -1);
    for(auto j : Add[i]){
      odt.assign(j);
      sgt.update(1, 1, cy, j.l, j.r, 1);
    }
  }
  int res = 0;
  for(int i = 1; i <= n; i++){
    int x = dsu.find(i);
    if(!vis[x]){
      vis[x] = ++res;
    }
    ans.push_back(vis[x] - 1);
  }
  return ans;
}

P6881 [JOI 2020 Final] 火灾 / Fire

这道题怎么说呢,感觉有很多没怎么见过的 \(trick\)。

首先有个基本的想法,以序列下标为 \(x\) 轴,以时间为 \(y\) 轴,我们就能把每个时刻的序列状态给表示出来。

对于每个相同的值,其图形都可以看作一个平行四边形,这是最标准的图形。

接下来我们需要确定这个平行四边形的四个顶点。

记 \(l_i\) 为 \(i\) 左边第一个 \(>a_i\) 的数,\(r_i\) 为 \(i\) 右边第一个 \(>a_i\) 的数,那么四个顶点分别为 \((i,0),(i,i-l_i-1),(r_i-1,r_i-i-1),(r_i-1,r_i-l_i-2)\)。

这可以用单调栈求出。

当然这里有 \(\text{corner case}\),就是左边或右边没有 \(>a_i\) 的元素,这种情况就是普通的等腰直角三角形,且有一条直角边为 \(x\) 轴。为了方便统一处理,我们可以将其补全成一个平行四边形,只是多余的部分我们不算贡献即可。

我们考虑平行四边形怎么算贡献。有一个经典的做法是拆成三个三角形,如图:

看成一个大三角形减去两个小三角形。

而对于每一个三角形,我们知道它是一个等要直角三角形,且有一条直角边在 \(x\) 轴上和它的上顶点。

对于询问,相当于是一条平行于 \(x\) 轴的线段,我们差分一下,变成平行于 \(x\) 轴的射线。

现在我们需要知道对于一条射线,它与三角形的交线长度。

假设射线的端点为 \((x_0,y_0)\),三角形的上顶点为 \((x,y)\),我们可以分以下两种情况讨论。

  • \((x_0,y_0)\) 在三角形内,如图:

    我们通过几何关系能够得到交线长度为 \(y-(x-x_0)-y_0+1\),整理一下为 \((x_0-y_0)-(x-y)+1\)。

    当 \(x_0\le x,y_0\le y,x_0-y_0\ge x-y\) 时满足 \((x_0,y_0)\) 在三角形内,发现 \(y_0\le y\) 这个条件时多余的,于是只剩下 \(x_0\le x,x_0-y_0\ge x-y\)。

  • \((x_0,y_0)\) 在三角形外,如图:

我们通过几何关系能够得到交线长度为 \(y-y_0+1\)。

于是我们可以对 \(x\) 轴从小到大扫描线,用两个数据结构来维护两种不同的贡献,动态维护两种贡献。发现一种需要维护单点加、前缀查,另一种需要维护单点加、后缀查,这都可以用树状数组维护,复杂度 \(\Theta(n\log n)\)。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 2e5 + 7;
int n, Q, stk[kN], l[kN], r[kN], s[kN], top, cntp, cntq;
ll ans[kN];
struct BIT1{
  ll tree1[kN * 3], tree2[kN * 3];
  void update(int x, int v){
    x--;
    ll v2 = 1ll * v * x;
    for(int i = x + n + 1; i <= 3 * n; i += i & -i) tree1[i] += v2, tree2[i] += 1ll * v;
    return ;
  }
  ll query(int x){
    ll res = 0;
    for(int i = x + n + 1; i; i -= i & -i) res += tree2[i] * 1ll * x, res -= tree1[i];
    return res;
  }
}ds1;
struct BIT2{
  ll tree1[kN * 3], tree2[kN * 3];
  void update(int x, int v){
    ++x;
    ll v2 = 1ll * v * x;
    for(int i = x + n + 1; i; i -= i & -i) tree1[i] += v2, tree2[i] += 1ll * v;
    return ;
  }
  ll query(int x){
    ll res = 0;
    for(int i = x + n + 1; i <= 3 * n; i += i & -i) res -= tree2[i] * 1ll * x, res += tree1[i];
    return res;
  }
}ds2;
struct Add{
  int x, y, a;
}p[kN * 3];
struct Query{
  int x, y, id;
}q[kN << 1];
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> Q;
  for(int i = 1; i <= n; i++) cin >> s[i];
  for(int i = 1; i <= n; i++){
    while(top && s[stk[top]] < s[i]) top--;
    if(top) l[i] = stk[top];
    else l[i] = i - n - 1;
    stk[++top] = i;
  } top = 0;
  for(int i = n; i >= 1; i--){
    while(top && s[stk[top]] <= s[i]) top--;
    if(top) r[i] = stk[top];
    else r[i] = n + 1;
    stk[++top] = i;
  } top = 0;
  for(int i = 1; i <= n; i++){
    p[++cntp] = {i - 1, i - l[i] - 2, -s[i]};
    p[++cntp] = {r[i] - 1, r[i] - i - 2, -s[i]};
    p[++cntp] = {r[i] - 1, r[i] - l[i] - 2, s[i]};
  }
  sort(p + 1, p + cntp + 1, [&](Add x, Add y){return x.x < y.x;});
  for(int i = 1; i <= cntp; i++) ds1.update(p[i].x - p[i].y, p[i].a);
  for(int i = 1; i <= Q; i++){
    int t, l, r;
    cin >> t >> l >> r;
    q[++cntq] = {l - 1, t, -i};
    q[++cntq] = {r, t, i};
  }
  sort(q + 1, q + cntq + 1, [&](Query x, Query y){return x.x < y.x;});
  for(int i = 1, j = 0; i <= cntq; i++){
    while(j < cntp && p[j + 1].x < q[i].x){
      ++j;
      ds1.update(p[j].x - p[j].y, -p[j].a);
      ds2.update(p[j].y, p[j].a);
    }
    int id = abs(q[i].id), v = (q[i].id > 0 ? 1 : -1);
    ans[id] += 1ll * v * (ds1.query(q[i].x - q[i].y) + ds2.query(q[i].y));
  }
  for(int i = 1; i <= Q; i++) cout << ans[i] << '\n';
  return 0;
}

P10656 [ROI 2017] 学习轨迹 (Day 2)

好题啊。

暴力的话枚举 \(l_1,r_1\),维护序列 \(b\) 的最大子段和,发现这难以优化。

这种题有一个性质:两个序列的带权中点必定有一个会被选。

考虑反证法:假如两个带权中点都没选,则每个序列选出来的权值小于该序列权值和的一半,还不如直接全选权值和最大的那个序列。

我们钦定选序列 \(a\) 的带权中点,如果选 \(b\) 序列的话,\(\text{swap}\) 一下再做一遍即可。

由于我们确定了会选序列 \(a\) 中的带权中点 \(A\),所以我们可以给 \(b\) 序列分段,每段单独考虑。

有一个不错的想法是扫描 \(r_2\)。发现每次新加入一个元素,在序列 \(a\) 中可以选择的区间可能会变小。假设新加入的元素的颜色在序列 \(a\) 中出现的位置为 \(x\)。

若 \(x\le A\),记 \(lt_i=x\),否则记 \(rt_i=x\)。对于 \(b\) 中的一段区间 \([l,r]\),\(a\) 中选择的区间为 \((\max_{i=l}^r lt_i,\min_{i=l}^r rt_i)\),可以用单调栈维护 \(lt_i\) 和 \(rt_i\) 的极值,用线段树维护左端点的答案即可。

时间复杂度 \(\Theta(n\log n)\)。

#include <bits/stdc++.h>
using namespace std;
void File(){
  freopen("1.in", "r", stdin);
}
using ll = long long;
#define pli pair<ll, int>
#define mp(a, b) make_pair(a, b)
#define fi first
#define se second
const int kN = 5e5 + 7;
int n, m, c[kN], d[kN], delta, pos[kN << 1];
const ll INF = 1e18;
ll a[kN], b[kN], ans[5];
int stkl[kN], stkr[kN], tpl, tpr, lt[kN], rt[kN];
struct SGT{
  struct node{
    pli s;
    ll tg;
  }tree[kN << 2];
  void pushup(int x){
    tree[x].s = max(tree[x << 1].s, tree[x << 1 | 1].s);
    return ;
  }
  void maketag(int x, ll v){
    tree[x].s.fi += v;
    tree[x].tg += v;
    return ;
  }
  void pushdown(int x){
    maketag(x << 1, tree[x].tg);
    maketag(x << 1 | 1, tree[x].tg);
    tree[x].tg = 0;
    return ;
  }
  void build(int x, int l, int r){
    tree[x].tg = 0;
    if(l == r){
      tree[x].s = mp(-INF, l);
      return ;
    }
    int mid = l + r >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    pushup(x);
    return ;
  }
  void update(int x, int l, int r, int L, int R, ll v){
    if(L <= l && r <= R){
      maketag(x, v);
      return ;
    } pushdown(x);
    int mid = l + r >> 1;
    if(L <= mid) update(x << 1, l, mid, L, R, v);
    if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
    pushup(x);
    return ;
  }
}sgt;
void solve(int *c, int *d, ll *a, ll *b, int n, int m, int op){
  int z = 0;
  for(int i = 1; i <= n; i++){
    if(a[i] * 2ll > a[n]){
      z = i;
      break;
    }
  }
  for(int i = 1; i <= m; i++){
    if(d[i] == c[z]){
      if(i > 1) solve(c, d, a, b, n, i - 1, op);
      if(i < m) delta += i, solve(c, d + i, a, b + i, n, m - i, op), delta -= i;
      return ;
    }
  }
  memset(pos, 0, sizeof(pos));
  for(int i = 1; i <= n; i++) pos[c[i]] = i;
  sgt.build(1, 1, m);
  tpl = tpr = 0;
  for(int i = 1; i <= m; i++){
    sgt.update(1, 1, m, i, i, INF);
    sgt.update(1, 1, m, 1, i, b[i] - b[i - 1]);
    int x = pos[d[i]];
    lt[i] = 0, rt[i] = n;
    if(x && x < z) lt[i] = x;
    if(x && x > z) rt[i] = x - 1;
    while(tpl && lt[stkl[tpl]] <= lt[i]){
      sgt.update(1, 1, m, stkl[tpl - 1] + 1, stkl[tpl], a[lt[stkl[tpl]]]);
      tpl--;
    }
    while(tpr && rt[stkr[tpr]] >= rt[i]){
      sgt.update(1, 1, m, stkr[tpr - 1] + 1, stkr[tpr], -a[rt[stkr[tpr]]]);
      tpr--;
    }
    sgt.update(1, 1, m, stkl[tpl] + 1, i, -a[lt[i]]);
    sgt.update(1, 1, m, stkr[tpr] + 1, i, a[rt[i]]);
    stkl[++tpl] = i, stkr[++tpr] = i;
    auto [u, v] = sgt.tree[1].s;
    if(u > ans[0]){
      ans[0] = u;
      int l1, r1, l2, r2;
      l2 = v, r2 = i;
      l1 = lt[*lower_bound(stkl + 1, stkl + tpl + 1, v)] + 1;
      r1 = rt[*lower_bound(stkr + 1, stkr + tpr + 1, v)];
      l2 += delta, r2 += delta;
      if(op) swap(l1, l2), swap(r1, r2);
      ans[1] = l1, ans[2] = r1;
      ans[3] = l2, ans[4] = r2;
      if(op) swap(l1, l2), swap(r1, r2);
    }
  }
  return ;
}
signed main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for(int i = 1; i <= n; i++) cin >> c[i];
  for(int i = 1; i <= n; i++) cin >> a[i], a[i] += a[i - 1];
  for(int i = 1; i <= m; i++) cin >> d[i];
  for(int i = 1; i <= m; i++) cin >> b[i], b[i] += b[i - 1];
  if(a[n] >= b[m]){;
    ans[0] = a[n], ans[1] = 1, ans[2] = n, ans[3] = ans[4] = 0;
  }
  else{
    ans[0] = b[m], ans[1] = ans[2] = 0, ans[3] = 1, ans[4] = m;
  }
  solve(c, d, a, b, n, m, 0);
  solve(d, c, b, a, m, n, 1);
  cout << ans[0] << '\n';
  cout << ans[1] << ' ' << ans[2] << '\n';
  cout << ans[3] << ' ' << ans[4] << '\n';
  return 0;
}

P13842 篱莘龙

考虑特殊性质,发现形如 \([b_i,a_i]\) 的线段至多选取一个,形如\([a_i,b_i]\) 的线段两两必须相交。

考虑既有 \([b_i,a_i]\) 的线段,又有 \([a_i,b_i]\) 的线段怎么处理。

首先,我们可以发现如果先选择了一条 \([b_i,a_i]\) 的线段,再选其他的 \([a_j,b_j]\) 的线段,其他的线段都要满足 \([b_i,a_i]\subseteq [a_j,b_j]\)。

最关键的一步来了,如果对于线段 \([b_i,a_i]\),存在其他的线段 \([a_j,b_j]\) 不满足 \([b_i,a_i]\subseteq [a_j,b_j]\),我们称这个线段 \([b_i,a_i]\) 是无用的。

因为后者的限制严格弱于前者。

接下来考虑如何实现。把所有的 \([a_i,b_i]\) 线段的两个端点插入 set 中。

假设当前我们加入的是 \([a_i,b_i]\) 的线段,相当于我们把原来的区间 \([l,r]\) 拆成了 \([l,mid]\) 和 \([mid,r]\)。我们把 \([a_i,b_i]\) 处的贡献加 \(1\),同时需要把跨 \(mid\) 的区间 \([b_i,a_i]\) 的贡献删除,由于至多加入 \(O(n)\) 条线段,所以我们删除也只会删 \(O(n)\) 条线段。考虑在每个 \([b_i,a_i]\) 线段的右端点 \(a_i\) 维护 \(b_i\) 的值,然后我们每次暴力查询右端点在 \([mid,r]\) 中的最小的 \(b_i\),如果 \(b_i\le mid\),我们就删除这个线段的贡献。

假设当前新加的是一个 \([b_i,a_i]\) 线段,我们可以简单的判断 set 中是否存在端点在 \([b_i,a_i]\) 之间,如果有的话,就不插入。否则插入并在 \(a_i\) 处加 \(1\),并在 \(a_i\) 处更新 \(b_i\)。

我们用线段树实现上面即可。

发现交上去会被卡常,我们把 set 换成 BIT 即可,具体操作为 BIT 维护 0-1 序列,然后树状数组倍增查询最靠前的前缀和为定值 \(S\) 的位置。

复杂度 \(\Theta(n \log n)\)。

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

void File(){
  freopen("nailong.in", "r", stdin);
  freopen("nailong.out", "w", stdout);
}

#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kN = 1e6 + 7;
int n, a[kN], b[kN];
struct BIT{
  int tree[kN << 1];
  void update(int x, int v){
    for(int i = x; i <= 2 * n; i += i & -i) tree[i] += v;
    return ;
  }
  int query(int x){
    int res = 0;
    for(int i = x; i; i -= i & -i) res += tree[i];
    return res;
  }
  int find(int s){
    int x = 0;
    for(int j = 20; j >= 0; j--){
      if(x + (1 << j) <= 2 * n && tree[x + (1 << j)] < s){
        s -= tree[x + (1 << j)];
        x += (1 << j);
      }
    }
    return x + 1;
  }
}bit;
struct SGT{
  struct node{
    int tag, mx;
    pii mn;
  }tree[kN << 3];
  void pushup(int x){
    tree[x].mn = min(tree[x << 1].mn, tree[x << 1 | 1].mn);
    tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
    return ;
  }
  void build(int x, int l, int r){
    tree[x].mn = mp(2 * n + 1, 0);
    if(l == r) return ;
    int mid = l + r >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    return ;
  }
  void maketag(int x, int l, int r, int v){
    tree[x].tag += v;
    tree[x].mx += v;
    return ;
  }
  void pushdown(int x, int l, int r){
    if(tree[x].tag){
      int mid = l + r >> 1;
      maketag(x << 1, l, mid, tree[x].tag);
      maketag(x << 1 | 1, mid + 1, r, tree[x].tag);
      tree[x].tag = 0;
    }
    return ;
  }
  void add(int x, int l, int r, int L, int R, int v){
    if(L <= l && r <= R){
      maketag(x, l, r, v);
      return ;
    }
    pushdown(x, l, r);
    int mid = l + r >> 1;
    if(L <= mid) add(x << 1, l, mid, L, R, v);
    if(mid < R) add(x << 1 | 1, mid + 1,r , L, R, v);
    pushup(x);
    return ;
  }
  void upd(int x, int l, int r, int pos, pii v){
    if(l == r){
      tree[x].mn = v;
      return ;
    }
    pushdown(x, l, r);
    int mid = l + r >> 1;
    if(pos <= mid) upd(x << 1, l, mid, pos, v);
    else upd(x << 1 | 1, mid + 1, r, pos, v);
    pushup(x);
    return ;
  }
  pii qry(int x, int l, int r, int L, int R){
    if(L <= l && r <= R) return tree[x].mn;
    pushdown(x, l, r);
    int mid = l + r >> 1;
    pii ans = mp(2 * n + 1, 0);
    if(L <= mid) ans = min(ans, qry(x << 1, l, mid, L, R));
    if(mid < R) ans = min(ans, qry(x << 1 | 1, mid + 1, r, L, R));
    pushup(x);
    return ans;
  }
}sgt;
void Ins(int x){
  int r = bit.find(bit.query(x) + 1) - 1;
  r = min(r, 2 * n);
  bit.update(x, 1);
  if(x > r) return ;
  while(1){
    pii v = sgt.qry(1, 1, 2 * n, x, r);
    if(v.first > x) break;
    sgt.add(1, 1, 2 * n, a[v.second], a[v.second], -1);
    sgt.upd(1, 1, 2 * n, a[v.second], mp(2 * n + 1, 0));
  } 
  return ;
}
signed main(){
  // File();
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  for(int i = 1; i <= n; i++) cin >> a[i] >> b[i];
  sgt.build(1, 1, 2 * n);
  for(int k = 1; k <= n; k++){
    if(a[k] < b[k]){
      Ins(a[k]);
      Ins(b[k]);
      sgt.add(1, 1, 2 * n, a[k], b[k], 1);
    }
    else{
      int it = bit.find(bit.query(b[k]) + 1);
      if(it > a[k]){
        sgt.add(1, 1, 2 * n, a[k], a[k], 1);
        sgt.upd(1, 1, 2 * n, a[k], mp(b[k], k));
      }
    }
    cout << sgt.tree[1].mx << '\n';
  }
  return 0;
}
posted @ 2026-07-26 17:03  CJ-rnd  阅读(28)  评论(0)    收藏  举报