模板库


写在前面

好困啊,不想写题,来写板子了。


基础

快读

inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) {
    if (ch == '-') {
      f = -1; 
    }
  }
  for (; isdigit(ch); ch = getchar()) {
    w = (w << 3) + (w << 1) + (ch ^ '0'); 
  }
  return f * w;
}

随机树生成器

#include <bits/stdc++.h>
const int kTreeSize = 100000;
int main() {
  srand(time(0));
  int n = kTreeSize;
  printf("%d\n", n);
  for (int i = 2; i <= n; ++ i) {
    printf("%d %d\n", i, rand() % (i - 1) + 1);
  }
  return 0;
}

随机图生成器

保证无自环重边。

#include <bits/stdc++.h>
#define pr std::pair
#define mp std::make_pair
const int kNodeSize = 10;
const int kEdgeSize = 20;
std::set <pr <int, int> > has;
int main() {
  srand(time(0));
  int n = kNodeSize, m = kEdgeSize;
  printf("%d %d\n", n, m);
  for (int i = 1; i <= m; ++ i) {
    int u_ = rand() % n + 1, v_ = rand() % n + 1;
    while (u_ == v_ || has.count(mp(u_, v_))) {
      u_ = rand() % n + 1, v_ = rand() % n + 1;
    }
    has.insert(mp(u_, v_));
    printf("%d %d\n", u_, v_);
  }
  return 0;
}

Checker

#include <bits/stdc++.h>
int main() {
  for (int i = 1; ; i ++) {
    printf("%d:", i);
    system("datamaker.exe");
    system("force.exe");
    system("std.exe");
    if (system("fc std.in force.in")) {
      break;
    }
  }
}

数据结构

并查集

将集合关系转化为图论中连通块的方式进行维护。

P3367 【模板】并查集

路径压缩 + 按秩合并

均摊复杂度 \(O(m)\)

//知识点:并查集 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
//=============================================================
int n, m, fa[kMaxn], size[kMaxn]; 
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
int Find(int x_) {
  return x_ == fa[x_] ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int u_, int v_) {
  int fu = Find(u_), fv = Find(v_);
  if (fu == fv) return ;
  if (size[fu] > size[fv]) std::swap(fu, fv);
  fa[fu] = fv;
  size[fv] += size[fu];
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    fa[i] = i;
    size[i] = 1;
  }
  for (int i = 1; i <= m; ++ i) {
    int opt = read(), x = read(), y = read();
    if (opt == 1) {
      Union(x, y);
    } else {
      printf("%c\n", Find(x) == Find(y) ? 'Y' : 'N');
    }
  }
  return 0;
}

可撤销并查集

记录操作的变化,压入栈中。
为保证回退次数,只能使用按秩合并。

//知识点:可撤销并查集 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = 2e5 + 10;
//=============================================================
int n, m, top, fa[kMaxn], size[kMaxn]; 
struct Stack {
  int u, v, fa, size;
} st[kMaxm];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
int Find(int x_) {
  return x_ == fa[x_] ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int u_, int v_) {
  int fu = Find(u_), fv = Find(v_);
  if (size[fu] > size[fv]) std::swap(fu, fv);
  st[++ top] = (Stack) {fu, fv, fa[fu], size[fu]};
  if (fu != fv) {
    fa[fu] = fv;
    size[fv] += size[fu];
    size[fu] = 0;
  }
}
void Restore() {
  Stack now = st[top];
  if (now.u != now.v) {
    fa[now.u] = now.fa;
    size[now.v] -= now.size;
    size[now.u] = now.size;
  }
  top --;
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    fa[i] = i;
    size[i] = 1;
  }
  for (int i = 1; i <= m; ++ i) {
    int opt = read(), x = read(), y = read();
    if (opt == 1) {
      Union(x, y);
    } else {
      printf("%c\n", Find(x) == Find(y) ? 'Y' : 'N');
    }
  }
  return 0;
}

单调栈

求每个元素右侧第一个比它大的数的位置。

维护一个单调递减的栈。

P5788 【模板】单调栈

//知识点:单调栈 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e6 + 10;
//=============================================================
int n, top, a[kMaxn], st[kMaxn], ans[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  for (int i = 1; i <= n; ++ i) {
    while (top && a[st[top]] < a[i]) {
      ans[st[top]] = i;
      top --;
    }
    st[++ top] = i;
  }
  for (int i = 1; i <= n; ++ i) printf("%d ", ans[i]);
  return 0;
}

单调队列

滑动窗口求区间最大值。

P1886 滑动窗口 /【模板】单调队列

//知识点:单调队列
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, k, a[kMaxn];
int h, t, q[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Init() {
  h = t = 0;
}
void InsertMin(int pos_) {
  while (t >= h && a[q[t]] >= a[pos_]) {
    t --;
  }
  q[++ t] = pos_;
}
void InsertMax(int pos_) {
  while (t >= h && a[q[t]] <= a[pos_]) {
    t --;
  }
  q[++ t] = pos_;
}
int Query(int pos_) {
  while (t >= h && q[h] + k <= pos_) ++ h;
  return a[q[h]];
}
//=============================================================
int main() {
  n = read(), k = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  for (int i = 1; i < k; ++ i) InsertMin(i);
  for (int i = k; i <= n; ++ i) {
    InsertMin(i);
    printf("%d ", Query(i));
  }
  
  printf("\n");
  Init();
  for (int i = 1; i < k; ++ i) InsertMax(i);
  for (int i = k; i <= n; ++ i) {
    InsertMax(i);
    printf("%d ", Query(i));
  }
  return 0;
}

树状数组

二进制优化前缀和,维护前缀信息。

若想维护区间信息必须具有可减性。

单点加区间和

P3374 【模板】树状数组 1

//知识点:树状数组
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Bit {
  #define lowbit(x) (x&-x)
  int Lim;
  LL t[kMaxn];
  void Init(int lim_) {
    Lim = lim_;
  }
  void Insert(int pos_, LL val_) {
    for (int i = pos_; i <= Lim; i += lowbit(i)) {
      t[i] += val_;
    }
  }
  LL Sum(int pos_) {
    LL ret = 0;
    for (int i = pos_; i; i -= lowbit(i)) {
      ret += t[i];
    }
    return ret;
  }
  #undef lowbit
}
//=============================================================
int main() {
  n = read(), m = read();
  Bit::Init(n);
  for (int i = 1; i <= n; ++ i) {
    a[i] = read();
    Bit::Insert(i, a[i]); 
  }
  while (m --) {
    int opt = read();
    if (opt == 1) {
      int x = read(), k = read();
      Bit::Insert(x, 1ll * k);
    } else {
      int x = read(), y = read();
      printf("%lld\n", Bit::Sum(y) - Bit::Sum(x - 1));
    }
  }
  return 0;
}

ST 表

用长度为 2 的幂的区间覆盖查询区间。
维护不具有可减性信息。

静态区间最大值。

P3865 【模板】ST表

//
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace ST {
  int Log2[kMaxn], f[kMaxn][22];
  void MakeST () {
    Log2[1] = 0;
    for (int i = 1; i <= n; ++ i) {
      f[i][0] = a[i];
      if (i > 1) Log2[i] = Log2[i >> 1] + 1;
    }
    
    for (int i = 1; i <= 22; ++ i) {
      for (int j = 1; j + (1 << i) - 1 <= n; ++ j) {
        f[j][i] = std::max(f[j][i - 1],
                           f[j + (1 << (i - 1))][i - 1]);
      }
    }
  }
  int Query(int l_, int r_) {
    int lth = Log2[r_ - l_ + 1];
    return std::max(f[l_][lth], f[r_ - (1 << lth) + 1][lth]);
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  ST::MakeST();
  while (m --) {
    int l = read(), r = read();
    printf("%d\n", ST::Query(l, r));
  }
  return 0;
}

分块

想了想还是把分块扔到数据结构里了= =

分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。

Loj6280. 数列分块入门 4

//知识点:分块
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cmath>
#include <cstring>
#define LL long long
const int kN = 5e4 + 10;
//=============================================================
int n, m;
int block_size, block_num, L[kN], R[kN], bel[kN];
LL a[kN], sum[kN], tag[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmin(LL &fir, LL sec) {
  if (sec < fir) fir = sec;
}
void PrepareBlock() {
  block_size = sqrt(n); //根据实际要求选择一个合适的大小。
  block_num = n / block_size; 
  for (int i = 1; i <= block_num; ++ i) { //分配块左右边界。
    L[i] = (i - 1) * block_size + 1;
    R[i] = i * block_size;
  }
  if (R[block_num] < n) { //最后的一个较小的块。
    ++ block_num;
    L[block_num] = R[block_num - 1] + 1;
    R[block_num] = n;
  }
  //分配元素所属的块编号。
  for (int i = 1; i <= block_num; ++ i) {
    for (int j = L[i]; j <= R[i]; ++ j) {
      bel[j] = i;
      sum[i] += a[j]; //预处理区间和
    }
  }
}
void Modify(int L_, int R_, int val_) {
  int bell = bel[L_], belr = bel[R_];
  if (bell == belr) {
    for (int i = L_; i <= R_; ++ i) {
      a[i] += val_;
      sum[bell] += val_;
    }
    return ;
  }
  for (int i = bell + 1; i <= belr - 1; ++ i) tag[i] += val_;
  for (int i = L_; i <= R[bell]; ++ i) {
    a[i] += val_;
    sum[bell] += val_;
  }
  for (int i = L[belr]; i <= R_; ++ i) {
    a[i] += val_;
    sum[belr] += val_;
  }
}
LL Query(int L_, int R_, int mod_) {
  int bell = bel[L_], belr = bel[R_], ret = 0;
  if (bell == belr) {
    for (int i = L_; i <= R_; ++ i) ret = (ret + a[i] + tag[bell]) % mod_;
    return ret;
  }
  for (int i = bell + 1; i <= belr - 1; ++ i) {
    ret = (ret + sum[i] + (R[i] - L[i] + 1) * tag[i] % mod_) % mod_;
  }
  for (int i = L_; i <= R[bell]; ++ i) ret = (ret + a[i] + tag[bell]) % mod_;
  for (int i = L[belr]; i <= R_; ++ i) ret = (ret + a[i] + tag[belr]) % mod_;
  return ret;
}
//=============================================================
int main() { 
  n = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  PrepareBlock();
  for (int i = 1; i <= n; ++ i) {
    int opt = read(), l = read(), r = read(), c = read();
    if (opt == 0) {
      Modify(l, r, c);
    } else {
      printf("%lld\n", Query(l, r, c + 1));
    }
  }
  return 0; 
}

线段树

区间加区间和

P3372 【模板】线段树 1

//知识点:线段树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Seg {
  #define ls (now_<<1)
  #define rs (now_<<1|1)
  #define mid ((L_+R_)>>1)
  LL sum[kMaxn << 2], tag[kMaxn << 2];
  void Pushup(int now_) {
    sum[now_] = sum[ls] + sum[rs];
  }
  void Pushdown(int now_, int L_, int R_) {
    sum[ls] += 1ll * tag[now_] * (mid - L_ + 1);
    sum[rs] += 1ll * tag[now_] * (R_ - mid);
    tag[ls] += tag[now_];
    tag[rs] += tag[now_];
    tag[now_] = 0ll;
  }
  void Build(int now_, int L_, int R_) {
    if (L_ == R_) {
      sum[now_] = a[L_];
      tag[now_] = 0ll;
      return ;
    }
    Build(ls, L_, mid);
    Build(rs, mid + 1, R_);
    Pushup(now_); 
  }
  void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
    if (l_ <= L_ and R_ <= r_) {
      sum[now_] += 1ll * (R_ - L_ + 1) * val_;
      tag[now_] += val_;
      return ;
    }
    Pushdown(now_, L_, R_);
    if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
    if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
    Pushup(now_);
  }
  LL Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ and R_ <= r_) return sum[now_];
    Pushdown(now_, L_, R_);
    LL ret = 0;
    if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
    if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
    return ret;
  }
  #undef ls
  #undef rs
  #undef mid
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  Seg::Build(1, 1, n);
  while (m --) {
    int opt = read();
    if (opt == 1) {
      int l = read(), r = read(), k = read();
      Seg::Modify(1, 1, n, l, r, 1ll * k);
    } else {
      int l = read(), r = read();
      printf("%lld\n", Seg::Query(1, 1, n, l, r));
    }
  }
  return 0;
}

可持久化线段树

P3919 【模板】可持久化线段树 1(可持久化数组)

单点修改,单点历史版本查询。

//知识点:可持久化线段树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, a[kMaxn], root[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Seg {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid ((L_+R_)>>1)
  int node_num, lson[kMaxn << 5], rson[kMaxn << 5], val[kMaxn << 5];
  void Build(int &now_, int L_, int R_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      val[now_] = a[L_];
      return ;
    }
    Build(ls, L_, mid);
    Build(rs, mid + 1, R_);
  }
  void Modify(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      val[now_] = val_;
      return ;
    }
    ls = lson[pre_];
    rs = rson[pre_];
    if (pos_ <= mid) Modify(ls, lson[pre_], L_, mid, pos_, val_);
    else Modify(rs, rson[pre_], mid + 1, R_, pos_, val_);
  }
  int Query(int now_, int L_, int R_, int pos_) {
    if (L_ == R_) return val[now_];
    if (pos_ <= mid) return Query(ls, L_, mid, pos_);
    return Query(rs, mid + 1, R_, pos_);
  }
  #undef ls
  #undef rs
  #undef mid
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  Seg::Build(root[0], 1, n);
  for (int i = 1; i <= m; ++ i) {
    int pre = read(), opt = read(), pos = read();
    if (opt == 1) {
      int val = read();
      Seg::Modify(root[i], root[pre], 1, n, pos, val);
    } else {
      printf("%d\n", Seg::Query(root[pre], 1, n, pos));
      root[i] = root[pre];
    }
  }
  return 0;
}

动态开点线段树合并

动态开点 的线段树的合并。
总复杂度是 \(O(n\log n)\)\(n\) 是节点个数。

P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并

树上路径涂色,修改完后单点查询最多涂色数。

//知识点:线段树合并 
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define LL long long
const int kMaxn = 3e5 + 10;
const int kInf = 1e5 + 5;
//=============================================================
int n, m, root[kMaxn];
int edge_num, head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1];
int dep[kMaxn], fa[kMaxn], son[kMaxn], top[kMaxn], size[kMaxn];
int ans[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
void AddEdge(int u_, int v_) {
  v[++ edge_num] = v_; 
  ne[edge_num] = head[u_];
  head[u_] = edge_num;
}
namespace Cut {
  void Dfs1(int u_, int fat_) {
    fa[u_] = fat_;
    size[u_] = 1;
    dep[u_] = dep[fat_] + 1;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fat_) continue ;
      Dfs1(v_, u_);
      size[u_] += size[v_];
      if (size[v_] > size[son[u_]]) son[u_] = v_;
    }
  }
  void Dfs2(int u_, int topn_) {
    top[u_] = topn_;
    if (son[u_]) Dfs2(son[u_], topn_);
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == son[u_] || v_ == fa[u_]) continue ;
      Dfs2(v_, v_);
    }
  }
  int GetLca(int x_, int y_) {
    for (; top[x_] != top[y_]; x_ = fa[top[x_]]) {
      if (dep[top[x_]] < dep[top[y_]]) std :: swap(x_, y_); 
    }
    return dep[x_] < dep[y_] ? x_ : y_;
  }
}

