通用算法

基础算法

常作为其它算法的基础。

快速乘 & 快速幂

快速乘可避免模数大于 int 取值范围时溢出,可将快速幂中乘法替换为快速乘版本以避免乘法溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
i64 qmul(i64 x, i64 y, i64 m) {
i64 z = (f80) x / m * y + 0.5L;
u64 c = (u64) x * y - (u64) z * m;
return c < m ? (i64) c : (i64) (c + m);
}

i64 qpow(i64 a, i64 n, i64 m) {
i64 res = 1;
while (n) {
if (n & 1) res = res * a % m;
a = a * a % m;
n >>= 1;
}
return res;
}

Barrett 模乘

Barrett 模乘是一种通过预计算除数的倒数来优化模运算的高效算法,用乘法和移位代替除法,在模数编译期未知的情况下可以优化取模(如 Pollard-Rho 算法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Barrett {  
i64 d;
i128 b;

explicit Barrett(i64 d) : d(d), b(((i128) 1 << 64) / d) {
}

friend i64 operator %(i64 a, const Barrett &m) {
i64 w = (m.b * a) >> 64;
w = a - w * m.d;
return w >= m.d ? w - m.d : w;
}

friend i64 operator /(i64 a, const Barrett &d) {
i64 w = (d.b * a) >> 64;
return a - w * d.d >= d.d ? w + 1 : w;
}
};

基姆拉尔森计算公式

以 1970 年 1 月 1 日为星期四为基准,累加日期计算 y 年 m 月 d 日是星期几。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const int ds[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};  

bool is_leap(int y) {
return y % 400 == 0 || (y % 4 == 0 && y % 100 != 0);
}

int get_days(int y, int m) {
return ds[m - 1] + (is_leap(y) && m == 2);
}

int get_week(int y, int m, int d) {
int res = 2;
for (int i = 1970; i < y; i++) res += 365 + is_leap(i);
for (int i = 1; i < m; i++) res += get_days(y, i);
res += d;
return res % 7 + 1;
}

蔡勒公式

基准相同,可以快速计算任意年份,返回 0 代表星期日。

1
2
3
4
5
int get_week(int y, int m, int d)  
{
if (m == 1 || m == 2) m += 12, y--;
return (d + 2 * m + 3 * (m + 1) / 5 + y + y / 4 - y / 100 + y / 400 + 1) % 7;
}

三分算法

整数范围凹函数三分算法,实现了对函数平台区域的处理,若平均平台长度为 k ,则期望复杂度约为 O(klog n)

1
2
3
4
5
6
7
8
9
10
i64 l = 0, r = 1e18, al, ar;
while (l < r) {
i64 ml = (l + r) / 2, mr = ml + 1;
al = f(ml), ar = f(mr);
while (al == ar && ml > l) al = f(--ml);

if (al >= ar) l = mr;
else r = ml;
}
cout << min(al, ar) << endl;

排序算法

排序算法是将数据排列的方法,在基础排序算法的基础上上可以扩展出更多功能。

快速排序

基于随机选取基数的三路快速排序,是不稳定排序,可以在此基础上修改出线性第 k 大算法。

1
2
3
4
5
6
7
8
9
10
11
12
auto qsort = [&](auto &&self, int l, int r) {
if (r - l <= 1) return;
auto pivot = a[l + rnd() % (r - l)];
int i = l, j = l, k = r;
while (i < k) {
if (a[i] < pivot) swap(a[i++], a[j++]);
else if (a[i] > pivot) swap(a[i], a[--k]);
else i++;
}
self(self, l, j);
self(self, k, r);
};

归并排序

基于分治思想将数组分段排序后合并,是稳定排序,可以在此基础上修改用于解决偏序问题其中一维。

1
2
3
4
5
6
7
8
9
10
11
12
13
auto msort = [&](auto self, int l, int r) {
if (r - l <= 1) return;
int m = l + r >> 1;
self(self, l, m), self(self, m, r);
int i = l, j = m, k = l;
while (i < m && j < r) {
if (a[i] <= a[j]) b[k++] = a[i++];
else b[k++] = a[j++];
}
while (i < m) b[k++] = a[i++];
while (j < r) b[k++] = a[j++];
for (i = l; i < r; ++i) a[i] = b[i];
};

搜索算法

搜索,也就是对状态空间进行枚举,通过穷尽所有的可能来找到最优解,或者统计合法解的个数。

精确覆盖问题要求在给定集合中选取若干互不相交的子集,使得目标集合的每个元素恰好被覆盖一次。

Dancing Links X 算法通过高效回溯和双向链表优化搜索,常用于解决数独、 N 皇后、 Pentomino 拼图等组合优化问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
struct DLX {
struct node {
int l = 0, r = 0, u = 0, d = 0, row = -1, col = -1;
};

vector<node> adj;
vector<int> head, sz, ans;

explicit DLX(int r, int c) : adj(c + 1), head(r + 1, -1), sz(c + 1) {
for (int i = 0; i <= c; ++i) {
adj[i].l = i - 1, adj[i].r = i + 1;
adj[i].u = adj[i].d = adj[i].col = i;
}
adj[0].l = c, adj[c].r = 0;
}

void insert(int r, int c) {
int id = (int) adj.size();
adj.emplace_back(0, 0, adj[c].u, c, r, c);
adj[c].u = adj[adj[c].u].d = id;
if (head[r] == -1) {
head[r] = adj[id].l = adj[id].r = id;
} else {
int first = head[r];
int last = adj[first].l;
adj[id].l = last, adj[id].r = first;
adj[last].r = adj[first].l = id;
}
sz[c]++;
}

void remove(int c) {
adj[adj[c].r].l = adj[c].l;
adj[adj[c].l].r = adj[c].r;
for (int i = adj[c].d; i != c; i = adj[i].d) {
for (int j = adj[i].r; j != i; j = adj[j].r) {
adj[adj[j].d].u = adj[j].u;
adj[adj[j].u].d = adj[j].d;
sz[adj[j].col]--;
}
}
}

void restore(int c) {
for (int i = adj[c].u; i != c; i = adj[i].u) {
for (int j = adj[i].l; j != i; j = adj[j].l) {
adj[adj[j].d].u = adj[adj[j].u].d = j;
sz[adj[j].col]++;
}
}
adj[adj[c].r].l = adj[adj[c].l].r = c;
}

bool dance() {
if (adj[0].r == 0) return true;
int c = adj[0].r;
for (int i = adj[c].r; i != 0; i = adj[i].r) {
if (sz[i] < sz[c]) c = i;
}
remove(c);
for (int i = adj[c].d; i != c; i = adj[i].d) {
ans.push_back(adj[i].row);
for (int j = adj[i].r; j != i; j = adj[j].r) remove(adj[j].col);
if (dance()) return true;
for (int j = adj[i].l; j != i; j = adj[j].l) restore(adj[j].col);
ans.pop_back();
}
restore(c);
return false;
}
};

数独问题

使用 Dancing links X 算法高效解决数独问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
array<array<int, 9>, 9> ma{};
auto get = [&](int x, int y, int k)-> array<int, 4> {
int box = x / 3 * 3 + y / 3;
return {
x * 9 + y + 1,
81 + x * 9 + k + 1,
162 + y * 9 + k + 1,
243 + box * 9 + k + 1
};
};

int id = 0;
DLX solver(730, 324);
vector<tuple<int, int, int> > rows;
for (int x = 0; x < 9; ++x) {
for (int y = 0; y < 9; ++y) {
if (ma[x][y] != 0) {
int k = ma[x][y] - 1;
for (int c: get(x, y, k)) solver.insert(id, c);
rows.emplace_back(x, y, k);
id++;
} else {
for (int k = 0; k < 9; ++k) {
for (int c: get(x, y, k)) solver.insert(id, c);
rows.emplace_back(x, y, k);
id++;
}
}
}
}

solver.dance();
for (int rid: solver.ans) {
auto [x, y, k] = rows[rid];
ma[x][y] = k + 1;
}

动态规划算法

具有最优子结构,无后效性和子问题重叠的问题可以使用动态规划解决。

完全背包 DP

定义价值重量比为价值与重量的比值,则第 i 种物品的价值重量比为 $\frac{v_i}{w_i}$

设第 s 种物品为价值重量比最大的那些物品之中重量最小的那种物品。

设使承重量为 W 的背包所能获得最大价值的方案中,有 a 件非第 s 种物品,有 b 件第 s 种物品。

于是转化为有 x 件非第 s 种物品, y 件第 s 种物品,且在这 x 件非第 s 种物品中,不存在若干件物品的重量和为 ws 的倍数。

由鸽笼原理得:x < ws ,故这 x 件物品的重量和最大为 (ws − 1)max(wi), i ≠ s ,不超过 u2

ans = max(dp[w] + ⌊(W − w) ÷ ws⌋ × vs), 1 ≤ w ≤ u2

执行这样的 dp ,可以在 O(u3) 的时间复杂度解决完全背包问题,其中 u 为最大的物品重量。

以下代码提供了简洁的实现,并使用数论方法将时间复杂度优化到 O(u2log u)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> id(n);  
iota(id.begin(), id.end(), 0);
ranges::sort(id, [&](int x, int y) {
return (i64) v[x] * w[y] > (i64) v[y] * w[x];
});

vector<i64> dp(min(W, u * u) + 1);
for (int i = 1; i <= min(W, u * u); ++i) {
for (int j = 0; j < min((3 * u * u + i - 1) / i, n); ++j) {
if (w[id[j]] <= i) dp[i] = max(dp[i], dp[i - w[id[j]]] + v[id[j]]);
}
}
i64 m = (max(0, W - u * u) + w[id[0]] - 1) / w[id[0]];
i64 ans = dp[W - m * w[id[0]]] + m * v[id[0]];

混合背包 DP

将背包按对 w[i] 取模的余数分组,转移时同一组价值差满足等差数列,且每组之间的状态转移不互相影响,因此使 val = dp[k * w[i] + j] − k * v[i] ,每个容量的背包从 val 最大值转移,且 d[i 固定,可以使用单调队列优化,时间复杂度 O(nW)

下列代码中 d 为每个物品可被选择的次数,设为 inf 以选择无穷次。

1
2
3
4
5
6
7
8
9
10
11
12
13
vector<int> dp(W + 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < w[i]; j++) {
deque<info> deq;
for (int k = 0; k * w[i] + j <= W; k++) {
int val = dp[k * w[i] + j] - k * v[i];
while (!deq.empty() && val > deq.back().val) deq.pop_back();
deq.emplace_back(k, val);
if (deq.front().k < k - d[i]) deq.pop_front();
dp[k * w[i] + j] = deq.front().val + k * v[i];
}
}
}

状压 DP

状压 DP 通过将状态压缩为整数来达到优化转移的目的。

旅行商问题

旅行商问题求经过每个点的一次或多次的最短回路或通路,是组合优化中的一个 NPH 问题。

dp[i][j] 表示经过状态 i 所表示的点,到达 j 点的最短通路,经过恰当的初始化和后处理可以解决各类旅行商问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
array<int, 20> val;
val.fill(inf);
vector<array<int, 20> > dp(1 << n, val);
for (int i = 0; i < n; ++i) dp[1 << i][i] = 0;
for (int i = 0; i < 1 << n; ++i) {
for (int j = 0; j < n; ++j) {
if (dp[i][j] == inf) continue;
for (int k = 0; k < n; ++k) {
if (i & 1 << k) continue;
dp[i + (1 << k)][k] = min(dp[i + (1 << k)][k], dp[i][j] + maxd[j][k]);
}
}
}

轮廓线 DP

给定一个 n × m 的矩形网格,计算用 1 × 2 多米诺骨牌完全平铺该网格的方法数。

dp[i][j][s] 表示第 i 行第 j 列的轮廓线状态为 s 时的方案数,逐格进行 DP,但记录的状态是各行内的轮廓线上格子的覆盖情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (n < m) swap(n, m);  
vector<i64> dp(1 << m);
dp[(1 << m) - 1] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
vector<i64> ndp(1 << m);
for (int k = 0; k < 1 << m; k++) {
if (k & 1 << j) ndp[k ^ 1 << j] += dp[k];
if (j && !(k & 1 << j - 1) && (k & 1 << j)) ndp[k | 1 << j - 1] += dp[k];
if (i && !(k & 1 << j)) ndp[k | 1 << j] += dp[k];
}
dp = std::move(ndp);
}
}
cout << dp[(1 << m) - 1] << '\n';

