DS 练习

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;
}
posted @ 2026-07-26 17:03  CJ-rnd  阅读(4)  评论(0)    收藏  举报