namespace Seg {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid ((L_+R_)>>1)
  int node_num, lson[kMaxn << 5], rson[kMaxn << 5], sum[kMaxn << 5];
  void Pushup(int now_) {
    sum[now_] = std::max(sum[ls], sum[rs]);
  }
  void Insert(int &now_, int L_, int R_, int pos_, int val_) {
    if (! now_) now_ = ++ node_num;
    if (L_ == R_) {
      sum[now_] += val_;
      return ;
    }
    if (pos_ <= mid) Insert(ls, L_, mid, pos_, val_);
    else Insert(rs, mid + 1, R_, pos_, val_);
    Pushup(now_);
  }
  //不新建结点写法,now 直接连上了 y。
  //若之后 now 还和其他的合并,就破坏了 y 的结构。
  int Merge(int now_, int y_, int L_, int R_) {
    if (! now_ || ! y_) return now_ + y_;
    if (L_ == R_) {
      sum[now_] += sum[y_];
      return now_;
    }
    ls = Merge(ls, lson[y_], L_, mid);
    rs = Merge(rs, rson[y_], mid + 1, R_);
    Pushup(now_);
    return now_;
  }
  int Query(int now_, int L_, int R_) {
    if (L_ == R_) return L_;
    if (sum[ls] >= sum[rs]) return Query(ls, L_, mid);
    else return Query(rs, mid + 1, R_);
  }
  #undef ls
  #undef rs
  #undef mid
}
void Dfs(int u_) {
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa[u_]) continue ;
    Dfs(v_);
    root[u_] = Seg::Merge(root[u_], root[v_], 1, kInf);
  }
  if (! Seg::sum[root[u_]]) ans[u_] = 0;
  else ans[u_] = Seg::Query(root[u_], 1, kInf);
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i < n; ++ i) {
    int u = read(), v = read();
    AddEdge(u, v), AddEdge(v, u);
  }
  Cut::Dfs1(1, 0), Cut::Dfs2(1, 1); 
  for (int i = 1; i <= m; ++ i) {
    int u = read(), v = read(), z = read();
    int lca = Cut::GetLca(u, v);
    Seg::Insert(root[u], 1, kInf, z, 1);
    Seg::Insert(root[v], 1, kInf, z, 1);
    Seg::Insert(root[lca], 1, kInf, z, - 1);
    if (lca != 1) Seg::Insert(root[fa[lca]], 1, kInf, z, - 1);
  }
  Dfs(1);
  for (int i = 1; i <= n; ++ i) printf("%d\n", ans[i]);
  return 0;
}

主席树

动态开点权值线段树。

P3834 【模板】可持久化线段树 2(主席树)

静态区间最 k 大。

「静态区间第k小值」笔记

//知识点:主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 2e5 + 10;
//=============================================================
int n, m, d_num, a[kMaxn], data[kMaxn], map[kMaxn];
int root[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Prepare() {
	n = read(), m = read();
	for (int i = 1; i <= n; ++ i) {
		a[i] = data[i] = read();
	}
	std::sort(data + 1, data + n + 1);
	data[0] = -0x3f3f3f3f;
	for (int i = 1; i <= n; ++ i) {
		if (data[i] != data[i - 1]) d_num ++;
		data[d_num] = data[i];
	}
	for (int i = 1; i <= n; ++ i) {
		int ori = a[i];
		a[i] = std :: lower_bound(data + 1, data + d_num + 1, ori) - data;
		map[a[i]] = ori;
	}
}
namespace Hjt {
  #define ls lson[now_]
  #define rs rson[now_]
  #define mid ((L_+R_)>>1)
  int node_num, lson[kMaxn << 5], rson[kMaxn << 5], size[kMaxn << 5];
  void Insert(int &now_, int pre_, int L_, int R_, int val_) {
    now_ = ++ node_num;
    size[now_] = size[pre_] + 1;
    ls = lson[pre_], rs = rson[pre_];
    if (L_ == R_) return ;
    if (val_ <= mid) Insert(ls, lson[pre_], L_, mid, val_);
    else Insert(rs, rson[pre_], mid + 1, R_, val_);
  }
  int Query(int r_, int l_, int L_, int R_, int k_) {
    if (L_ == R_) return L_;
    int sz = size[lson[r_]] - size[lson[l_]];
    if (k_ <= sz) return Query(lson[r_], lson[l_], L_, mid, k_);
    else return Query(rson[r_], rson[l_], mid + 1, R_, k_ - sz);
  }
  #undef ls
  #undef rs
  #undef mid
}
//=============================================================
int main() {
  Prepare();
	for (int i = 1; i <= n; ++ i) {
		Hjt::Insert(root[i], root[i - 1], 1, d_num, a[i]);	
	}
	for (int i = 1; i <= m; ++ i) {
		int l = read(), r = read(), k = read();
		printf("%d\n", map[Hjt::Query(root[r], root[l - 1], 1, d_num, k)]);
	}
	return 0;
}

带修主席树

P2617 Dynamic Rankings

树状数组套主席树。

//知识点:主席树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kN = 1e6 + 10; 
//=============================================================
struct Opt {
  bool type;
  int l, r, k, pos, val;
} q[kN];
int n, m, d_num, data[kN], num[kN];
std::vector <int> tmp1, tmp2;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Hjt {
  #define ls lson[now_]
  #define rs rson[now_]
  #define mid ((L_+R_)>>1)
  int node_num, root[kN], sz[kN << 5], lson[kN << 5], rson[kN << 5];
  void Modify(int &now_, int L_, int R_, int pos_, int val_) {
    if (!now_) now_ = ++ node_num;
    sz[now_] += val_;
    if (L_ == R_) return ;
    if (pos_ <= mid) Modify(ls, L_, mid, pos_, val_);
    else Modify(rs, mid + 1, R_, pos_, val_);
  }
  int Query(int L_, int R_, int k_) {
    if (L_ == R_) return L_;
    int sizel = 0;
    for (int i = 0; i < tmp1.size(); ++ i) sizel -= sz[lson[tmp1[i]]];
    for (int i = 0; i < tmp2.size(); ++ i) sizel += sz[lson[tmp2[i]]];
    
    if (k_ <= sizel) {
      for (int i = 0; i < tmp1.size(); ++ i) tmp1[i] = lson[tmp1[i]];
      for (int i = 0; i < tmp2.size(); ++ i) tmp2[i] = lson[tmp2[i]];
      return Query(L_, mid, k_);
    }
    for (int i = 0; i < tmp1.size(); ++ i) tmp1[i] = rson[tmp1[i]];
    for (int i = 0; i < tmp2.size(); ++ i) tmp2[i] = rson[tmp2[i]];
    return Query(mid + 1, R_, k_ - sizel);
  }
}
namespace Bit {
  #define low(x) (x&-x)
  void Add(int pos_, int val_) {
    int p = std::lower_bound(data + 1, data + d_num + 1, num[pos_]) - data;
    for (int i = pos_; i <= n; i += low(i)) {
      Hjt::Modify(Hjt::root[i], 1, d_num, p, val_);
    }
  }
  int Query(int l_, int r_, int k_) {
    tmp1.clear(), tmp2.clear();
    for (int i = l_ - 1; i; i -= low(i)) tmp1.push_back(Hjt::root[i]);
    for (int i = r_; i; i -= low(i)) tmp2.push_back(Hjt::root[i]);
    return Hjt::Query(1, d_num, k_);
  }
}
void Init() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) data[++ d_num] = num[i] = read();
  for (int i = 1; i <= m; ++ i) {
    char opt[5]; scanf("%s", opt + 1);
    q[i].type = (opt[1] == 'Q');
    if (q[i].type) {
      q[i].l = read(), q[i].r = read(), q[i].k = read();
    } else {
      q[i].pos = read(), data[++ d_num] = q[i].val = read();
    }
  }
  std::sort(data + 1, data + d_num + 1);
  d_num = std::unique(data + 1, data + d_num + 1) - data - 1;
  for (int i = 1; i <= n; ++ i) Bit::Add(i, 1);
}
//=============================================================
int main() {
  Init();
  for (int i = 1; i <= m; ++ i) {
    if (q[i].type) {
      int l = q[i].l, r = q[i].r, k = q[i].k;
      printf("%d\n", data[Bit::Query(l, r, k)]);
    } else {
      int pos = q[i].pos, val = q[i].val;
      Bit::Add(pos, -1);
      num[pos] = val;
      Bit::Add(pos, 1);
    }
  }
  return 0;
}

线段树分治

有这样的一种模型:

  • 给定一些仅在 给定时间范围 内有效的操作。
  • 询问某个时间点所有操作的贡献。

考虑离线操作,对时间轴建立线段树,将每个操作转化为线段树上的区间标记操作。
查询时遍历整棵线段树,到达每个节点时执行相应的操作,到达叶节点统计贡献,回溯时撤销操作的影响

「离线可过」动态图连通性

「笔记」线段树分治

//知识点:线段树分治,可撤销并查集
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <map>
#include <vector>
#define pr std::pair
#define mp std::make_pair
#define LL long long
const int kMaxn = 5e4 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
struct Operation {
  int type, u, v;
} opt[kMaxm << 1];
struct Stack {
  int u, v, fa, size;
} st[kMaxm];
int n, m, k, top, fa[kMaxn], size[kMaxn];
std::map <pr <int, int>, int> tim;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
int Find(int x_) {
  while (x_ != fa[x_]) x_ = fa[x_];
  return x_;
}
void Union(int u_, int v_) {
  int fu = Find(u_), fv = Find(v_);
  if (size[fu] > size[fv]) std :: swap(fu, fv);
  st[++ top] = (Stack) {fu, fv, fa[fu], size[fu]};
  if (fu != fv) {
    fa[fu] = fv;
    size[fv] += size[fu];
    size[fu] = 0;
  }
}
void Restore() {
  Stack now = st[top];
  if (now.u != now.v) {
    fa[now.u] = now.fa;
    size[now.v] -= now.size;
    size[now.u] = now.size;
  }
  top --;
}
namespace Seg {
  #define ls (now_<<1)
  #define rs (now_<<1|1)
  #define mid ((L_+R_)>>1)
  std::vector <int> t[kMaxm << 2];
  void Modify(int now_, int L_, int R_, int l_, int r_, int val_) {
    if (l_ <= L_ && R_ <= r_) {
      t[now_].push_back(val_);
      return ;
    }
    if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
    if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
  }
  void Solve(int now_, int L_, int R_) {
    for (int i = 0, lim = t[now_].size(); i < lim; ++ i) {
      Operation nowo = opt[t[now_][i]];
      Union(nowo.u, nowo.v);
    }
    if (L_ == R_) {
      if (opt[L_].type == 2) {
        int u = opt[L_].u, v = opt[L_].v;
        printf("%c\n", Find(u) == Find(v) ? 'Y' : 'N');
      }
      for (int i = 0, lim = t[now_].size(); i < lim; ++ i) {
        Restore();
      }
      return ;
    }
    Solve(ls, L_, mid);
    Solve(rs, mid + 1, R_);
    for (int i = 0, lim = t[now_].size(); i < lim; ++ i) {
      Restore();
    }
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= m; ++ i) {
    int nowo = read(), u = read(), v = read();
    opt[i] = (Operation) {nowo, u, v};
    if (u > v) std::swap(u, v);
    if (nowo == 0) {
      tim[mp(u, v)] = i;
    } else if (nowo == 1) {
      Seg::Modify(1, 1, m, tim[mp(u, v)], i - 1, i);
      tim[mp(u, v)] = 0;
    } 
  }

  for (std::map <pr<int, int>, int>::iterator it = tim.begin(); it != tim.end(); ++ it) {
    if (it->second) {
      Seg::Modify(1, 1, m, it->second, m, it->second);
    }
  }
  for (int i = 1; i <= n; ++ i) {
    fa[i] = i;
    size[i] = 1;
  }
  Seg::Solve(1, 1, m);
  return 0;
}

轻重链剖分

将一棵树分为多条重链,将树信息转化为序列信息。
维护路径信息时用多条重链信息覆盖路径。

P3384 【模板】轻重链剖分

路径加,路径和,子树加,子树和,树上路径问题转化为 dfn 序上的序列问题,线段树维护序列信息。

我八十年没写树剖了居然 1A 了= =