插头 DP

给出 n × m 的矩形网格,有些格子不能铺线,其它格子必须铺,求形成一个闭合回路的方案数。

由于每合并一组连通的插头,就会生成一条独立的回路,因而在本题中,我们还需要区分插头之间的连通性。

插头 DP 在轮廓线 DP 的基础上,引入最小表示法表示轮廓线上每个插头所属连通块信息,对状态进行额外编码,解决带连通性的状压 DP 问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
struct HashTable {  
static constexpr int maxs = 200006, pr = 100003;

int sz;
vector<int> hd, nxt;
vector<i64> st, val;

HashTable() : sz(0), hd(pr, -1), nxt(maxs, -1), st(maxs), val(maxs) {
}

void clear() {
sz = 0;
fill(hd.begin(), hd.end(), -1);
fill(nxt.begin(), nxt.end(), -1);
}

void push(i64 s, i64 v) {
int x = s % pr;
for (int i = hd[x]; ~i; i = nxt[i]) {
if (st[i] == s) {
val[i] += v;
return;
}
}
st[sz] = s;
val[sz] = v;
nxt[sz] = hd[x];
hd[x] = sz++;
}

void roll() {
for (int i = 0; i < sz; i++) st[i] <<= offs;
}
};

i64 encode(const vector<int> &b, int m) {
vector<int> bb(m + 1, -1);
bb[0] = 0;
i64 s = 0;
for (int i = m, bn = 1; i >= 0; i--) {
if (bb[b[i]] == -1) bb[b[i]] = bn++;
s = s << offs | bb[b[i]];
}
return s;
}

