线段树

线段树本质上是一个二分区间的结构。或二分下标,或二分值域,又时也能二分时间轴。他们都有相应的思想、目的、适用场景。而利用线段树对区间划分的特性,可以使用它来维护各种信息,因此线段树具有着众多分支,但它们的本质相同。

作者并不擅长线段树的使用,故进行一些专项练习。

T1

portal

题意:给定一个长度为 \(n\) 的小写字母串,有 \(m\) 次询问:区间 \([l,r]\) 中的子母如果能重组得到一个回文串,就保留修改。问最后的字符串。\(1\leq n,m\leq 10^5\)请使用文件读写,从 "input.txt" 内读入,"output" 中输出

Solution

考虑对每个子母开一个线段树,维护区间内这个子母出现过多少次。修改时,暴力记录每个子母有多少个,记作 \(b_i\)。如果有超过 \(1\) 个子母个数为奇数,就不可能组成回文串;如果有一个,就把那个子母放在正中间。其余部分按照子母序两边暴力放就行了。这是个明显的区间赋值操作,打个 tag 就行了。时间复杂度 \(O(26(n + m)\log n)\)

\(\\\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
#include <fstream>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1, kA = 26;
ifstream fin("input.txt");
ofstream fout("output.txt");

int n, m;
string s;

struct Segment {
  struct Node {
    int w, t;
  } t[4 * kN];
  void PU(int x) { t[x].w = t[2 * x].w + t[2 * x + 1].w; }
  void PD(int x, int l, int r) {
    if (t[x].t == -1) {
      return;
    }
    t[2 * x].t = t[2 * x + 1].t = t[x].t;
    int m = (l + r) / 2;
    t[2 * x].w = t[x].t * (m - l + 1);
    t[2 * x + 1].w = t[x].t * (r - m);
    t[x].t = -1;
  }
  void B(int c, int x = 1, int l = 1, int r = n) {
    t[x] = {0, -1};
    if (l == r) {
      t[x].w = (s[l] - 'a' == c);
      return;
    }
    int m = (l + r) / 2;
    B(c, 2 * x, l, m);
    B(c, 2 * x + 1, m + 1, r);
    PU(x);
  }
  void U(int L, int R, int w, int x = 1, int l = 1, int r = n) {
    if (r < L || R < l) {
      return;
    } else if (L <= l && r <= R) {
      t[x].t = w;
      t[x].w = w * (r - l + 1);
      return;
    }
    int m = (l + r) / 2;
    PD(x, l, r);
    U(L, R, w, 2 * x, l, m);
    U(L, R, w, 2 * x + 1, m + 1, r);
    PU(x);
  }
  int Q(int L, int R, int x = 1, int l = 1, int r = n) {
    if (r < L || R < l) {
      return 0;
    } else if (L <= l && r <= R) {
      return t[x].w;
    }
    int m = (l + r) / 2;
    PD(x, l, r);
    return Q(L, R, 2 * x, l, m) + Q(L, R, 2 * x + 1, m + 1, r);
  }
} t[26];

int b[kA], o, p;
int main() {
  fin.tie(0)->sync_with_stdio(0);
  fin >> n >> m;
  fin >> s, s = '#' + s;
  for (int i = 0; i < kA; i++) {
    t[i].B(i);
  }
  for (int _ = 1, l, r; _ <= m; _++) {
    fin >> l >> r;
    fill_n(b, kA, 0), o = 0;
    for (int i = 0; i < kA; i++) {
      b[i] = t[i].Q(l, r);
      if (b[i] % 2 == 1) {
        o++, p = i;
      }
    }
    if (o > 1) {
      continue;
    }
    for (int i = 0; i < kA; i++) {
      t[i].U(l, r, 0);
    }
    if (o == 1) {
      int m = (l + r) / 2;
      t[p].U(m, m, 1), b[p]--;
    }
    for (int i = 0; i < kA; i++) {
      if (!b[i]) {
        continue;
      }
      t[i].U(l, l + b[i] / 2 - 1, 1);
      t[i].U(r - b[i] / 2 + 1, r, 1);
      l += b[i] / 2, r -= b[i] / 2;
    }
  }
  for (int i = 1; i <= n; i++) {
    for (int c = 0; c < kA; c++) {
      if (t[c].Q(i, i)) {
        fout << char(c + 'a');
        break;
      }
    }
  }
  return 0;
}

T2

portal

题意:给定一个大小为 \(n\) 的序列 \(h\),进行 \(q\) 次操作:单点修改值;在 \(n\) 个数中选择其中几个,并把值 \(v\) 任意分配加到这些数中(可以不是整数),问这些数可能的最小最大值,不保留更改。\(1\leq n,q\leq 10^5\)

Solution

最大值最小,一眼二分答案。你发现我选择的这几个数一定在值域区间 \([1, r]\) 内全选。于是用权值线段树维护一下值域区间内有多少个数、这些数的和。然后暴力二分最大值。时间复杂度 \(O(n\log^2 V)\),足以通过此题。

但是作者认为这不够美丽。因为权值线段树本身就是一个按权值二分的 DS,可以直接线段树上二分。但是,直接二分需要支持实数域的线段树上二分,更不优雅了。考虑改变二分对象:你发现一定是把选中的数都加到一个定值,然后平均分,否则会浪费选中数数量。因此可以二分这个定值,计算出答案。考虑到无论何时序列中的数都是整数,就可以直接整数域二分了。时间复杂度 \(O(n\log V)\)

\(\\\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
#include <iomanip>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;

int n, q;
int h[kN];

struct Node {
  int l, r, sz;
  LL w;
} t[50 * kN];
int rt, tot;
void U(int h, int w = 1, int &x = rt, int l = 0, int r = 1e9) {
  if (!x) {
    x = ++tot;
  }
  t[x].sz += w, t[x].w += 1ll * h * w;
  if (l == r) {
    return;
  }
  int m = (l + r) / 2;
  if (h <= m) {
    U(h, w, t[x].l, l, m);
  } else {
    U(h, w, t[x].r, m + 1, r);
  }
}
LL sz, ans;
void Q(LL lm, int x = 1, int l = 0, int r = 1e9) {
  if (!x) {
    return;
  } else if (l == r) {
    sz += t[x].sz, ans += t[x].w;
    return;
  }
  int m = (l + r) / 2;
  LL f = (t[t[x].l].sz + sz) * (m + 1) - (t[t[x].l].w + ans);
  if (f > lm) {
    Q(lm, t[x].l, l, m);
  } else {
    sz += t[t[x].l].sz, ans += t[t[x].l].w;
    Q(lm, t[x].r, m + 1, r);
  }
}

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> q;
  for (int i = 1; i <= n; i++) {
    cin >> h[i];
    U(h[i]);
  }
  for (int _ = 1, o; _ <= q; _++) {
    cin >> o;
    if (o == 1) {
      int l, r;
      cin >> l >> r;
      U(h[l], -1), h[l] = r, U(r);
    } else {
      LL l;
      cin >> l;
      sz = ans = 0, Q(l);
      cout << fixed << setprecision(5) << 1.0 * (l + ans) / sz << '\n';
    }
  }
  return 0;
}

T3

portal

题意:给定一个长度为 \(n\) 的字符串 \(s\),将进行 \(m + k\) 次操作:区间赋值、区间询问是否存在长度为 \(d\) 的循环节(可以是混周期,最后一个周期可以不全)。\(1\leq n, m + k\leq 10^5\)

Solution

这里有一个经典的字符串结论,即 “有长度为 \(n - d\) 的 border” 和 “有长度为 \(d\) 的循环节” 互为充要条件。

Proof

首先由后者推出前者是显然可行的,这里试证前者推后者。

\(n - d > \frac{n}{2}\),即 border 存在重叠部分。设前面、后面的 border 为区间 \(s[1, i]\)\(s(j, n]\)。由 border 定义有 \(s[1, j] = s(j, 2j], s(j, 2j] = s(2j, 3j] \cdots\) 如此反复,最后会剩下小于 \(j\) 个字符:如果他是后缀的末尾,那么一定也被前缀的末尾匹配,而前缀的末尾又一定是一个循环节的前缀。

\(n - d \leq \frac{n}{2}\),直接把 \(s[1, j]\) 当作循环节。因为 \(s[1, i] = s[j, n]\),所以 \(s[j, n]\) 和循环节的一个前缀相等,显然成立。

于是可以考虑开个线段树维护哈希值,需要支持区间赋值和区间查询哈希值。难点和细节点在于哈希值的转移、合并。可以预处理下 base 的次幂的前缀和,以便区间赋值时迅速算出新哈希值。

\(\\\)

Code
// stODDDDDDDDDDDDDDDDDDDDDDDDDDD hzt CCCCCCCCCCCCCCCCCCCCCCCCCOrz

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using PII = pair<int, int>;
using LL = long long;
constexpr int kN = 1e5 + 1, kB = 131, kP = 777777773;

int n, m, k;
string s;
LL p[kN], sp[kN];

struct Node {
  LL f;
  char t;
} t[4 * kN];
void PU(int x, int l, int r) {
  int m = (l + r) / 2;
  t[x].f = (t[2 * x].f * p[r - m] % kP + t[2 * x + 1].f) % kP;
}
void PD(int x, int l, int r) {
  if (t[x].t != -1) {
    int m = (l + r) / 2;
    t[2 * x].f = t[x].t * sp[m - l] % kP;
    t[2 * x + 1].f = t[x].t * sp[r - m - 1] % kP;
    t[2 * x].t = t[2 * x + 1].t = t[x].t;
    t[x].t = -1;
  }
}
void B(int x = 1, int l = 1, int r = n) {
  t[x].t = -1;
  if (l == r) {
    t[x].f = s[l];
    return;
  }
  int m = (l + r) / 2;
  B(2 * x, l, m);
  B(2 * x + 1, m + 1, r);
  PU(x, l, r);
}
void U(int L, int R, char c, int x = 1, int l = 1, int r = n) {
  if (R < l || r < L) {
    return;
  } else if (L <= l && r <= R) {
    t[x].f = c * sp[r - l] % kP;
    t[x].t = c;
    return;
  }
  int m = (l + r) / 2;
  PD(x, l, r);
  U(L, R, c, 2 * x, l, m);
  U(L, R, c, 2 * x + 1, m + 1, r);
  PU(x, l, r);
}
LL Q(int L, int R, int x = 1, int l = 1, int r = n) {
  if (R < l || r < L) {
    return 0;
  } else if (L <= l && r <= R) {
    return t[x].f;
  }
  int m = (l + r) / 2;
  PD(x, l, r);
  LL al = Q(L, R, 2 * x, l, m), ar = Q(L, R, 2 * x + 1, m + 1, r);
  return (al * p[max(0, min(r, R) - m)] + ar) % kP;
}

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m >> k >> s, s = '#' + s;
  p[0] = sp[0] = 1;
  for (int i = 1; i <= n; i++) {
    p[i] = kB * p[i - 1] % kP;
    sp[i] = (sp[i - 1] + p[i]) % kP;
  }
  B();
  for (int _ = 1, o, l, r, p; _ <= m + k; _++) {
    cin >> o >> l >> r >> p;
    if (o == 1) {
      U(l, r, p + '0');
    } else {
      if (Q(l, r - p) == Q(l + p, r)) {
        cout << "YES\n";
      } else {
        cout << "NO\n";
      }
    }
  }
  return 0;
}
posted @ 2023-11-03 22:25  Lightwhite  阅读(27)  评论(0)    收藏  举报