//知识点:重链剖分,线段树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, m, root, p, ori[kMaxn], a[kMaxn];
int e_num, head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1];
int fa[kMaxn], son[kMaxn], dep[kMaxn], size[kMaxn], top[kMaxn];
int d_num, dfn[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
namespace Seg {
  #define ls (now_<<1)
  #define rs (now_<<1|1)
  #define mid ((L_+R_)>>1)
  LL sum[kMaxn << 2], tag[kMaxn << 2];
  void Pushup(int now_) {
    sum[now_] = (sum[ls] + sum[rs]) % p;
  }
  void Pushdown(int now_, int L_, int R_) {
    sum[ls] += 1ll * tag[now_] * (mid - L_ + 1) % p;
    sum[rs] += 1ll * tag[now_] * (R_ - mid) % p;
    tag[ls] += tag[now_] % p; 
    tag[rs] += tag[now_] % p;
    sum[ls] %= p, sum[rs] %= p;
    tag[ls] %= p, tag[rs] %= p;
    tag[now_] = 0ll;
  }
  void Build(int now_, int L_, int R_) {
    if (L_ == R_) {
      sum[now_] = a[L_] % p;
      tag[now_] = 0ll;
      return ;
    }
    Build(ls, L_, mid);
    Build(rs, mid + 1, R_);
    Pushup(now_); 
  }
  void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
    if (l_ <= L_ and R_ <= r_) {
      sum[now_] += 1ll * (R_ - L_ + 1) * val_ % p;
      tag[now_] += val_ % p;
      sum[now_] %= p, tag[now_] %= p;
      return ;
    }
    Pushdown(now_, L_, R_);
    if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
    if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
    Pushup(now_);
  }
  LL Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ and R_ <= r_) return sum[now_];
    Pushdown(now_, L_, R_);
    LL ret = 0;
    if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
    if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
    return ret % p;
  }
  #undef ls
  #undef rs
  #undef mid
}
namespace Cut {
  void Dfs1(int u_, int fa_) {
    fa[u_] = fa_;
    size[u_] = 1;
    dep[u_] = dep[fa_] + 1;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa_) continue ;
      Dfs1(v_, u_);
      if (size[v_] > size[son[u_]]) son[u_] = v_;
      size[u_] += size[v_];
    }
  }
  void Dfs2(int u_, int top_) {
    dfn[u_] = ++ d_num;
    a[d_num] = ori[u_];
    top[u_] = top_;
    if (son[u_]) Dfs2(son[u_], top_);
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa[u_] or v_ == son[u_]) continue ;
      Dfs2(v_, v_);
    }
  }
  void Modify(int u_, int v_, int val_) {
    for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
      if (dep[top[u_]] < dep[top[v_]]) {
        std::swap(u_, v_);
      }
      Seg::Modify(1, 1, n, dfn[top[u_]], dfn[u_], val_);
    }
    if (dep[u_] < dep[v_]) std::swap(u_, v_);
    Seg::Modify(1, 1, n, dfn[v_], dfn[u_], val_);
  }
  LL Query(int u_, int v_) {
    LL ret = 0;
    for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
      if (dep[top[u_]] < dep[top[v_]]) {
        std::swap(u_, v_);
      }
      ret += Seg::Query(1, 1, n, dfn[top[u_]], dfn[u_]);
      ret %= p;
    }
    if (dep[u_] < dep[v_]) std::swap(u_, v_);
    ret += Seg::Query(1, 1, n, dfn[v_], dfn[u_]);
    ret %= p;
    return ret;
  }
}
//=============================================================
int main() {
  n = read(), m = read(), root = read(), p = read();
  for (int i = 1; i <= n; ++ i) ori[i] = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_), AddEdge(v_, u_);
  }
  Cut::Dfs1(root, 0), Cut::Dfs2(root, root);
  Seg::Build(1, 1, n);
  while (m --) {
    int opt = read();
    if (opt == 1) {
      int u_ = read(), v_ = read(), val_ = read() % p;
      Cut::Modify(u_, v_, val_);
    } else if (opt == 2) {
      int u_ = read(), v_ = read();
      printf("%lld\n", Cut::Query(u_, v_));
    } else if (opt == 3) {
      int x_ = read(), val_ = read() % p;
      Seg::Modify(1, 1, n, dfn[x_], dfn[x_] + size[x_] - 1, val_);
    } else if (opt == 4) {
      int x_ = read();
      printf("%lld\n", Seg::Query(1, 1, n, dfn[x_], dfn[x_] + size[x_] - 1));
    }
  }
  return 0;
}

Splay

普通平衡树

P3369 【模板】普通平衡树

Splay 是一种二叉搜索树的维护方式。它通过不断将某节点旋转到根节点,使得整棵树仍满足二叉搜索树的性质,且保持平衡不至于退化为链,以保证复杂度。

//知识点:Splay
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Splay {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  const int kMaxNode = 1e6 + 10;
  int root, node_num, fa[kMaxNode], son[kMaxNode][2];
  int val[kMaxNode], cnt[kMaxNode], siz[kMaxNode];
  int top, bin[kMaxNode];
  void Pushup(int now_) { //更新节点大小信息
    if (!now_) return ;
    siz[now_] = cnt[now_];
    if (ls) siz[now_] += siz[ls];
    if (rs) siz[now_] += siz[rs];
  }
  int WhichSon(int now_) { //获得儿子类型
    return now_ == son[f][1];
  }
  void Clear(int now_) { //清空节点,同时进行垃圾回收
    f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
    bin[++ top] = now_;
  }
  int NewNode(int fa_, int val_) { //建立新节点
    int now_ = top ? bin[top --] : ++ node_num; //优先调用垃圾堆
    f = fa_, val[now_] = val_, siz[now_] = cnt[now_] = 1;
    return now_;
  }
  void Rotate(int now_) { //旋转操作
    int fa_ = f, whichson = WhichSon(now_);
    if (fa[f]) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    son[fa_][whichson] = son[now_][whichson ^ 1];
    fa[son[fa_][whichson]] = fa_;

    son[now_][whichson ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_);
  }
  void Splay(int now_) { //旋转到根操作
    for (; f != 0; Rotate(now_)) {
      if (fa[f]) Rotate(WhichSon(now_) == WhichSon(f) ? f : now_);
    }
    root = now_;
  }
  void Insert(int now_, int fa_, int val_) { //插入操作
    if (now_ && val[now_] != val_) {
      Insert(son[now_][val[now_] < val_], now_, val_);
      return ;
    }
    if (val[now_] == val_) ++ cnt[now_];
    if (!now_) {
      now_ = NewNode(fa_, val_);
      if (f) son[f][val[f] < val_] = now_;
    }
    Pushup(now_), Pushup(f), Splay(now_); //注意旋转到根
    return ;
  }
  int Find(int now_, int val_) { //将权值 val 对应节点旋转至根。在平衡树上二分。
    if (!now_) return false; //不存在
    if (val_ < val[now_]) return Find(ls, val_);
    if (val_ == val[now_]) {
      Splay(now_);
      return true;
    }
    return Find(rs, val_);
  }
  void Delete(int val_) { //删除一个权值 val 
    if (!Find(root, val_)) return ; //将 val 转到根
    if (cnt[root] > 1) {
      -- cnt[root];
      Pushup(root);
      return ;
    }
    int oldroot = root;
    if (!son[root][0] && !son[root][1]) {
      root = 0;
    } else if (!son[root][0]) { 
      root = son[root][1], fa[root] = 0;
    } else if (!son[root][1]) {
      root = son[root][0], fa[root] = 0;
    } else if (son[root][0] && son[root][1]) {
      //将中序遍历中 root 前的一个元素作为新的 root。该元素即为 root 左子树中最大的元素。
      int leftmax = son[root][0];
      while (son[leftmax][1]) leftmax = son[leftmax][1];
      Splay(leftmax); //转到根
      son[root][1] = son[oldroot][1], fa[son[root][1]] = root; //继承信息
    }
    Clear(oldroot), Pushup(root);
  }
  int QueryRank(int val_) { //查询 val_ 的排名
    Insert(root, 0, val_); //先插入,将其转到根,查询左子树大小
    int ret = siz[son[root][0]] + 1;
    Delete(val_);
    return ret;
  }
  int QueryVal(int rk_) { //查询排名为 rk 的权值。在平衡树上二分。
    int now_ = root;
    while (true) {
      if (!now_) return -1;
      if (ls && siz[ls] >= rk_) { //注意 =
        now_ = ls;
      }else {
        rk_ -= ls ? siz[ls] : 0;
        if (rk_ <= cnt[now_]) { //该权值即为 val[now_]
          Splay(now_);
          return val[now_];
        }
        rk_ -= cnt[now_];
        now_ = rs;
      }
    }
  }
  int QueryPre(int val_) { //查询前驱
    Insert(root, 0, val_); //插入 val_,将其旋转到根,前驱(中序遍历中前一个严肃)即为左子树中最大的元素。
    int now_ = son[root][0];
    while (rs) now_ = rs;
    Delete(val_);
    return val[now_];
  }
  int QueryNext(int val_) { //查询后继
    Insert(root, 0, val_);
    int now_ = son[root][1];
    while (ls) now_ = ls;
    Delete(val_);
    return val[now_];
  }
}
//=============================================================
int main() { 
  int n = read();
  while (n --) {
    int opt = read(), x = read();
    if (opt == 1) {
      Splay::Insert(Splay::root, 0, x);
    } else if (opt == 2) {
      Splay::Delete(x);
    } else if (opt == 3) {
      printf("%d\n", Splay::QueryRank(x));
    } else if (opt == 4) {
      printf("%d\n", Splay::QueryVal(x));
    } else if (opt == 5) {
      printf("%d\n", Splay::QueryPre(x));
    } else if (opt == 6) {
      printf("%d\n", Splay::QueryNext(x));
    }
  }
  return 0; 
}

文艺平衡树

P3391 【模板】文艺平衡树

将被操作区间放置到一棵单独的子树内,对整棵子树打标记实现区间修改。注意标记下放的写法,反转标记取反并交换左右儿子。

//知识点:Splay
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Splay {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  const int kMaxNode = kN;
  int root, node_num, fa[kMaxNode], son[kMaxNode][2];
  int val[kMaxNode], siz[kMaxNode], cnt[kMaxNode];
  bool tag[kN];
  int top, bin[kMaxNode];
  void Pushup(int now_) {
    if (!now_) return ;
    siz[now_] = cnt[now_];
    if (ls) siz[now_] += siz[ls];
    if (rs) siz[now_] += siz[rs];
  }
  void Pushdown(int now_) {
    if (!now_ || !tag[now_]) return ;
    tag[ls] ^= 1, tag[rs] ^= 1;
    std::swap(ls, rs);
    tag[now_] = false;
  }
  void Clear(int now_) {
    f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
    bin[++ top] = now_;
  }
  int NewNode(int fa_, int val_) {
    int now_ = top ? bin[top --] : ++ node_num;
    f = fa_, val[now_] = val_, cnt[now_] = siz[now_] = 1;
    return now_;
  }
  bool WhichSon(int now_) {
    return now_ == son[f][1];
  }
  void Rotate(int now_) {
    int fa_ = f, whichson = WhichSon(now_);
    if (fa[f]) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    son[fa_][whichson] = son[now_][whichson ^ 1];
    fa[son[fa_][whichson]] = fa_;

    son[now_][whichson ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_);
  }
  void Splay(int now_, int fa_) {
    for (; f != fa_; Rotate(now_)) {
      if (fa[f] != fa_) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_); 
    }
    if (!fa_) root = now_;
  }
  void Build(int &now_, int fa_, int L_, int R_) {
    if (L_ > R_) return;
    int mid = (L_ + R_) >> 1; 
    now_ = NewNode(fa_, mid);
    Build(ls, now_, L_, mid - 1);
    Build(rs, now_, mid + 1, R_);
    Pushup(now_);
  }
  int Kth(int now_, int rk_) {
    Pushdown(now_);
    if (ls && rk_ <= siz[ls]) return Kth(ls, rk_);
    if (rk_ <= siz[ls] + cnt[now_]) return now_;
    return Kth(rs, rk_ - siz[ls] - cnt[now_]);
  }
  void Modify(int L_, int R_) {
    int x = Kth(root, L_ - 1 + 1), y = Kth(root, R_ + 1 + 1); //偏移量
    Splay(x, 0), Splay(y, x);
    tag[son[son[root][1]][0]] ^= 1;
  }
  void Print(int now_) {
    Pushdown(now_);
    if (ls) Print(ls);
    if (val[now_] && val[now_] <= n) printf("%d ", val[now_]);
    if (rs) Print(rs);
  }
}
//=============================================================
int main() { 
  n = read(), m = read();
  Splay::Build(Splay::root, 0, 0, n + 1);
  while (m --) {
    int l = read(), r = read();
    Splay::Modify(l ,r);
  }
  Splay::Print(Splay::root);
  return 0; 
}

用于解决动态树问题,通过 Splay 动态维护树链剖分结构,从而将树上问题转化为 Splay 上的序列问题的一种数据结构。

支持动态加删除边,修改路径信息,查询路径、子树信息。不支持修改子树。

P1501 Tree II

//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
const LL mod = 51061;
//=============================================================
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace LCT {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  const int kMaxNode = kN;
  int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode]; //Splay 的结构信息
  LL siz[kMaxNode], sum[kMaxNode], tagplus[kMaxNode], tagprod[kMaxNode]; //子树大小,子树和,两种标记
  bool tagrev[kMaxNode]; //Splay 的子树反转标记
  void Pushup(int now_) { //维护子树和 和 子树大小
    sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
    siz[now_] = siz[ls] + siz[rs] + 1;
  }
  void PushReverse(int now_) { //子树反转标记,使得 Splay 节点的中序遍历反向。若原 splay 表示一条自顶向下的链,反转相当于将 splay 表示的链的边反向,父子关系互换。
    if (!now_) return;
    std::swap(ls, rs);
    tagrev[now_] ^= 1;
  }
  void PushPlus(int now_, LL val_) {
    if (!now_) return;
    val[now_] = (val[now_] + val_) % mod;
    sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
    tagplus[now_] = (tagplus[now_] + val_) % mod;
  }
  void PushProd(int now_, LL val_) {
    if (!now_) return;
    val[now_] = val[now_] * val_ % mod;
    sum[now_] = sum[now_] * val_ % mod;
    tagplus[now_] = tagplus[now_] * val_ % mod;
    tagprod[now_] = tagprod[now_] * val_ % mod;
  }
  void Pushdown(int now_) { //注意下放顺序
    LL plus = tagplus[now_], prod = tagprod[now_], rev = tagrev[now_];
    if (prod != 1) PushProd(ls, prod), PushProd(rs, prod);
    if (plus) PushPlus(ls, plus), PushPlus(rs, plus);
    if (rev) PushReverse(ls), PushReverse(rs);
    tagprod[now_] = 1, tagplus[now_] = 0, tagrev[now_] = 0;
  }
  bool IsRoot(int now_) { //判断 now_ 是否为当前 Splay 的根
    return son[f][0] != now_ && son[f][1] != now_;
  }
  bool WhichSon(int now_) {
    return son[f][1] == now_;
  }
  void Rotate(int now_) {
    int fa_ = f, w = WhichSon(now_);
    if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    son[fa_][w] = son[now_][w ^ 1];
    fa[son[fa_][w]] = fa_;

    son[now_][w ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_); 
  }
  void Update(int now_) { //将 Splay 路径上的所有标记下放
    if (!IsRoot(now_)) Update(f);
    Pushdown(now_);
  }
  void Splay(int now_) {
    Update(now_);
    for (; !IsRoot(now_); Rotate(now_)) {
      if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
    }
  }
  void Access(int now_) { //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,满足 Splay 的中序遍历深度递减的性质。
    //自下向上构建,舍弃父亲的原有右儿子,换成 -> last 的链。
    for (int last_ = 0; now_; last_ = now_, now_ = f) {
      Splay(now_), rs = last_;
      Pushup(now_);
    }
  }
  void MakeRoot(int now_) { //使 now 成为原树的根
    Access(now_); //先使得树中由根->now 的链成为实链,构造出由它们组成的 Splay。
    Splay(now_); //使 now 成为 splay 的根节点
    PushReverse(now_); //将根->now 的链反转,使 now 成为原树的根。原理参考 PushReverse 函数的注释。
  }
  int Find(int now_) { //找到 now_ 所在原树的根
    Access(now_); 
    Splay(now_);
    while (ls) Pushdown(now_), now_ = ls; //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,再找到 Splay 中序遍历的第一个元素,即为原树的根。
    
    Splay(now_); //为了下一步操作,把根再转回去
    return now_; 
  }
  void Split(int x_, int y_) { //构造由路径 x->y 组成的 Splay
    MakeRoot(x_); //使 x 成为根,构造出根 -> y 的 Splay 即得,Splay 根的子树信息即为路径信息。
    Access(y_);
    Splay(y_);
  }
  void Link(int x_, int y_) { //加边 (x,y)
    MakeRoot(x_); //使 x 成为根,再给根一个父亲
    if (Find(y_) != x_) fa[x_] = y_;
  }
  void Cut(int x_, int y_) { //删边 (x,y)
    MakeRoot(x_); //使 x 成为根
    //Find(y_) != x_ 保证 y 与 x 连通
    //在 Find 函数中 Access(y) 之后,y 所在的 splay 由 x->y 的链构成。又 x 是 splay 的根,y 的中序遍历在 x 后,则若边 (x,y) 存在,则 y 的位置只有一种可能:  
    //y 是 x 的右儿子,且 y 没有左儿子。从而保证 y 在中序遍历中是 x 的后一个。
    if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
    fa[y_] = son[x_][1] = 0; //断绝父子关系
    Pushup(x_);
  }
  void Modify(int x_, int y_, int val_, int type) { //路径修改
    Split(x_, y_); //给路径构成的 Splay 打标记
    if (!type) PushPlus(y_, val_);
    else PushProd(y_, val_);
  }
  LL Query(int x_, int y_) { //路径查询
    Split(x_, y_); //返回路径构成的 Splay 的信息
    return sum[y_];
  }
}
//=============================================================
signed main() { 
  int n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    LCT::val[i] = LCT::tagprod[i] = LCT::siz[i] = 1;
  }
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    LCT::Link(u_, v_);
  }
  for(int i = 1; i <= m; i ++) {
	  char opt[2]; scanf("%s", opt);
	  int u1 = read(), v1 = read();
	  if(opt[0] == '+') LCT::Modify(u1, v1, read(), 0);
	  if(opt[0] == '*') LCT::Modify(u1, v1, read(), 1);
	  if(opt[0] == '/') printf("%lld\n", LCT::Query(u1, v1));
    if(opt[0] == '-') {
      int u2 = read(), v2 = read(); 
      LCT::Cut(u1, v1), LCT::Link(u2, v2);
    }
	}
  return 0; 