vector<int> decode(i64 s, int m) {
vector<int> b(m + 1);
for (int i = 0; i < m + 1; i++) {
b[i] = s & mask;
s >>= offs;
}
return b;
}

void solve() {
int n, m;
cin >> n >> m;
vector<string> mp(n);
for (int i = 0; i < n; i++) cin >> mp[i];

if (m > n) {
vector<string> nmp(m, string(n, '\0'));
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
nmp[j][i] = mp[i][j];
swap(n, m);
mp = move(nmp);
}

int ex = 0, ey = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (mp[i][j] != '*') ex = i, ey = j;

vector<HashTable> H(2);
auto dp = &H[0], ndp = &H[1];
dp->push(0, 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
ndp->clear();
for (int idx = 0; idx < dp->sz; idx++) {
i64 s = dp->st[idx], v = dp->val[idx];

vector<int> b = decode(s, m);
int lt = b[j], up = b[j + 1];
bool dn = i != n - 1 && mp[i + 1][j] != '*';
bool rt = j != m - 1 && mp[i][j + 1] != '*';
auto push = [&](int dval, int rval) {
vector<int> nb = b;
nb[j] = dval, nb[j + 1] = rval;
ndp->push(encode(nb, m), v);
};

if (mp[i][j] == '*') {
if (!lt && !up) push(0, 0);
} else if (lt && up) {
if (lt == up) {
if (i == ex && j == ey) push(0, 0);
} else {
for (auto &val: b)
if (val == lt) val = up;
push(0, 0);
}
} else if (lt || up) {
int t = lt | up;
if (dn) push(t, 0);
if (rt) push(0, t);
} else {
if (dn && rt) push(m, m);
}
}
swap(dp, ndp);
if (i == ex && j == ey) break;
}
if (i == ex) break;
dp->roll();
}