图论

多源最短路

Floyd

//知识点:多源最短路
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e3 + 10;
//=============================================================
int n, m, s, f[kMaxn][kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Floyd() {
  for (int i = 1; i <= n; ++ i) f[i][i] = 0;
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      for (int k = 1; k <= n; ++ k) {
        Chkmin(f[j][k], f[j][i] + f[i][k]);
      }
    }
  }
}
//=============================================================
int main() {
  n = read(), m = read(), s = read();
  memset(f, 63, sizeof (f));
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read(), w_ = read();
    Chkmin(f[u_][v_], w_);
  }
  Floyd();
  for (int i = 1; i <= n; ++ i) {
    printf("%d ", f[s][i]);
//    printf("%d ", f[s][i] >= 0x3f3f3f3f ? 2147483647 : f[s][i]);
  }
  return 0;
}

单源最短路三合一

Dijkstra,Bellman-Ford,Spfa 三合一代码。

P3371 【模板】单源最短路径(弱化版)

//知识点:单源最短路 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kMaxn = 1e5 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
int n, m, s;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
  v[++ e_num] = v_;
  w[e_num] = w_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Dijkstra(int s_) {
  std::priority_queue <pr <int, int> > q;
  memset(dis, 63, sizeof (dis));
  memset(vis, 0, sizeof (vis));
  dis[s_] = 0;
  q.push(mp(0, s_));
  
  while (! q.empty()) {
    int u_ = q.top().second;
    q.pop();
    if (vis[u_]) continue ;
    vis[u_] = true;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        q.push(mp(-dis[v_], v_));
      }
    }
  }
}
void BellmanFord(int s_) {
  memset(dis, 63, sizeof (dis));
  dis[s_] = 0;
  while (1) {
    bool flag = true;
    for (int u_ = 1; u_ <= n; ++ u_) {
      for (int i = head[u_]; i; i = ne[i]) {
        int v_ = v[i], w_ = w[i];
        if (dis[u_] + w_ < dis[v_]) {
          dis[v_] = dis[u_] + w_;
          flag = false;
        } 
      }
    }
    if (flag) break ;
  }
}
void Spfa(int s_) {
  std::queue <int> q;
  memset(vis, 0, sizeof (vis));
  memset(dis, 63, sizeof (dis));
  q.push(s_);
  dis[s_] = 0;
  vis[s_] = true;
  
  while (! q.empty()) {
    int u_ = q.front();
    q.pop();
    vis[u_] = false;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        if (! vis[v_]) {
          q.push(v_);
          vis[v_] = true;
        }
      }
    }
  }
}
//=============================================================
int main() {
  n = read(), m = read(), s = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read(), w_ = read();
    AddEdge(u_, v_, w_);
  }
  Dijkstra(s);
  // BellmanFord(s);
  // Spfa(s);
  for (int i = 1; i <= n; ++ i) {
    printf("%d ", dis[i]);
//    printf("%d ", dis[i] >= 0x3f3f3f3f ? 2147483647 : dis[i]);
  }
  return 0;
}

最小生成树

P3366 【模板】最小生成树

Kruscal

克鲁斯卡尔大战克苏鲁。

基于排序与并查集的贪心加边。

//知识点:最小生成树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kMaxn = 5e4 + 10;
const int kMaxm = 4e5 + 10;
//=============================================================
int n, m, ans, fa[kMaxn], size[kMaxn];
struct Edge {
  int u, v, w;
} e[kMaxm];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool CompareEdge(Edge f_, Edge s_) {
  return f_.w < s_.w;
}
int Find(int x_) {
  return x_ == fa[x_] ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int u_, int v_) {
  int fu = Find(u_), fv = Find(v_);
  if (fu == fv) return ;
  if (size[fu] > size[fv]) std::swap(fu, fv);
  fa[fu] = fv;
  size[fv] += size[fu];
}
void Kruscal() {
  int tot = 0;
  std::sort(e + 1, e + m + 1, CompareEdge);
  for (int i = 1; i <= n; ++ i) {
    fa[i] = i;
    size[i] = 1; 
  }
  for (int i = 1; i <= m && tot < n - 1; ++ i) {
    int u_ = e[i].u, v_ = e[i].v, w_ = e[i].w;
    if (Find(u_) == Find(v_)) continue ;
    Union(u_, v_);
    ans += w_;
    ++ tot;
  }
  if (tot < n - 1) ans = -1;
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read(), w_ = read();
    e[i] = (Edge) {u_, v_, w_};
  }
  Kruscal();
  if (ans < 0) {
    printf("orz\n");
  } else {
    printf("%d\n", ans);
  }
  return 0;
}

Prim

类似 Dijkstra,贪心地找将每一个点加入连通块的边的最小值。

用了堆优化。

//知识点:单元最短路 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kMaxn = 5e4 + 10;
const int kMaxm = 4e5 + 10;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
  v[++ e_num] = v_;
  w[e_num] = w_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Prim() {
  std::priority_queue <pr <int, int> > q;
  memset(dis, 63, sizeof (dis));
  memset(vis, 0, sizeof (vis));
  dis[1] = 0;
  q.push(mp(0, 1));
  
  int tot = 0;
  while (! q.empty() && tot < n) {
    int u_ = q.top().second;
    q.pop();
    if (vis[u_]) continue ;
    ++ tot;
    vis[u_] = true;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (w_ < dis[v_] && !vis[v_]) {
        dis[v_] = w_;
        q.push(mp(-dis[v_], v_));
      }
    }
  }
  if (tot < n) ans = -1;
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read(), w_ = read();
    AddEdge(u_, v_, w_);
    AddEdge(v_, u_, w_);
  }
  Prim();
  if (ans < 0) {
    printf("orz\n");
  } else {
    for (int i = 1; i <= n; ++ i) ans += dis[i];
    printf("%d\n", ans);
  }
  
  return 0;
}

最近公共祖先

P3379 【模板】最近公共祖先(LCA)

倍增

优化暴力上跳,用2 的幂的和拼出跳的深度。

//知识点:最近公共祖先 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 5e5 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, root;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int dep[kMaxn], fa[kMaxn][21];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Dfs(int u_, int fa_) {
  fa[u_][0] = fa_;
  dep[u_] = dep[fa_] + 1;
  for (int i = 1; i <= 20; ++ i) {
    fa[u_][i] = fa[fa[u_][i - 1]][i - 1];
  }
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue ;
    Dfs(v_, u_);
  }
}
int Lca(int u_, int v_) {
  if (dep[u_] < dep[v_]) std::swap(u_, v_);
  for (int i = 20; i >= 0; -- i) {
    if (dep[fa[u_][i]] >= dep[v_] ) {
      u_ = fa[u_][i];
    }
  }
  if (u_ == v_) return u_;
  for (int i = 20; i >= 0; -- i) {
    if (fa[u_][i] != fa[v_][i]) {
      u_ = fa[u_][i];
      v_ = fa[v_][i];
    }
  }
  return fa[u_][0];
}
//=============================================================
int main() {
  n = read(), m = read(), root = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_);
    AddEdge(v_, u_);
  }
  Dfs(root, 0);
  while (m --) {
    int u_ = read(), v_ = read();
    printf("%d\n", Lca(u_, v_));
  }
  return 0;
}

重链剖分

暴力上跳转化为跳重链。

//知识点:重链剖分
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 5e5 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, root;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int fa[kMaxn], son[kMaxn], dep[kMaxn], size[kMaxn], top[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
namespace Cut {
  void Dfs1(int u_, int fa_) {
    fa[u_] = fa_;
    size[u_] = 1;
    dep[u_] = dep[fa_] + 1;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa_) continue ;
      Dfs1(v_, u_);
      if (size[v_] > size[son[u_]]) son[u_] = v_;
      size[u_] += size[v_];
    }
  }
  void Dfs2(int u_, int top_) {
    top[u_] = top_;
    if (son[u_]) Dfs2(son[u_], top_);
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa[u_] or v_ == son[u_]) continue ;
      Dfs2(v_, v_);
    }
  }
  int Lca(int u_, int v_) {
    for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
      if (dep[top[u_]] < dep[top[v_]]) {
        std::swap(u_, v_);
      }
    }
    return dep[u_] < dep[v_] ? u_ : v_;
  }
}
//=============================================================
int main() {
  n = read(), m = read(), root = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_), AddEdge(v_, u_);
  }
  Cut::Dfs1(root, 0), Cut::Dfs2(root, root);
  while (m --) {
    int u_ = read(), v_ = read();
    printf("%d\n", Cut::Lca(u_, v_));
  }
  return 0;
}

RMQ

dfs 维护一个序列。
每当一个点被访问到,就将它加入到序列的尾部。
维护每个节点的深度,在序列中第一次出现的位置。

根据 dfs 的性质,对于节点 x、y,它们第一次出现位置这一段区间内深度最浅的点即为其 LCA。

预处理时间空间复杂度实际上是 \(𝑂(2𝑛 \log⁡(2𝑛))\) 的,单次查询复杂度 \(O(1)\)
使用局限性比较大,在某些题目中会有作用。

//知识点:LCA
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e5 + 10;
const int kM = kN << 1;
//=============================================================
int n, m, root;
int e_num, head[kN], v[kM], ne[kM], dep[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
namespace ST {
  int num, Log2[kN << 1], f[kN << 1][22], fir[kN];
  void Dfs(int u_, int fa_) {
    dep[u_] = dep[fa_] + 1;
    fir[u_] = ++ num;
    f[num][0] = u_;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa_) continue ;
      Dfs(v_, u_);
      f[++ num][0] = u_;
    }
  }
  void Prepare() {
    Dfs(root, root);
    Log2[1] = 0;
    for (int i = 2; i <= num; ++ i) {
      Log2[i] = Log2[i >> 1] + 1; 
    }
    for (int i = 1; i <= 21; ++ i) {
      for (int j = 1; j + (1 << i) - 1 <= num; ++ j) {
        if (dep[f[j][i - 1]] < dep[f[j + (1 << (i - 1))][i - 1]]) {
          f[j][i] = f[j][i - 1];
        } else {
          f[j][i] = f[j + (1 << (i - 1))][i - 1];
        }
      }
    }
  }
  int Lca(int u_, int v_) {
    int l = fir[u_], r = fir[v_];
    if (l > r) std::swap(l, r);
    int lth = Log2[r - l + 1];
    if (dep[f[l][lth]] < dep[f[r - (1 << lth) + 1][lth]]) {
      return f[l][lth];
    }
    return f[r - (1 << lth) + 1][lth];
  }
}
//=============================================================
int main() {
  n = read(), m = read(), root = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_), AddEdge(v_, u_);
  }
  ST::Prepare();
  for (int i = 1; i <= n; ++ i) {
    int u_ = read(), v_ = read();
    printf("%d\n", ST::Lca(u_, v_));
  }
  return 0;
}

树的直径

图中所有最短路径的最大值即为「直径」,可以用两次 DFS 或者树形 DP 的方法在 O(n) 时间求出树的直径。

之前写的垃圾文章:「笔记」树的直径

树形 DP

对每个节点维护最长链和次长链,相加取最大值。

//知识点:树的直径 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
int Dfs(int u_, int fa_) {
  int f1 = 0, f2 = 0;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue ;
    int f = Dfs(v_, u_) + 1;
    if (f > f1) {
      f2 = f1;
      f1 = f;
    } else if (f > f2) {
      f2 = f;
    }
  }
  Chkmax(ans, f1 + f2);
  return f1;
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_);
    AddEdge(v_, u_);
  }
  Dfs(1, 0);
  printf("%d\n", ans);
}

2 次DFS

距离一个点最远的点一定是直径的一个端点。
一次 DfS 找到一个端点,第二次找到另一个。

由于 dis 初值是 0,所以需要 -1。

//知识点:树的直径 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int dis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
int Dfs(int u_, int fa_) {
  dis[u_] = dis[fa_] + 1;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue ;
    Dfs(v_, u_);
  }
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_);
    AddEdge(v_, u_);
  }
  Dfs(1, 0);
  int max_dis = 0, u_ = 0;
  for (int i = 1; i <= n; ++ i) {
    if (dis[i] > max_dis) {
      max_dis = dis[i];
      u_ = i;
    }
    dis[i] = 0;
  }
  Dfs(u_, 0);
  max_dis = 0, u_ = 0;
  for (int i = 1; i <= n; ++ i) {
    if (dis[i] > max_dis) {
      max_dis = dis[i];
      u_ = i;
    }
    dis[i] = 0;
  }
  printf("%d\n", max_dis - 1);
}

判断负权环

P3385 【模板】负环

Spfa,判断到达某点最短路上的点数是否 \(>n\)
若为真则有负环。

//知识点:负环 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kMaxn = 1e5 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
int n, m;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn], cnt[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
  v[++ e_num] = v_;
  w[e_num] = w_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Init() {
  e_num = 0;
  memset(head, 0, sizeof (head));
}
bool SpfaBfs(int s_) {
  std::queue <int> q;
  memset(cnt, 0, sizeof (cnt));
  memset(vis, 0, sizeof (vis));
  memset(dis, 63, sizeof (dis));
  q.push(s_);
  dis[s_] = 0;
  vis[s_] = true;
  
  while (! q.empty()) {
    int u_ = q.front();
    q.pop();
    vis[u_] = false;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        cnt[v_] = cnt[u_] + 1;
        if (cnt[v_] > n) return true;
        if (! vis[v_]) {
          q.push(v_);
          vis[v_] = true;
        }
      }
    }
  }
  return false;
}
//=============================================================
int main() {
  int T = read();
  while (T --) {
    Init();
    n = read(), m = read(); 
    for (int i = 1; i <= m; ++ i) {
      int u_ = read(), v_ = read(), w_ = read();
      AddEdge(u_, v_, w_);
      if (w_ >= 0) AddEdge(v_, u_, w_);
    }
    printf("%s\n", SpfaBfs(1) ? "YES" : "NO");
  }
  return 0;
}

差分约束

P5960 【模板】差分约束算法

\(x_i -x_j\le k\) 的一堆不等式。
转化为单源最短路中三角不等式 \(dis_i\le dis_j + k\) 的形式,从 \(j\)\(i\) 连一条权值为 \(k\) 的单向边。
建立超级源点,与各点距离为 0,跑最短路。

若有负环则无解,否则最短路即为一组合法解。
合法解同加 \(\Delta\) 仍为合法解。

upd on 2023.2.7:
注意到不等式有传递性,建立图论模型,\(x_i -x_j\le k\) 则从 \(j\)\(i\) 连一条权值为 \(k\) 的单向边,若存在环则环上的点 \(u\) 满足 \(x_u-x_u \le \sum{k}\),则图上有负环无解,否则有无数组解。考虑构造一组解:建立超级源点向每个点连单向边,权值为 0,无负环则从超级源点到其他点均有最短路,其中某些点最短路为 0。根据每条边有 \(\operatorname{dis}_i \le \operatorname{dis}_j + k\),则每个点的最短路的值均满足约束条件,\(\operatorname{dis}_u=0\) 等价于钦定 \(x_u = 0\),这样可以得到满足所有 \(x_i\le 0\)\(x\) 的最大解。
类似地,将 \(x_i-x_j\le k\) 变形为 \(x_j\ge x_i - k\),从 \(i\)\(j\) 连一条边权为 \(-k\) 的边,加入超级源点后求最长路可得所有 \(x_i\ge 0\)\(x\) 的最小解。

//知识点:负环 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kMaxn = 1e5 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
int n, m;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn], cnt[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
  v[++ e_num] = v_;
  w[e_num] = w_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Init() {
  e_num = 0;
  memset(head, 0, sizeof (head));
}
bool SpfaBfs(int s_) {
  std::queue <int> q;
  memset(cnt, 0, sizeof (cnt));
  memset(vis, 0, sizeof (vis));
  memset(dis, 63, sizeof (dis));
  q.push(s_);
  dis[s_] = 0;
  vis[s_] = true;
  
  while (! q.empty()) {
    int u_ = q.front();
    q.pop();
    vis[u_] = false;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        cnt[v_] = cnt[u_] + 1;
        if (cnt[v_] > n) return true;
        if (! vis[v_]) {
          q.push(v_);
          vis[v_] = true;
        }
      }
    }
  }
  return false;
}
//=============================================================
int main() {
  n = read(), m = read(); 
  for (int i = 1; i <= m; ++ i) {
    int v_ = read(), u_ = read(), w_ = read();
    AddEdge(u_, v_, w_);
  }
  for (int i = 1; i <= n; ++ i) {
    AddEdge(0, i, 0);
  }
  if (SpfaBfs(0)) {
    printf("NO\n");
    return 0;
  }
  for (int i = 1; i <= n; ++ i) {
    printf("%d ", dis[i]);    
  }
  return 0;
}

强连通相关

缩点

P3387 【模板】缩点

缩点 + Topsort DP。

//知识点:缩点
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = 2e5 + 10;
//=============================================================
int n, m, ans, ori[kMaxn];
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int d_num, b_num, dfn[kMaxn], low[kMaxn], bel[kMaxn], val[kMaxn];
int into[kMaxn], f[kMaxn];
std::vector <int> newv[kMaxn];
std::stack <int> st;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Tarjan(int u_) {
  dfn[u_] = low[u_] = ++ d_num;
  st.push(u_);
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (! dfn[v_]) {
      Tarjan(v_);
      Chkmin(low[u_], low[v_]);
    } else if (! bel[v_]) {
      Chkmin(low[u_], dfn[v_]);
    }
  }
  if (dfn[u_] == low[u_]) { //
    ++ b_num;
    for (int now = 0; now != u_; st.pop()) {
      now = st.top();
      bel[now] = b_num;
      val[b_num] += ori[now];
    }
  }
}
void Topsort() {
  std::queue <int> q;
  for (int i = 1; i <= b_num; ++ i) {
    if (! into[i]) {
      f[i] = val[i];
      q.push(i);
    }
  }
  while (! q.empty()) {
    int u_ = q.front();
    q.pop();
    for (int i = 0, lim = newv[u_].size(); i < lim; ++ i) {
      int v_ = newv[u_][i];
      Chkmax(f[v_], f[u_] + val[v_]);
      into[v_] --;
      if (! into[v_]) {
        q.push(v_);
      }
    }
  }
  for (int i = 1; i <= b_num; ++ i) {
    Chkmax(ans, f[i]);
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) ori[i] = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read();
    if (u_ == v_) continue ;
    AddEdge(u_, v_);
  }
  for (int i = 1; i <= n; ++ i) {
    if (! dfn[i]) Tarjan(i);
  }
  for (int u_ = 1; u_ <= n; ++ u_) {
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (bel[u_] == bel[v_]) continue ;
      newv[bel[u_]].push_back(bel[v_]);
      into[bel[v_]] ++;
    }
  }
  Topsort();
  printf("%d\n", ans);
  return 0;
}

割点

P3388 【模板】割点(割顶)

以下是几个月前写的垃圾文字:

应用Tarjan求割点。

按照dfs的遍历顺序,将图看成一棵树,图便有了层次性,将dfs回溯的边当做回边。
dfn[] 记录每一个点的dfs序,low[] 记录能连通的dfs序最小的点的编号。
初始时 dfn[i] = low[i],在不断dfs递归过程中进行更新。

对于搜索树的根节点,若它有两个 或 两个以上的子树,去掉此点后, 其各子树便不连通,说明它是一个割点。

对于其他的节点,更新完成后, 若有一条边, 满足 low[v] >= dfn[u]
说明 v 点, 一定到达不了 u 的祖先 (dfs 树意义下的祖先),则删去此点后,u 与 v 不连通,u 是一个割点。