assert(dp->sz <= 1);
if (dp->sz == 1) cout << dp->val[0] << '\n';
else cout << 0 << '\n';
}

数位 DP

数位 DP 主要用于解决 “在区间范围内,满足某种约束的数字的数量、总和、平方” 这一类问题。

状态(形参)设置

以下为记忆化搜索函数常设定的形参。

  • pos: int 型变量,表示当前枚举的位置,一般从高到低
  • limit: bool 型变量,表示枚举的第 pos 位是否受到限制
    • 为 true 表示取的数不能大于 a[pos],而只有在 [pos + 1, len] 的位置该值才为 ture
    • 否则表示当前位没有限制,可以取到 [0, R - 1] ,因为 R 进制的数中最大能取到的就是 R - 1
  • last: int 型变量,表示上一位(第 pos + 1 位)填写的值
    • 往往用于约束了相邻数位之间的关系的题目,可以存 -1 以代替 lead0 状态
  • lead0: int 型变量,表示是否有前导零,即在 [pos + 1, len] 这些位置是不是都是前导零
    • 基于常识,我们往往默认一个数没有前导零,也就是最高位不能为 0 ,即不会写为 000123 而是 123
    • 只有没有前导零的时候,才能计算 0 的贡献
    • 以下常见情况需要考虑处理前导零
      • 统计 0 的出现次数
      • 相邻数字的差值
      • 以最高位为起点确定的位数
  • sum : int 型变量,表示当前 [pos + 1, len] 的数位和或数字出现个数
  • r : int 型变量,表示整个数前缀取模某个数 p 的余数
    • 该参数一般会用在:约束中出现了能被 p 整除
    • 当然也可以拓展为数位和取模的结果
  • st : int 型变量,用于状态压缩
    • 对一个集合的数在数位上的出现次数的奇偶性有要求时,其二进制形式就可以表示每个数出现的奇偶性
    • 通过四进制可以同时保存数字是否出现以及奇偶信息,且奇偶互换为对应位取反