//知识点:割点 
/*
By:Luckyblock

*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kMaxn = 2e4 + 10;
const int kMaxm = 2e5 + 10;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int d_num, dfn[kMaxn], low[kMaxn];
bool cut[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Tarjan(int u_, int root_) {
  int son = 0;
  dfn[u_] = low[u_] = ++ d_num;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (! dfn[v_]) {
      Tarjan(v_, root_);
      Chkmin(low[u_], low[v_]);
      if (u_ == root_) ++ son;
      if (low[v_] >= dfn[u_] && u_ != root_) {
        ans += (! cut[u_]);
        cut[u_] = true;
      }
    } else {
      Chkmin(low[u_], dfn[v_]);
    }
  }
  if (u_ == root_ && son >= 2) {
    ans += (!cut[u_]);
    cut[u_] = true;
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read();
    AddEdge(u_, v_);
    AddEdge(v_, u_);
  }
  for (int i = 1; i <= n; ++ i) {
    if (! dfn[i]) {
      Tarjan(i, i); 
    }
  }
  printf("%d\n", ans);
  for (int i = 1; i <= n; ++ i) {
    if (cut[i]) {
      printf("%d ", i);
    }
  }
  return 0;
}

2-SAT

\(n\) 个 01 变量 \(x_1\sim x_n\),另有 \(m\) 个变量取值需要满足的限制。
每个限制是一个 \(k\) 元组 \((x_{p_1}, x_{p_2}, \cdots, x_{p_3})\),满足 \(x_{p_1}\oplus x_{p_2}\oplus\cdots\oplus x_{p_3} = a\)。其中 \(a\)\(0/1\)\(\oplus\) 是某种二元 bool 运算。
要求构造一种满足所有限制的变量的赋值方案。
建立图论模型,使用强连通算法实现。

P4782 【模板】-SAT 问题

//知识点:2-SAT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 2e6 + 10;
//=============================================================
int n, m, e_num, head[kN], v[kN << 1], ne[kN << 1];
int dfn_num, bel_num, dfn[kN], low[kN], bel[kN];
std::stack <int> st;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
void Add(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Tarjan(int u_) {
  dfn[u_] = low[u_] = ++ dfn_num;
  st.push(u_);
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (!dfn[v_]) {
      Tarjan(v_);
      Chkmin(low[u_], low[v_]);
    } else if (!bel[v_]) {
      Chkmin(low[u_], dfn[v_]);
    }
  }
  if (dfn[u_] == low[u_]) {
    ++ bel_num;
    while (true) {
      int top = st.top(); st.pop();
      bel[top] = bel_num;
      if (top == u_) break;
    } 
  }
}
//=============================================================
int main() { 
  n = read(), m = read();
  for (int i = 1; i <= m; ++ i) {
    int x = read(), a = read(), y = read(), b = read();
    if (a && b) Add(x + n, y), Add(y + n, x);
    if (!a && b) Add(x, y), Add(y + n, x + n);
    if (a && !b) Add(x + n, y + n), Add(y, x);
    if (!a && !b) Add(x, y + n), Add(y, x + n);
  }
  for (int i = 1; i <= 2 * n; ++ i) {
    if (!dfn[i]) Tarjan(i);
    if (i <= n && bel[i] == bel[i + n]) {
      printf("IMPOSSIBLE\n");
      return 0;
    }
  }
  printf("POSSIBLE\n");
  for (int i = 1; i <= n; ++ i) printf("%d ", bel[i] < bel[i + n]); //Tarjan 算法赋给强连通分量的编号顺序与拓扑序相反
  return 0; 
}

虚树

对于树 \(T=(V,E)\),给定关键点集 \(S\subseteq V\),则可定义虚树 \(T'=(V',E')\)
对于点集 \(V'\subseteq V\),使得 \(u\in V'\) 当且仅当 \(u\in S\),或 \(\exist x,y\in S,\operatorname{lca}(x,y)=u\)
对于边集,\((u,v)\in E'\),当且仅当 \(u,v\in V'\),且 \(u\)\(v\)\(V'\) 中深度最深的祖先。

个人理解:
仅保留关键点及其 \(\operatorname{lca}\),缩子树成边,仅保留分叉点,可能删去一些不包含关键点的子树。
压缩了树的信息,同时也丢失了部分树的信息。
一个分叉点会合并至少两个关键点,虚树节点数最多为 \(2k-1\) 个。节点数变为了 \(O(k)\) 级别。

建树考虑增量法,每次向虚树中添加一个关键点。考虑先求得 关键节点 的 dfs 序,按照 dfs 序添加关键节点。这样可以保证相邻两个关键点的 \(\operatorname{lca}\) 深度不小于不相邻关键点的深度。

考虑单调栈维护虚树最右侧的链(上一个关键点与根的链),单调栈中节点深度递增,栈顶一定为上一个关键点。钦定 1 号节点为根,先将其压入栈中。
每加入一个关键点 \(a_i\),令 \(\operatorname{lca}(a_{i-1},a_i)=w\)。将栈顶 \(\operatorname{dep}_x > \operatorname{dep}_w\) 的弹栈,加入 \(w,a_i\),即为新的右链。特别地,若栈顶存在 \(\operatorname{dep}_x=\operatorname{dep}_w\),不加入 \(w\) 节点。
在此过程中维护每个节点的父节点,在弹栈时进行连边并维护信息,即得虚树。单次建虚树复杂度 \(O(kw)\) 级别,其中 \(w\) 为单次求 \(\operatorname{lca}\) 的复杂度。

其中 \(\operatorname{Cut}\) 为封装后的树链剖分。

虚树上 DP 板题:「SDOI2011」消耗战

namespace VT { //Virtual Tree
  #define dep Cut::dep
  const int kMaxNode = kN;
  int top, node[kMaxNode], st[kMaxNode]; //栈
  int tag[kMaxNode]; //标记是否为关键点
  std::vector <int> newv[kMaxNode]; //虚树
  bool CMP(int fir_, int sec_) { //按 dfs 序比较
    return Cut::dfn[fir_] < Cut::dfn[sec_];
  }
  void Push(int u_) { //向虚树中加入 u_
    int lca = Cut::Lca(u_, st[top]);
     //为什么这里要写成 top - 1?因为 lca 可能出现在栈中,这样写便于处理。
    for (; dep[st[top - 1]] > dep[lca]; -- top) {
      newv[st[top - 1]].push_back(st[top]);
    }
    if (lca != st[top]) {
      newv[lca].push_back(st[top]); -- top;
      if (lca != st[top]) st[++ top] = lca;
    }
    if (st[top] != u_) st[++ top] = u_;
  }
  void Build(int siz_) {
    for (int i = 1; i <= siz_; ++ i) {
      node[i] = read();
      tag[node[i]] = 1;
    }
    std::sort(node + 1, node + siz_ + 1, CMP);
    st[top = 0] = 1;
    for (int i = 1; i <= siz_; ++ i) Push(node[i]);
    for (; top; -- top) newv[st[top - 1]].push_back(st[top]);
  }
}

网络流

网络最大流

P3376 【模板】网络最大流

EK

从源点开始 bfs 实现 FF,到达汇点后停止 bfs 并增广一次。增广时记录转移前驱并更新路径上边的残余容量,答案加上最小流量。

//知识点:网络最大流,EK
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 210 + 10;
const int kM = 1e4 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, m, s, t;
int e_num = 1, head[kN], v[kM], ne[kM], from[kN];
LL f[kN], w[kM];
bool vis[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
  v[++ e_num] = v_;
  w[e_num] = 1ll * w_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
bool Bfs() { //找到一条增广路
  memset(vis, false, sizeof (vis));
  std::queue <int> q; 
  vis[s] = false;
  f[s] = kInf;
  q.push(s);
  while (!q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i]; 
      LL w_ = w[i];
      if (!w_ || vis[v_]) continue;
      f[v_] = std::min(f[u_], w_);
      vis[v_] = true; 
      from[v_] = i;
      q.push(v_);
      if (v_ == t) return true; 
    }
  }
  return false;
}
LL EK() {
  LL ret = 0;
  while (Bfs()) { //更改路径的残余容量
    for (int u_ = t; u_ != s; u_ = v[from[u_] ^ 1]) {
      w[from[u_]] -= f[t]; 
      w[from[u_] ^ 1] += f[t];
    }
    ret += f[t];
  }
  return ret; 
}
//=============================================================
int main() {
  n = read(), m = read(), s = read(), t = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read(), w_ = read();
    AddEdge(u_, v_, w_);
    AddEdge(v_, u_, 0);
  }
  printf("%lld\n", EK());
  return 0;
}

Dinic

使用 dfs 找增广路,不过在每轮增广前使用 bfs 将残余网络分层。一个点的层数为残余网络上它与源点的最小边数。分层时若汇点的层数不存在,说明不存在增广路,即可停止增广。
之后 dfs 增广时,每次转移仅向比当前点层数多 1 的点转移。这可以保证每次找到的增广路是最短的。

多路增广 + 当前弧优化。

//知识点:网络最大流,Dinic
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 210 + 10;
const int kM = 1e4 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, m, s, t;
int e_num = 1, head[kN], v[kM], ne[kM];
int cur[kN], dep[kN];
LL w[kM];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
  v[++ e_num] = v_;
  w[e_num] = 1ll * w_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
bool Bfs() {
  std::queue <int> q;
  memset(dep, 0, sizeof (dep));
  dep[s] = 1; //注意初始化 
  q.push(s); 
  while (!q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      LL w_ = w[i];
      if (w_ > 0 && !dep[v_]) { 
        dep[v_] = dep[u_] + 1;
        q.push(v_);
      }
    }
  }
  return dep[t];
}
LL Dfs(int u_, LL into_) {
  if (u_ == t || !into_) return into_;
  LL out = 0;
  for (int& i = cur[u_]; i && into_; i = ne[i]) { //当前弧优化 
    int v_ = v[i];
    LL w_ = w[i];
    if (dep[v_] == dep[u_] + 1 && w_ > 0) {
      LL ret = Dfs(v_, std::min(into_, w_));
      if (!ret) dep[v_] = kN; //没有贡献,之后不会再访问 
      w[i] -= ret, w[i ^ 1] += ret;
      out += ret, into_ -= ret;
    }
  }
  return out;
}
LL Dinic() {
  LL ret = 0;
  while (Bfs()) {
    for (int i = 1; i <= n; ++ i) cur[i] = head[i]; //初始化当前弧 
    ret += Dfs(s, kInf);
  }
  return ret;
}
//=============================================================
int main() {
  n = read(), m = read(), s = read(), t = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read(), w_ = read();
    AddEdge(u_, v_, w_);
    AddEdge(v_, u_, 0);
  }
  printf("%lld\n", Dinic());
  return 0;
}

二分图匹配

P3386 【模板】二分图最大匹配

匈牙利

枚举一边的点,找增广路。
复杂度 \(O(n^2 m)\)

//知识点:二分图最大匹配 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 500 + 10;
const int kMaxe = 5e4 + 10;
//=============================================================
int n, m, e, ans;
int e_num, head[kMaxn], v[kMaxe], ne[kMaxe];
int match[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
bool Dfs(int u_) {
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (vis[v_]) continue ;
    vis[v_] = true;
    if (! match[v_] or Dfs(match[v_])) {
      match[v_] = u_;
      return true;
    }
  }
  return false;
}
//=============================================================
int main() {
  n = read(), m = read(), e = read();
  for (int i = 1; i <= e; ++ i) {
    int u_ = read(), v_ = read();
    if (u_ > n || v_ > m) continue ;
    AddEdge(u_, v_);
  }
  for (int i = 1; i <= n; ++ i) {
    memset(vis, 0, sizeof (vis));
    if (Dfs(i)) ++ ans;
  }
  printf("%d\n", ans);
  return 0;
}

网络流

建模,在一个匹配中, 任意两条边都没有共同的端点,每一个端点 最多只有 1 的贡献。 在网络流中,最多只有 1 的贡献可转化为 边的容量为 1。由此可对原二分图进行转化:

新建源点 与 汇点。

  • 源点向每个左侧点 各连一条容量为 1 的有向边。
  • 原图中的边改为 自左向右的有向边,容量为 1。
  • 每个右侧点向汇点 各连一条容量为 1 的有向边。

求最大流即为最大匹配。

线段树优化建图

用于解决区间连边的一个建图小技巧。

对于区间连边问题,其解决方案是建立一系列虚点,虚点到实点的权值为 0,一个虚点与某一段连续区间内的实点相连。此时若想对该区间进行区间连边,直接连向此虚点即可。
为解决区间的拆分问题,可以将虚点建成一个如下图所示的类似线段树的结构,使得区间的拆分可以放到线段树上进行。

CF786B Legacy

//知识点:线段树优化建图 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 4e5 + 10;
const LL kInf = 0x3f3f3f3f3f3f3f3f;
//=============================================================
int n, q, start, node_num;
int e_num, head[kN], v[kN << 4], w[kN << 4], ne[kN << 4];
bool vis[kN];
LL dis[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) {
    w = (w << 3) + (w << 1) + (ch ^ '0');
  }
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Add(int u_, int v_, int w_) {
  v[++ e_num] = v_, w[e_num] = w_;
  ne[e_num] = head[u_], head[u_] = e_num;
}
#define ls (lson[now_])
#define rs (rson[now_])
#define mid ((L_+R_)>>1)
struct SegmentTree {
  int root, lson[kN], rson[kN];
  void Build(int &now_ ,int L_, int R_, bool type_) {
    if (L_ == R_) {
      now_ = L_;
      return ;
    }
    now_ = ++ node_num;
    Build(ls, L_, mid, type_);
    Build(rs, mid + 1, R_, type_);
    if (!type_) Add(now_, ls, 0), Add(now_, rs, 0);
    if (type_) Add(ls, now_, 0), Add(rs, now_, 0);
  }
  void Modify(int now_, int L_, int R_, int l_, int r_, int u_, int w_, 
              bool type_) {
    if (l_ <= L_ && R_ <= r_) {
      if (!type_) Add(u_, now_, w_);
      if (type_) Add(now_, u_, w_);
      return ;
    }
    if (l_ <= mid) Modify(ls, L_, mid, l_, r_, u_, w_, type_);
    if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, u_, w_, type_);
  }
} Seg[2];
#undef ls
#undef rs
#undef mid
void Init() {
  node_num = n;
  Seg[0].Build(Seg[0].root, 1, n, 0);
  Seg[1].Build(Seg[1].root, 1, n, 1);
}
void Spfa(int s_) {  //他死了
  std::queue <int> q;
  memset(dis, 0x3f, sizeof(dis));
  dis[s_] = 0;
  q.push(s_);
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (int i = head[u]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u] + w_ < dis[v_]) {
        dis[v_] = dis[u] + w_;
        q.push(v_);
      }
    }
  }
}
#define pr std::pair
#define mp std::make_pair
void Dijkstra(int s_) {
  std::priority_queue <pr <LL, int> > q;
  memset(dis, 63, sizeof (dis));
  dis[s_] = 0;
  q.push(mp(0, s_));
  while (! q.empty()) {
    int u_ = q.top().second; q.pop();
    if (vis[u_]) continue;
    vis[u_] = true;
    
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        q.push(mp(-dis[v_], v_));
      }
    }
  }
}
#undef pr
#undef mp
//=============================================================
int main() {
  n = read(), q = read(), start = read();
  Init();
  while (q --) {
    int opt = read();
    if (opt == 1) {
      int u_ = read(), v_ = read(), w_ = read();
      Add(u_, v_, w_);
    } else {
      int u_ = read(), l_ = read(), r_ = read(), w_ = read();
      int type = (opt == 3);
      Seg[type].Modify(Seg[type].root, 1, n, l_, r_, u_, w_, type);
    }
  }
  Dijkstra(start);
  for (int i = 1; i <= n; ++ i) {
    printf("%lld ", dis[i] < kInf ? dis[i] : -1); 
  }
  return 0;
}

字符串

Trie

字典树。

namespace Trie {
  const int kMaxNode = kN << 5;
  int node_num, tr[kMaxNode][2];
  LL val[kMaxNode];
  void Insert(LL val_) {
    int now_  = 0;
    for (LL i = 33; i >= 0; -- i) {
      int ch = val_ >> i & 1ll;
      if (!tr[now_][ch]) tr[now_][ch] = ++ node_num;
      now_ = tr[now_][ch];
    }
    val[now_] = val_;
  }
}

可持久化 Trie

Q:什么时候需要可持久化?
A:想建一车 Trie 但是建不下的时候。

其实我很疑惑该把这个加到数据结构还是字符串里的= =

P4735 最大异或和

可持久化 Trie 可以将全局的二进制运算问题转化到区间上进行。

//知识点:
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 6e6 + 10;
//=============================================================
int n, m, a[kN], sum[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Trie {
  #define ls (tr[now_][0])
  #define rs (tr[now_][1])
  const int kMaxRoot = kN;
  const int kMaxNode = kN << 2;
  int node_num, root[kMaxRoot], tr[kMaxNode][2], last[kMaxNode];
  void Insert(int &now_, int pre_, int val_, int lth_, int id_) {
    now_ = ++ node_num;
    ls = tr[pre_][0], rs = tr[pre_][1], last[now_] = id_;
    if (lth_ < 0) return ;
    int ch = val_ >> lth_ & 1;
    Insert(tr[now_][ch], tr[pre_][ch], val_, lth_ - 1, id_);
  }
  int Query(int now_, int val_, int lth_, int l_) {
    if (lth_ < 0) return sum[last[now_]];
    int ch = val_ >> lth_ & 1;
    if (last[tr[now_][ch ^ 1]] >= l_) {
      return Query(tr[now_][ch ^ 1], val_, lth_ - 1, l_);
    }
    return Query(tr[now_][ch], val_, lth_ - 1, l_);
  }
}
void Init() {
  n = read(), m = read();
  Trie::last[0] = -1;
  Trie::Insert(Trie::root[0], 0, 0, 25, 0);
  for (int i = 1; i <= n; ++ i) {
    sum[i] = sum[i - 1] ^ read();
    Trie::Insert(Trie::root[i], Trie::root[i - 1], sum[i], 25, i);
  }
}
//=============================================================
int main() { 
  Init();
  while (m --) {
    char opt[5]; scanf("%s", opt + 1);
    if (opt[1] == 'A') {
      ++ n;
      sum[n] = sum[n - 1] ^ read(); 
      Trie::Insert(Trie::root[n], Trie::root[n - 1], sum[n], 25, n);
    } else {
      int l = read(), r = read(), val = sum[n] ^ read();
      printf("%d\n", Trie::Query(Trie::root[r - 1], val, 25, l - 1) ^ val);
    }
  }
  return 0; 
}

KMP

子串匹配问题。在朴素算法中,如果在某一位上失配,则会抛弃当前已匹配的部分,跳到下一个位置再从开头进行匹配。
而 KMP 利用了当前已匹配的部分,使得在下一个位置时不必从开头进行匹配,从而对朴素算法进行了加速。

P3375 【模板】KMP 字符串匹配

//知识点:KMP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
char s1[kN], s2[kN];
int n1, n2;
int fail[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
//=============================================================
int main() {
  scanf("%s", s1 + 1);
  scanf("%s", s2 + 1);
  n1 = strlen(s1 + 1), n2 = strlen(s2 + 1);

  fail[1] = 0;
  for (int i = 2, j = 0; i <= n2; ++ i) {
    while (j > 0 && s2[i] != s2[j + 1]) j = fail[j];
    if (s2[i] == s2[j + 1]) ++ j;
    fail[i] = j;
  }
  for (int i = 1, j = 0; i <= n1; ++ i) {
    while (j > 0 && (j == n2 || s1[i] != s2[j + 1])) j = fail[j];
    if (s1[i] == s2[j + 1]) ++ j;
    if (j == n2) printf("%d\n", i - n2 + 1);
  }
  for (int i = 1; i <= n2; ++ i) printf("%d ", fail[i]);
  return 0;
}

AC 自动机

可以认为是 KMP 算法在 Trie 树上的应用,与 KMP 算法在失配时应用已匹配部分的 border 进行跳转类似,AC 自动机在失配时会根据失配指针跳转到 Trie 树上代表已匹配部分的 border 的节点,从而加速匹配。

P5357 【模板】AC自动机

//知识点:ACAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#define LL long long
const int kT = 2e6 + 10;
const int kN = 2e5 + 10;
//=============================================================
int n;
char s[kT];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace ACAM {
  std::vector <int> id[kN];
  int node_num, tr[kN][26], sum[kN], fail[kN];
  int e_num, size[kN], head[kN], v[kN], ne[kN];
  void Add(int u_, int v_) {
    v[++ e_num] = v_;
    ne[e_num] = head[u_];
    head[u_] = e_num;
  }
  void Dfs(int u_) {
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Dfs(v_);
      size[u_] += size[v_];
    }
    for (int i = 0, lim = id[u_].size(); i < lim; ++ i) {
      sum[id[u_][i]] = size[u_];
    }
  }
  void Insert(int id_, char *s_) {
    int now = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[now][s_[i] - 'a']) tr[now][s_[i] - 'a'] = ++ node_num;
      now = tr[now][s_[i] - 'a'];
    }
    id[now].push_back(id_);
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      int u_ = q.front(); q.pop();
      for (int i = 0; i < 26; ++ i) {
        if (tr[u_][i]) {
          fail[tr[u_][i]] = tr[fail[u_]][i];
          q.push(tr[u_][i]);
        } else {
          tr[u_][i] = tr[fail[u_]][i];
        }
      }
    }
  }
  void Query(char *t_) {
    int now = 0, lth = strlen(t_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      now = tr[now][t_[i] - 'a'];
      ++ size[now];
    }
    for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
    Dfs(0);
    for (int i = 1; i <= n; ++ i) printf("%d\n", sum[i]);
  }
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) {
    scanf("%s", s + 1);
    ACAM::Insert(i, s); 
  }
  ACAM::Build();
  scanf("%s", s + 1);
  ACAM::Query(s);
  return 0;
}

最小表示法

当字符串 \(S\) 中可以选定一个位置 \(i\) 满足:

\[S[i:n]+S[1:i-1]=T \]

则称 \(S\)\(T\) 循环同构

字符串 \(S\)最小表示 为与 \(S\) 循环同构的所有字符串中字典序最小的字符串。

最小表示法思想与 KMP 类似,都是重复利用之前比较过的部分,减少时间复杂度。
学习笔记:「最小表示法」P1368 工艺 /【模板】最小表示法

P1368 工艺 /【模板】最小表示法

//知识点:最小表示法 
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define ll long long
const int kMaxn = 3e5 + 10;
//=============================================================
int n, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
int MinRep() {
  int i = 0, j = 1, k = 0;
  while (i < n && j < n && k < n) {
    int delta = a[(i + k) % n] - a[(j + k) % n];
    if (delta == 0) {
      ++ k;
      continue ;
    }
    if (delta > 0) i += k + 1;
    else j += k + 1;
    if (i == j) j ++;
    k = 0;
  }
  return std :: min(i, j);
}
//=============================================================
int main() {
  n = read();
  for (int i = 0; i < n; ++ i) a[i] = read();
  for (int i = MinRep(), j = 0; j < n; ++ j) {
    printf("%d ", a[(i + j) % n]);
  }
  return 0;
}

后缀数组

字符串 \(S\) 的后缀数组定义为两个数组 \(sa,rk\)
\(sa\) 储存 \(S\) 的所有后缀 按字典序排序后的起始下标,满足 \(S[sa_{i-1}:n]<S[sa_i:n]\)
\(rk\) 储存 \(S\) 的所有后缀的排名。
结合后缀的最长公共前缀,后缀数组可以用来处理许多复杂度字符串问题。

「笔记」后缀数组

UOJ#35. 后缀排序

//知识点:SA
/*
By:Luckyblock
I love Marisa;
But Marisa has died;
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
char S[kMaxn];
//rk[i]: 倍增过程中子串[i:i+2^k-1]的排名,
//sa[i] 排名为i的子串 [i:i+2^k-1],
//它们互为反函数。
//存在越界风险,如果不写特判,rk 和 oldrk 要开 2 倍空间。
int n, m, sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1];
int id[kMaxn], cnt[kMaxn]; //用于计数排序的两个 temp 数组
int height[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetHeight() {
  for (int i = 1, k = 0; i <= n; ++ i) {
      if (rk[i] == 1) {
        k = 0;
      } else {
        if (k) -- k;
        int j = sa[rk[i] - 1];
        while (i + k <= n && j + k <= n && 
               S[i + k] == S[j + k]) {
          ++ k;
        }
      }
      height[rk[i]] = k;
    }
}

//=============================================================
int main() {
  scanf("%s", S + 1);
  n = strlen(S + 1);
  m = std :: max(n, 300); //值域大小
  
  //初始化 rk 和 sa
  for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[i]];
  for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
  for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;

  //倍增过程。w 是已经推出的子串长度,数值上等于上述分析中的 2^{k-1}。
  //注意此处的 sa 数组存的并不是后缀的排名,存的是倍增过程中指定长度子串的排名。
  for (int w = 1; w < n; w <<= 1) {
    //按照后半截 rk[i+w] 作为第二关键字排序。
    memset(cnt, 0, sizeof (cnt));
    for (int i = 1; i <= n; ++ i) id[i] = i;
    for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i] + w]]; //越界风险,2倍空间
    for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i] + w]] --] = id[i];

    //按照前半截 rk[i] 作为第一关键字排序。
    memset(cnt, 0, sizeof (cnt));
    for (int i = 1; i <= n; ++ i) id[i] = sa[i];
    for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i]]];
    for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i]]] --] = id[i];

    //更新 rk 数组,可以滚动数组一下,但是可读性会比较差(
    for (int i = 1; i <= n; ++ i) oldrk[i] = rk[i];
    for (int p = 0, i = 1; i <= n; ++ i) {
      if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&  //判断两个子串是否相等。
          oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) { //越界风险,2倍空间
        rk[sa[i]] = p;
      } else {
        rk[sa[i]] = ++ p;
      }
    }
  }
  for (int i = 1; i <= n; ++ i) printf("%d ", sa[i]);
  printf("\n");
  GetHeight();
  for (int i = 2; i <= n; ++ i) printf("%d ", height[i]);
  return 0;
}

后缀自动机

「笔记」后缀自动机

字符串 \(S\) 的后缀自动机 (suffix automaton, SAM) 是一个可以且尽可以接受 \(S\) 所有后缀的 最小的 DFA。
更形式化的定义:

  • 字符串 \(S\) 的 SAM 是一张 DAWG(有向单词无环图)。节点被称作 状态,边被称作状态间的 转移
  • 存在一个起始节点,称作 起始状态。其它节点均可从起始节点出发到达。
  • 每个 转移 都标有一些字母。从一个节点出发的所有转移均 不同
  • 存在数个 终止状态。若从 \(t_0\) 出发,最终转移到一个终止状态,路径上所有转移连接起来一定是 \(S\) 的一个后缀。\(S\) 的每个后缀均可用一条 从起始节点到某个终止状态 的路径构成。
  • 在所有满足上述条件的 DFA 中,SAM 的节点数是最少的。

SAM 并不是一个典型的 DFA,在 DAWG 基础上,除 \(t_0\) 外的每个状态都被添加了一条 后缀链接。所有后缀链接组成了树状结构,这棵树被称为 parent 树

字符串 \(S\) 的 SAM 能包含 \(S\) 所有子串的信息。
SAM 将这些信息以高度压缩的形式储存,对于一个长度为 \(n\) 的字符串,它的 SAM 空间复杂度仅为 \(O(n)\),构建 SAM 的时间复杂度也仅为 \(O(n)\)

P3804 【模板】后缀自动机 (SAM)

//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
int n, last = 1, node_num = 1; //注意 1 为起始状态,其他状态的标号从 1 开始。 
int ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1]; //SAM
int edge_num, v[kMaxn << 1], ne[kMaxn << 1], head[kMaxn << 1]; //Graph
ll ans, size[kMaxn << 1];
char S[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(ll &fir, ll sec) {
  if (sec > fir) fir = sec;
}
void AddEdge(int u_, int v_) {
  v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
}
void Insert(int c_) { //原串 S -> 新串 S + c
  int p = last, now = last = ++ node_num;
  size[now] = 1, len[now] = len[p] + 1; //仅为终止状态 size 赋值。
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  
  //case 1:
  if (! p) {link[now] = 1; return ;} 

  //case 2:
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return ;}

  //case 3:
  int newq = ++ node_num;
  len[newq] = len[p] + 1; 
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 

  link[newq] = link[q];
  link[q] = link[now] = newq; 
  
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
}

//parent 树上 DP,用孩子更新父亲。
//孩子的长度比父亲长,则孩子出现的地方,父亲一定出现,size 累加。
//父亲的 size 也可能不为 0(为终止状态)。
void Dfs(int u) { 
  for (int i = head[u]; i; i = ne[i]) {
    Dfs(v[i]);
    size[u] += size[v[i]];
  }
  if (size[u] > 1) GetMax(ans, 1ll * size[u] * len[u]);
}
//=============================================================
int main() {
  scanf("%s", S + 1);
  int n = strlen(S + 1);
  for (int i = 1; i <= n; ++ i) Insert(S[i] - 'a');
  for (int i = 2; i <= node_num; ++ i) AddEdge(link[i], i);
  Dfs(1);
  printf("%lld\n", ans);
  return 0; 
}

广义后缀自动机

「笔记」广义后缀自动机

广义 SAM 是一种用于维护 Trie 的子串信息的 SAM 的简单变体。

将多个模式串插入到 Trie 后,即可使用广义 SAM 维护多模式串的信息。这是广义 SAM 最广泛的应用之一,本文的重点也在于此。其基本思想是将多串的信息进行压缩,使得 SAM 在仍满足节点数最少的同时 包含所有子串的信息。此时 SAM 中的一个状态可能同时代表多个串中相应的子串。

P6139 【模板】广义后缀自动机(广义 SAM)

//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
ll ans;
char S[kMaxn];
int node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) return q;
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    return newq;
  }
  int p = last_, now = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
//=============================================================
int main() {
  int T = read();
  while (T --) {
    scanf("%s", S + 1);
    int n = strlen(S + 1), last = 1;
    for (int i = 1; i <= n; ++ i) {
      last = Insert(S[i] - 'a', last);
    }
  }
  for (int i = 2; i <= node_num; ++ i) {
    ans += len[i] - len[link[i]];
  }
  printf("%lld\n", ans);
  return 0; 
}

Manacher

「笔记」manacher 算法 - Luckyblock

//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
const int kN = 1e7 + 1e6 + 10;
//=============================================================
int n, p[kN << 1];
char s[kN], t[kN << 1];
//=============================================================
inline int read() {
	int f = 1, w = 0; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
	for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
	return f * w;
}
//=============================================================
int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	for (int i = 1; i <= n; ++ i) t[2 * i - 1] = '%', t[2 * i] = s[i];
	t[n = 2 * n + 1] = '%';
	int pos = 0, r = 0;
	for (int i = 1; i <= n; ++ i) {
		p[i] = 1;
		if (i < r) p[i] = std::min(p[2 * pos - i], r - i + 1);
		while (i - p[i] >= 1 && i + p[i] <= n && 
					 t[i - p[i]] == t[i + p[i]]) {
			++ p[i];
		}
		if (i + p[i] - 1 > r) pos = i, r = i + p[i] - 1;
	}
	int maxp = 0;
	for (int i = 1; i <= n; ++ i) maxp = std::max(maxp, p[i] - 1);
	printf("%d\n", maxp);
	return 0;
}

数学

快速幂

P1226 【模板】快速幂||取余运算

转化为 \(\prod a^{2^?}\)

//知识点:快速幂 
/*
By:Luckyblock
注意 p = 0 
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
//=============================================================
LL a, b, p;
//=============================================================
inline LL read() {
  LL f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
LL QPow(LL x_, LL y_, LL mod_) {
  LL ret = 1;
  x_ %= mod_;
  for (; y_; y_ >>= 1ll) {
    if (y_ & 1) ret = ret * x_ % mod_;
    x_ = x_ * x_ % mod_;
  }
  return ret % mod_;
}
//=============================================================
int main() {
  LL a = read(), b = read(), p = read();
  printf("%lld^%lld mod %lld=%lld", a, b, p, QPow(a, b, p));
  return 0;
}

线性筛

P3383 【模板】线性筛素数

每个数只被它的最小质因子筛掉。

//知识点:先行筛 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e8 + 10;
//=============================================================
int n, q;
int p_num, p[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void GetPrime(int n_) {
  for (int i = 2; i <= n_; ++ i) {
    if (! vis[i]) {
      p[++ p_num] = i; 
    }
    for (int j = 1; j <= p_num; ++ j) {
      if (i * p[j] > n_) break;
      vis[i * p[j]] = true;
      if (i % p[j] == 0) break;
    }
  }
}
//=============================================================
int main() {
  n = read(), q = read();
  GetPrime(n);
  while (q --) {
    int k = read();
    printf("%d\n", p[k]);
  }
  return 0;
}

线性求逆元

P3811 【模板】乘法逆元

\(p\in \mathbb{P}\),则有:

\[\operatorname{inv}(i) = \left(p - \left\lfloor\frac{p}{i}\right\rfloor\right)\times \operatorname{inv}(p \bmod i) \pmod p \]

//知识点:线性求逆元 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e6 + 10;
//=============================================================
int n, p, inv[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
  n = read(), p = read();
  inv[1] = 1;
  for (int i = 2; i <= n; ++ i) {
    inv[i] = 1ll * (p - p / i) * inv[p % i] % p;
  }
  for (int i = 1; i <= n; ++ i) {
    printf("%d\n", inv[i]);
  }
  return 0;
}

矩阵快速幂

P3390 【模板】矩阵快速幂

矩阵乘法:

\[\begin{aligned} A\times B &= \begin{bmatrix} a_{1,1}& a_{1,2}\\ a_{2,1}& a_{2,2} \end{bmatrix} \times \begin{bmatrix} b_{1,1}& b_{1,2}\\ b_{2,1}& b_{2,2} \end{bmatrix}\\ &= \begin{bmatrix} \sum a_{1,i} b_{i,1}& \sum a_{1,i}b_{i,2}\\ \sum a_{2,i} b_{i,1}& \sum a_{2,i}b_{i,2} \end{bmatrix} \end{aligned}\]

矩阵满足结合律,故可拆分为 \(\prod A^{2^?}\),使用快速幂求解。

//知识点:矩阵乘法,矩阵快速幂
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxm = 100 + 1;
const LL kMod = 1e9 + 7;
//=============================================================
int n;
LL b;
struct Matrix {
  LL a[kMaxm][kMaxm];
  void build() {
    for (int i = 1; i <= n; ++ i) {
      a[i][i] = 1;
    }
  }
  Matrix operator * (const Matrix &b_) const {
    Matrix ret;
    memset(ret.a, 0, sizeof (ret.a));
    for (int k = 1; k <= n; ++ k) {
      for (int i = 1; i <= n; ++ i) {
        for (int j = 1; j <= n; ++ j) {
          ret.a[i][j] += a[i][k] * b_.a[k][j] % kMod;
          ret.a[i][j] %= kMod;
        }
      }
    }
    return ret;
  }
} a;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
Matrix QMatrixPow(Matrix a_, LL b_) {
  Matrix ret;
  ret.build();
  for (; b_; b_ >>= 1) {
    if (b_ & 1) ret = ret * a_;
    a_ = a_ * a_;
  }
  return ret;
}
//=============================================================
int main() {
  n = read(); scanf("%lld", &b);
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      a.a[i][j] = read();
    }
  }
  Matrix ans = QMatrixPow(a, b);
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      printf("%d ", ans.a[i][j]);
    }
    printf("\n");
  }
  return 0;
}

三分法

P3382 【模板】三分法

之前讲课的课件:「二分与三分」课件配套资料

//知识点:三分法求函数极值 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 13 + 5;
const double eps = 1e-7;
//=============================================================
int n;
double l, r, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
double f(double x_) {
  double ret = 0;
  for (int i = 1; i <= n + 1; ++ i) {
    ret = ret * x_ + a[i];
  }
  return ret;
}
//=============================================================
int main() {
  n = read();
  scanf("%lf %lf", &l, &r);
  for (int i = 1; i <= n + 1; ++ i) scanf("%lf", &a[i]);
  while (r - l > eps) {
    double lx = (2.0 * l + r) / 3, rx = (l + 2.0 * r) / 3;
    if (f(lx) < f(rx)) {
      l = lx;
    } else {
      r = rx;
    }
  }
  printf("%.5lf", l);
  return 0;
}

高斯消元

P3389 【模板】高斯消元法

小学解方程。

//知识点:高斯消元 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cmath>
#include <cstring>
#define LL long long
const double eps = 1e-7;
const int kMaxn = 100 + 10;
//=============================================================
int n;
double a[kMaxn][kMaxn], ans[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n + 1; ++ j) {
      scanf("%lf", &a[i][j]);
    }
  }
  for (int i = 1; i <= n; ++ i) { //每次将第 i 行第 i 个元素消为 1。 
    int pos = i;  //每次找到最大系数消除,减小误差 
    for (int j = i + 1; j <= n; ++ j) {
      if (fabs(a[j][i]) > fabs(a[pos][i])) {
        pos = j;
      }
    }
    
    if (fabs(a[pos][i]) < eps) { //系数为 0,无解 
      printf("No Solution");
      return 0;
    }
    
    for (int j = 1; j <= n + 1; ++ j) { //交换到第 i 行 
      std::swap(a[pos][j], a[i][j]);
    }
    
    double tmp = a[i][i]; //将第 i 行第 i 个消成 1,同时处理该行其他系数 
    for (int j = i; j <= n + 1; ++ j) {
      a[i][j] /= tmp;
    }
    for (int j = i + 1; j <= n; ++ j) { //枚举其他行 
      tmp = a[j][i]; //该行对应系数 
      for (int k = i; k <= n + 1; ++ k) {
        a[j][k] -= a[i][k] * tmp; //注意第 i 行的系数均已被处理过 
      }
    }
  }
  
  ans[n] = a[n][n + 1];
  for (int i = n - 1; i >= 1; -- i) {
    ans[i] = a[i][n + 1];
    for (int j = i + 1; j <= n; ++ j) { //回带
      ans[i] -= a[i][j] * ans[j];
    }
  }
  
  for (int i = 1; i <= n; ++ i) {
    printf("%.2lf\n", ans[i]);
  }
  return 0;
}

卢卡斯定理

\[\dbinom{n}{m} = \begin{pmatrix} {\left\lfloor\frac{n}{p}\right\rfloor}\\{\left\lfloor\frac{m}{p}\right\rfloor}\end{pmatrix}\cdot \dbinom{n\bmod p}{m\bmod p}\pmod p\]

//知识点:Lucas 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxp = 1e5 + 10;
//=============================================================
LL p, fac[kMaxp];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
LL QPow(LL x_, LL y_) {
  x_ %= p;
  LL ret = 1ll;
  for (; y_; y_ >>= 1ll) {
    if (y_ & 1) ret = ret * x_ % p;
    x_ = x_ * x_ % p;
  }
  return ret;
}
LL C(LL n_, LL m_) {
  if (m_ > n_) return 0ll;
  return fac[n_] * QPow(fac[m_], p - 2ll) * QPow(fac[n_ - m_], p - 2ll);
}
LL Lucas(LL n_, LL m_) {
  if (! m_) return 1;
  return Lucas(n_ / p, m_ / p) * C(n_ % p, m_ % p) % p;
}
//=============================================================
int main() {
  int T = read();
  while (T --) {
    int n = read(), m = read();
    p = read(); 
    fac[0] = 1;
    for (int i = 1; i <= p; ++ i) {
      fac[i] = 1ll * fac[i - 1] * i % p;
    }
    printf("%lld\n", Lucas(n + m, m));
  }
  return 0;
}

矩阵求逆

Luogu

这里仅给出一种使用高斯-约旦消元的做法。

\([AB]\) 表示将一个 \(n\times m_b\) 的矩阵 \(B\) 拼接一个 \(n\times m_a\) 的矩阵 \(A\) 之后形成的矩阵,即有:

\[[AB]_{i, j} = \begin{cases} A_{i, j} &(j\le m_a)\\ B_{i, j - m_a} &(m_a <j \le m_a+m_b) \end{cases}\]

\(I\)\(n\times n\) 的单位矩阵,则有:

\[A^{-1}\times [AI] = [IA^{-1}] \]

则若我们可以通过对矩阵 \([AI]\) 进行若干操作使得矩阵的左半部分(即拼接后左半边的 \(A\))转化为单位矩阵 \(I\),则我们进行的操作等价于令 \(A^{-1}\) 乘上了矩阵 \([AI]\),则得到的矩阵的右半部分即为所求的逆矩阵 \(A^{-1}\)

使用高斯-约旦法将 \([AI]\) 的左半边消成对角矩阵,再令每行除以系数即可。

由于是在模意义下,消元时的除法需要用逆元代替。

总复杂度 \(O(n^3)\) 级别。

//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
#define LL long long
const int kN = 410;
const LL p = 1e9 + 7;
//=============================================================
int n;
LL a[kN][kN << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
  return f * w;
}
LL qpow(LL x_, LL y_) {
  LL ret = 1;
  while (y_) {
    if (y_ & 1) ret = ret * x_ % p;
    x_ = x_ * x_ % p, y_ >>= 1ll;
  }
  return ret;
}
bool Gauss_jordan() {
  for (int i = 1; i <= n; ++ i) {
    int pos = i;
    for (int j = i + 1; j <= n; ++ j) {
      if (a[j][i] > a[pos][i]) pos = j;
    }
    if (pos != i) std::swap(a[i], a[pos]);
    if (!a[i][i]) return false;

    LL inv = qpow(a[i][i], p - 2);
    for (int j = 1; j <= n; ++ j) {
      if (j == i) continue;
      LL delta = a[j][i] * inv % p;
      for (int k = 1; k <= 2 * n; ++ k) {
        a[j][k] = ((a[j][k] - delta * a[i][k]) % p + p) % p;
      }
    }
    for (int j = 1; j <= 2 * n; ++ j) a[i][j] = a[i][j] * inv % p;
  }
  return true;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  n = read();
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      a[i][j] = read(), a[i][i + n] = 1;
    }
  }
  if (!Gauss_jordan()) {
    printf("No Solution\n");
    return 0;
  }
  for (int i = 1; i <= n; ++ i) {
    for (int j = n + 1; j <= 2 * n; ++ j) {
      printf("%lld ", a[i][j]);
    }
    printf("\n");
  }
  return 0;
}

exgcd

Luogu

\(T\) 组数据,每组数据给定参数 \(a, b, c\),描述了一个不定方程 \(ax+by = c\)。要求:

  • 如果该方程无整数解,输出 -1
  • 若该方程有整数解,且有正整数解,则输出其正整数解的数量,所有正整数解中 \(x\) 的最小值,所有正整数解中 \(y\) 的最小值,所有正整数解中 \(x\) 的最大值,以及所有正整数解中 \(y\) 的最大值。
  • 若方程有整数解,但没有正整数解,你需要输出所有整数解中 \(x\) 的最小正整数值,\(y\) 的最小正整数值。

\(1\le T\le 2\times 10^5\)\(1\le a, b, c\le 10^9\)
500ms,16MB。

对于 \(ax + by = c\),显然应有 \(\gcd(a, b) \mid c\),若不满足该条件则无解,否则由裴蜀定理,\(ax + by = \gcd(a, b)\) 有无数组整数解,使用 exgcd 即可求得该不定方程的一组整数特解 \((x', y')\),则有:

\[\begin{aligned} ax' + by' = \gcd(a, b)\\ a\dfrac{c\times x'}{\gcd(a, b)} + b\times \dfrac{c\times y'}{\gcd(a, b)} = c \end{aligned}\]

\(\left(\dfrac{c\times x'}{\gcd(a, b)},\dfrac{c\times y'}{\gcd(a, b)}\right)\) 即为原不定方程的一组整数特解。我们记这组特解为 \((x_0, y_0)\),同时记 \(d = \gcd(a, b)\),接下来考虑在这一组特解的基础上,求得题目所需的解。

考虑将 \(x_0\) 变为 \(x_0 + p\),设 \(p\) 为正整数,则存在正整数 \(q\),令 \(x_0\) 变为 \(x_0+p\) 的同时会将 \(y_0\) 缩小到 \(y_0-q\),则有 \(a(x_0 + p) + b(y_0 - q) = c\)。联立 \(ax_0+by_0=c\) 可得 \(p = \frac{b\times q}{a}\)。则有 \(a\mid b\times q\),又有 \(b\mid b\times q\),则 \(b\times q\) 的最小值应为 \(\operatorname{lcm} (a,b)\)。则可得 \((x_0, y_0)\) 变化的最小变化满足:

\[\begin{cases} p_0 = \dfrac{\operatorname{lcm}(a,b)}{a} = \dfrac{b}{d}\\ q_0 = \dfrac{\operatorname{lcm}(a,b)}{a} = \dfrac{a}{d} \end{cases}\]

由于 \(\operatorname{lcm} (a,b) | b\times q\),易证 \((x_0, y_0)\) 的变化一定是 \((p_0, q_0)\) 的整数倍。

然后开始构造答案:

首先将 \(x_0\) 变为最小的正整数值,即找到 \(k\in \N\),使得:\(x_0 + k\times p_0 \ge 1\),则有:\(k\ge \frac{1-x_0}{p}\)\(k\) 应取 \(\left\lceil\frac{1-x_0}{p}\right\rceil\),则令 \(x_1 = x_0 + k\times p\)\(y_1 = y_0 - k\times q\)\((x_1, y_1)\) 即为 \(x\) 最小的正整数解。此时检查 \(y_1\) 的符号,如果 \(y_1>0\),则有正整数解,否则由于 \(x\) 已经取了最小的正整数解,则 \(y\) 不可能也为正整数,原方程无正整数解。

然后考虑每一问:

对于有正整数解的情况:

  • 解的个数。除去当前得到的特解 \((x_1, y_1)\),其他解的个数即在当前解基础上,保证 \(y>0\) 的情况下令 \(y-q_0\) 的次数,答案即 \(\left\lfloor\frac{y-1}{q_0}\right\rfloor + 1\)
  • \(x\) 的最小整数值。即 \(x_1\)
  • \(y\) 的最小正整数值。即在 \(y_1\) 基础上减去最多次 \(q_0\)\(y\) 的值,答案即 \((y_1 - 1)\bmod q + 1\)
  • \(x\) 的最大整数值。即上一问中 \(y\) 对应的 \(x\),答案即 \(x_1 + \left\lfloor\frac{y-1}{q_0}\right\rfloor\times p\)
  • \(y\) 的最大整数值。即 \(y_1\)

对于无正整数解的情况:

  • \(x\) 的最小正整数值。即 \(x_1\)
  • \(y\) 的最小正整数值。同构造 \(x_1\) 时使用的方法,取 \(k = \left\lceil\frac{1-y_0}{p}\right\rceil\),答案即 \(y_0 + k\times q_0\)

总复杂度 \(O(T\log A)\) 级别,\(A\) 为值域。

注意烦的一批的类型转换。

ceil() 的返回值是 double 类型,long long * double = double,此时可能会出现精度损失,需要注意作乘法前先把 ceil() 的返回值转化为 long long 类型。

//By:Luckyblock
/*
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <algorithm>
#define LL long long
//=============================================================
//=============================================================
inline LL read() {
  LL f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + ch - '0';
  return f * w;
}
LL exgcd(LL a_, LL b_, LL &x_, LL &y_) {
  if (!b_) {
    x_ = 1, y_ = 0;
    return a_;
  }
  LL d_ = exgcd(b_, a_ % b_, y_, x_);
  y_ -= a_ / b_ * x_;
  return d_;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    LL a = read(), b = read(), c = read(), x, y;
    LL d = exgcd(a, b, x, y);
    if (c % d != 0) {
      printf("-1\n");
      continue;
    }

    x *= c / d, y *= c / d;
    LL p = b / d, q = a / d, k;
    k = ceil((1.0 - x) / p), x += p * k, y -= q * k;

    if (y > 0) {
      printf("%lld ", (y - 1) / q + 1);
      printf("%lld ", x);
      printf("%lld ", (y - 1) % q + 1);
      printf("%lld ", x + (y - 1) / q * p);
      printf("%lld ", y);
    } else {
      printf("%lld ", x);
      printf("%lld ", y + q * (LL) ceil((1.0 - y) / q));
    }
    printf("\n");
  }
  return 0;
}

数学乱记

懒得往这搬了= =

「笔记」数学乱记

杂项

CDQ 分治

Cdq 分治是一种牛逼思想,一般用于解决区间点对问题。设当前处理的区间为 \([l, r]\),Cdq 分治的一般过程:

  1. \(l = r\),返回。
  2. 设区间中点为 \(mid\),递归处理 \([l,mid]\)\([mid + 1, r]\)
  3. 不横跨 \(mid\) 的点对都会在递归中被解决,仅考虑横跨 \(mid\) 的点对的贡献。

个人理解:
\(O(n^2)\) 级别的点对统计,利用单调性确定合法性,变为 \(O(n\log^k n)\) 级别。
太神了!

P3810 【模板】三维偏序(陌上花开)

//知识点:CDQ分治 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cstdio>
#include <ctype.h>
#include <cstring>
#define ll long long
#define lowbit(x) (x&-x)
#define mid ((l_+r_)>>1)
const int kMaxn = 1e5 + 10;
//=============================================================
struct Data {
  int x, y, z, cnt, ans;
} a[kMaxn];
int n, k, ans[kMaxn], t[kMaxn << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void GetMin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool Compare1(Data fir_, Data sec_) {
  if (fir_.x != sec_.x) return fir_.x < sec_.x;
  if (fir_.y != sec_.y) return fir_.y < sec_.y;
  return fir_.z < sec_.z;
}
bool Compare2(Data fir_, Data sec_) {
  if (fir_.y != sec_.y) return fir_.y < sec_.y;
  return fir_.z < sec_.z;
}
void Add(int pos_, int val_) {
  for (; pos_ <= k; pos_ += lowbit(pos_)) {
    t[pos_] += val_;
  }
}
int Sum(int pos_) {
  int ret = 0;
  for (; pos_; pos_ -= lowbit(pos_)) ret += t[pos_];
  return ret;
}
void Cdq(int l_, int r_) {
  if (l_ == r_) return ;
  Cdq(l_, mid), Cdq(mid + 1, r_);
  std :: sort(a + l_, a + mid + 1, Compare2);
  std :: sort(a + mid + 1, a + r_ + 1, Compare2);
  int i = l_, j = mid + 1;
  for (; j <= r_; ++ j) {
    for (; a[i].y <= a[j].y && i <= mid; ++ i) {
      Add(a[i].z, a[i].cnt);
    }
    a[j].ans += Sum(a[j].z);
  }
  for (j = l_; j < i; ++ j) Add(a[j].z, - a[j].cnt); 
}
bool JudgeEqual(Data fir_, Data sec_) {
  if (fir_.x != sec_.x) return false;
  if (fir_.y != sec_.y) return false;
  return fir_.z == sec_.z;
}
//=============================================================
int main() {
  int tmpn = n = read();
  k = read();
  for (int i = 1; i <= n; ++ i) {
    a[i] = (Data) {read(), read(), read()};
  }
  std :: sort(a + 1, a + n + 1, Compare1);
  n = 0;
  for (int i = 1, cnt = 0; i <= tmpn; ++ i) {
    cnt ++;
    if (! JudgeEqual(a[i], a[i + 1])) {
      a[++ n] = a[i], 
      a[n].cnt = cnt,
      cnt = 0;
    }
  }
  Cdq(1, n);
  for (int i = 1; i <= n; ++ i) {
    ans[a[i].ans + a[i].cnt - 1] += a[i].cnt;
  }
  for (int i = 0; i < tmpn; ++ i) {
    printf("%d\n", ans[i]);
  }
  return 0;
}
posted @ 2020-11-05 11:50  Luckyblock  阅读(533)  评论(7编辑  收藏  举报