数位 k 的出现次数

下面展示了一个查找 [0, x] 范围内不计前导 0 的数字中数位为 k 的数位个数的记忆化搜索实现。

limit 和 lead0 参数为 true 所确定的子问题不会出现重复且至多出现 a.size() 次,因此无需记忆化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto get = [&](int x, int k) {
vector<int> a{0};
while (x) a.push_back(x % 10), x /= 10;
vector mem(a.size(), vector<int>(a.size(), -1));
auto dfs = [&](auto &&self, int pos, bool limit, bool lead0, int sum) -> int {
if (!pos) return sum;
if (!limit && !lead0 && ~mem[pos][sum]) return mem[pos][sum];
int res = 0;
int up = limit ? a[pos] : 9;
for (int i = 0; i <= up; i++) {
int c0 = lead0 && i == 0;
res += self(self, pos - 1, limit && i == up, c0, c0 ? 0 : sum + (i == k));
}
if (!limit && !lead0) mem[pos][sum] = res;
return res;
};
return dfs(dfs, a.size() - 1, true, true, 0);
};

离线算法

算法在求解问题已具有与该问题相关的完全信息。

CDQ 分治

类似归并排序的思想,先按第一维属性划分为前一半和后一半,合并时计算前一半区间对后一半区间的影响。

三维偏序

一维排序,二维 CDQ 分治 ,三维树状数组,包含去重代码。

第一维相等时,后续维可以按相反弱序关系排序,以处理第一维非严格偏序问题。

当三维均不存在严格偏序关系时,答案贡献关系成环,不存在一种正确的更新顺序,此时需要先去重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int s = 0, c = 0;
for (int f = 0; f < n; c += a[f++].c) {
if (a[s].x == a[f].x && a[s].y == a[f].y && a[s].z == a[f].z) continue;
a[s].c = c, a[s].a = c - 1;
a[++s] = a[f], c = 0;
}
a[s].c = c, a[s].a = c - 1;
a.resize(++s);

Fenwick<int> bit(k + 1);
auto cdq = [&](auto &&self, int l, int r) -> void {
if (r - l <= 1) return;
int m = l + r >> 1;
self(self, l, m), self(self, m, r);
int i = l, j = m;
while (j < r) {
while (i < m && a[i].y <= a[j].y) bit.add(a[i].z, a[i].c), i++;
a[j].a += bit.sum(a[j].z), j++;
}
for (j = l; j < i; j++) bit.add(a[j].z, -a[j].c);
inplace_merge(a.begin() + l, a.begin() + m, a.begin() + r, cmpy);
};
cdq(cdq, 0, s);

整体二分

整体二分是一种离线分治算法,通过将多个查询或操作批量处理并递归地将问题划分为子问题,结合二分思想高效解决带有批量询问和更新的复杂问题。

带修区间第 k 小

下列代码使用整体二分的方法,通过将修改和查询抽象成操作序列,解决了了带修区间第 k 小问题,时间复杂度 O(nlog2n)

  • Q l r k 表示查询下标在区间 [l, r] 中的第 k 小的数
  • C x y 表示将 ax 改为 y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct info {
char op;
int l, r, k, x, y, v, i;
};

vector<int> ans(m);
Fenwick<int> f(n + 1);
auto qsort = [&](auto &&self, int l, int r, vector<info> &cq) {
if (r - l == 1) {
for (auto &i: cq) {
if (i.op == 'Q') ans[i.i] = l;
}
return;
}
int mid = (l + r) / 2;
vector<info> q1, q2;
for (auto i: cq) {
if (i.op == 'Q') {
int cnt = f.sum(i.r) - f.sum(i.l - 1);
if (cnt >= i.k) q1.push_back(i);
else q2.push_back(i), q2.back().k -= cnt;
} else {
if (i.y < mid) f.add(i.x, i.v), q1.push_back(i);
else q2.push_back(i);
}
}
for (auto i: cq) {
if (i.op == 'C' && i.y < mid) f.add(i.x, -i.v);
}
if (!q1.empty()) self(self, l, mid, q1);
if (!q2.empty()) self(self, mid, r, q2);
};

静态序列优化

维护一个指针 pos 追踪每轮划分的 mid,对于绝大多数可以用整体二分解决并且不带修改的问题,都可以应用此种优化以大幅降低数据结构的使用次数,注意不需要对询问做出修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct info {  
int l, r, k, i;
};

struct num {
int v, i;
};

int pos = 0;
vector<int> ans(m);
Fenwick<int> f(n + 1);
ranges::sort(a, [](auto x, auto y) { return x.v < y.v; });
auto qsort = [&](auto &&self, int l, int r, const vector<info> &cq) {
if (r - l == 1) {
for (auto &i: cq) ans[i.i] = l;
return;
}
int mid = l + (r - l >> 1);
while (pos < n && a[pos].v < mid) f.add(a[pos++].i, 1);
while (pos > 0 && a[pos - 1].v >= mid) f.add(a[--pos].i, -1);
vector<info> q1, q2;
for (auto i: cq) {
int cnt = f.sum(i.r) - f.sum(i.l - 1);
if (cnt >= i.k) q1.push_back(i);
else q2.push_back(i);
}
if (!q1.empty()) self(self, l, mid, q1);
if (!q2.empty()) self(self, mid, r, q2);
};

莫队算法

莫队算法是分块思想在查询上的应用,可以解决一类离线区间询问问题。

莫队算法的最优块长与更新块操作的效率有关,属于玄学,因此复杂度分析仅供图一乐。

普通莫队算法

若扩展到相邻区间操作复杂度 O(a) ,转移完毕后计算区间答案复杂度 O(b),块长 s 为 $\frac{n}{\sqrt{q}}$ ,则有最优总复杂度 $O(a*n\sqrt{q} + b * q)$

常用莫队维护权值块状数组,此时总复杂度 $O(n\sqrt{q})$

1
2
3
4
5
6
7
8
9
10
11
12
13
int len = (int) (n / sqrt(m)); 
ranges::sort(qs, [&](auto x, auto y) {
if (x.l / len != y.l / len) return x.l < y.l;
return x.l / len & 1 ? x.r > y.r : x.r < y.r;
});

for (auto &q: qs) {
while (l > q.l) move(--l, 1);
while (r < q.r) move(r++, 1);
while (l < q.l) move(l++, -1);
while (r > q.r) move(--r, -1);
q.ans = query();
}

回滚莫队

回滚莫队仅在端点扩展时添加元素,通过回滚(撤销)操作避免删除,最坏时间复杂度与普通莫队算法相同,但是回滚莫队缺乏优化,复杂度更紧。

下列代码使用回滚莫队的方法,解决了区间带权众数问题,时间复杂度 $O(n\sqrt{q})$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int len = (int) (n / sqrt(m));  
ranges::sort(qs, [&](auto x, auto y) {
if (x.l / len != y.l / len) return x.l < y.l;
return x.r < y.r; // 注意不能使用奇偶优化
});

i64 ans = 0;
vector<int> cnt(cv.size()), cnt1(cv.size());
auto move = [&](int x) {
ans = max(ans, (i64) ++cnt[b[x]] * a[x]);
};

int l = -1e9, r = 0;
for (auto &q: qs) {
if (q.l / len == q.r / len) { // 暴力回答问题
for (int i = q.l; i < q.r; i++) cnt1[b[i]] = 0;
for (int i = q.l; i < q.r; i++) {
q.ans = max(q.ans, (i64) ++cnt1[b[i]] * a[i]);
}
continue;
}
if (q.l / len != l / len - 1) { // 初始化左右端点
ans = 0;
cnt.assign(cv.size(), 0);
l = r = (q.l / len + 1) * len;
}
while (r < q.r) move(r++); // 扩展右端点
i64 tmp = ans; // 记录信息
while (l > q.l) move(--l); // 扩展左端点
while (l < (q.l / len + 1) * len) cnt[b[l++]]--;
q.ans = ans, ans = tmp; // 回滚信息
}

带修改莫队

普通莫队是不能带修改的,但可以强行加上一维时间维 t 表示经历的修改次数,变成三维莫队问题。

即把询问 [l, r] 变成 [l, r, t],那么我们的坐标也可以在时间维上移动。

若三个维度数量级分别为 x, y, z,查询次数为 q ,块长为 s ,则复杂度为 $O(\frac{xyz}{s^{2}}+qs)$,求导得块长 s$\dfrac{x^{1/3}y^{1/3}z^{1/3}}{q^{1/3}}$ 时有最优复杂度 O(x1/3y1/3z1/3q2/3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const int len = (int) pow(n, 0.67);
ranges::sort(qs, [&](auto &x, auto &y) {
if (x.l / len != y.l / len) return x.l < y.l;
if (x.r / len != y.r / len) return x.l / len & 1 ? x.r > y.r : x.r < y.r;
return x.r / len & 1 ? x.t < y.t : x.t > y.t;
});

auto movet = [&](int i, int val) {
auto [x, u, v] = ms[i];
if (val == -1) swap(u, v);
a[x] = v;
if (x < l || x >= r) return;
cnt[u]--, b[bk[u]]--;
cnt[v]++, b[bk[v]]++;
};

for (auto &q: qs) {
while (l > q.l) movelr(--l, 1);
while (r < q.r) movelr(r++, 1);
while (l < q.l) movelr(l++, -1);
while (r > q.r) movelr(--r, -1);
while (t < q.t) movet(++t, 1);
while (t > q.t) movet(t--, -1);
q.ans = query();
}

莫队二次离线

通过将莫队算法中的指针移动产生的询问二次离线处理,利用预处理信息优化时间复杂度。

适用于指针移动代价高的统计问题,先按莫队排序处理查询并记录指针移动的贡献,再离线批量计算这些贡献,关键点包括贡献拆分、预处理前缀信息与高效离线查询。

下列代码是区间逆序对的实现,当 q = O(n) 时,总复杂度为 $O(n\sqrt{n})$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int cl = 1, cr = 0;
vector<queryl> qsl;
vector<queryr> qsr;
for (int i = 0; i < m; i++) {
auto [_,ql, qr, qi] = qs[i];
if (cl > ql) qsl.emplace_back(1, cr, ql, cl - 1, i), cl = ql;
if (cr < qr) qsr.emplace_back(1, cl, cr + 1, qr, i), cr = qr;
if (cl < ql) qsl.emplace_back(-1, cr, cl, ql - 1, i), cl = ql;
if (cr > qr) qsr.emplace_back(-1, cl, qr + 1, cr, i), cr = qr;
}
ranges::sort(qsl, [](auto &x, auto &y) { return x.r > y.r; });
ranges::sort(qsr, [](auto &x, auto &y) { return x.l < y.l; });

cl = 0, cr = n + 1;
Block<int> bl(maxv + 1), br(maxv + 1);
for (auto &[op, ql, rs, re, qi]: qsr) {
while (cl < ql - 1) br.add(maxv + 1 - a[++cl], 1);
for (int i = rs; i <= re; i++) {
qs[qi].ans += (f[i] - br.sum(maxv - a[i])) * op;
}
}
for (auto &[op, qr, ls, le, qi]: qsl) {
while (cr > qr + 1) bl.add(a[--cr], 1);
for (int i = ls; i <= le; i++) {
qs[qi].ans += (g[i] - bl.sum(a[i] - 1)) * op;
}
}