分治杂记

分治杂记

分治(Divide and Conquer),就是把一个复杂的问题分成若干子问题,分别求解。本质是缩小了问题的规模。

普通的分治

[ABC373G] No Cross Matching

给定平面上的 \(n\) 个黑点和 \(n\) 个白点,构造一种方案,将黑白点两两匹配并连线段,使得任意两条线段不相交。

\(n \leq 100\) ,保证无三点共线,保证有解

找到最左下角的黑点,找到一个白点使得这两个点连线的两边内黑白点数量分别相等,然后分治即可。

找白点可以用极角排序,时间复杂度 \(O(n^2 \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e2 + 7;struct Point {int x, y, id;
} p[N << 1];int match[N];int n;void solve(int l, int r) {if (l > r)return;sort(p + l, p + r + 1, [](const Point &a, const Point &b) { return a.x < b.x; });for (int i = l + 1; i <= r; ++i)p[i].x -= p[l].x, p[i].y -= p[l].y;sort(p + l + 1, p + r + 1, [](const Point &a, const Point &b) {return atan2(a.x, a.y) > atan2(b.x, b.y);});for (int i = l + 1, cnt = 0; i <= r; cnt += (p[i++].id <= n ? 1 : -1))if (!cnt && ((p[i].id <= n) ^ (p[l].id <= n))) {match[min(p[l].id, p[i].id)] = max(p[l].id, p[i].id);solve(l + 1, i - 1), solve(i + 1, r);return;}
}signed main() {scanf("%d", &n);for (int i = 1; i <= n * 2; ++i)scanf("%d%d", &p[i].x, &p[i].y), p[i].id = i;solve(1, n * 2);for (int i = 1; i <= n; ++i)printf("%d ", match[i] - n);return 0;
}

一维点对

主要就是讨论点对分别在左右区间产生的贡献。

CF459D Pashmak and Parmida's problem

给定 \(a_{1 \sim n}\) ,设 \(f(l, r, x)\) 表示 \(x\)\(a_{l \sim r}\) 中的出现次数。求有多少对 \(i < j\) 满足 \(f(1, i, a_i) > f(j, n, a_j)\)

\(n \leq 10^6\)

预处理 \(f(1, i, a_i)\)\(f(i, n, a_i)\) 后就变为逆序对问题,可以归并做到 \(O(n \log n)\) ,比 DS 好写多了。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;map<int, int> mp;int a[N], pre[N], suf[N];ll ans;
int n;void solve(int l, int r) {if (l == r)return;int mid = (l + r) >> 1;solve(l, mid), solve(mid + 1, r);for (int i = l, j = mid; i <= mid; ++i) {while (j < r && pre[i] > suf[j + 1])++j;ans += j - mid;}inplace_merge(pre + l, pre + mid + 1, pre + r + 1);inplace_merge(suf + l, suf + mid + 1, suf + r + 1);
}signed main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d", a + i), pre[i] = ++mp[a[i]];mp.clear();for (int i = n; i; --i)suf[i] = ++mp[a[i]];solve(1, n);printf("%lld", ans);return 0;
}

友好点对

给出二维平面上的 \(n\) 个不同点。若存在一个矩形只包含两个点(包括边界),则称这两个点为友好点对。求友好点对的数量。

\(n \leq 10^5\)

不难发现若两个点是友好点对,则最优的矩形一定是使得这两个点在对角线两端。

考虑按横坐标分治。以左下-右上为例,左上-右下是对称的。处理跨过中点的贡献时,固定左侧的某个点,则右侧能选择的点是一个上升序列。分别对左右区间维护单调栈即可。

P7883 平面最近点对(加强加强版)

给定 \(n\) 个二维平面上的点,求最近点对距离的平方值。

\(n \leq 4 \times 10^5\)

考虑按 \(x\) 坐标分治,记分治的分界线为 \(mid\) ,左右区间内部的最近点对距离的较小值为 \(d\)

考虑处理左右区间之间对答案的贡献,首先发现一个点 \((x, y)\) 满足 \(|x - mid| \leq d\) 时才有可能更新答案。将这些点拿出来,并按 \(y\) 坐标排序。对于其中的一个点 \((x, y)\) ,可能更新答案的点 \((x', y')\) 必然满足 \(|y - y'| \leq d\) ,那么只要找到附近满足该条件的点更新即可。直觉可以发现这样的点是常数级别的,不然内部就可以更新了。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 4e5 + 7;struct Point {int x, y;
} p[N], tmp[N];int n;inline ll dist(const Point &a, const Point &b) {return 1ll * (a.x - b.x) * (a.x - b.x) + 1ll * (a.y - b.y) * (a.y - b.y);
}inline ll solve(int l, int r) {if (l == r)return inf;int mid = (l + r) >> 1, midx = p[mid].x;ll d = min(solve(l, mid), solve(mid + 1, r));inplace_merge(p + l, p + mid + 1, p + r + 1, [](const Point &a, const Point &b) { return a.y < b.y; });int len = 0;for (int i = l; i <= r; ++i)if (1ll * (midx - p[i].x) * (midx - p[i].x) < d)tmp[++len] = p[i];for (int i = 1; i <= len; ++i)for (int j = i + 1; j <= len && 1ll * (tmp[j].y - tmp[i].y) * (tmp[j].y - tmp[i].y) < d; ++j)d = min(d, dist(tmp[i], tmp[j]));return d;
}signed main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d%d", &p[i].x, &p[i].y);stable_sort(p + 1, p + 1 + n, [](const Point &a, const Point &b) { return a.x < b.x; });printf("%lld", solve(1, n));return 0;
}

CF429D Tricky Function

给定 \(a_{1 \sim n}\) ,令 \(f(i, j) = (i - j)^2 + g^2(i, j)\) ,其中 \(g(i, j) = \sum_{k = \min(i, j) + 1}^{\max(i, j)} a_k\) ,求 \(\min_{1 \leq i < j \leq n} f(i, j)\)

\(n \leq 10^5\)

首先可以发现 \(f(i, j) = f(j, i)\) ,钦定 \(j < i\) ,则 \(g(i, j) = s_i - s_j\) ,其中 \(s\) 为前缀和,因此 \(f(i, j) = (i - j)^2 + (s_i - s_j)^2\)

发现这个形式很像两点距离公式,问题转化为给定平面上 \(n\) 个点 \((i, s_i)\) ,求最近点对。不难用分治做到 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7;struct Point {int x, y;
} p[N], tmp[N];int n;inline ll dist(const Point &a, const Point &b) {return 1ll * (a.x - b.x) * (a.x - b.x) + 1ll * (a.y - b.y) * (a.y - b.y);
}inline ll solve(int l, int r) {if (l == r)return inf;int mid = (l + r) >> 1, midx = p[mid].x;ll d = min(solve(l, mid), solve(mid + 1, r));inplace_merge(p + l, p + mid + 1, p + r + 1, [](const Point &a, const Point &b) { return a.y < b.y; });int len = 0;for (int i = l; i <= r; ++i)if (1ll * (midx - p[i].x) * (midx - p[i].x) < d)tmp[++len] = p[i];for (int i = 1; i <= len; ++i)for (int j = i + 1; j <= len && 1ll * (tmp[j].y - tmp[i].y) * (tmp[j].y - tmp[i].y) < d; ++j)d = min(d, dist(tmp[i], tmp[j]));return d;
}signed main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d", &p[i].y), p[i].x = i, p[i].y += p[i - 1].y;printf("%lld", solve(1, n));return 0;
}

Xor

给定 \(a_{0 \sim 2^n - 1}\)\(b_{0 \sim n}\) ,令 \(c_i = \sum_{j = 0}^{2^n - 1} a_j \times b_{\operatorname{popcount}(i \oplus j)}\) ,求 \(c_{0 \sim 2^n - 1}\)

\(n \leq 18\)

考虑按二进制位分治。设当前分治区间为 \([l, r]\) ,深度为 \(d\) ,其中 \([l, mid]\) 的第 \(d\) 为均为 \(0\)\([mid + 1, r]\) 的第 \(d\) 位均为 \(1\)

\(f_{i, j}\) 表示分治区间内与 \(i\)\(j\) 位二进制不同的数的和,不难合并 \([l, mid]\)\([mid + 1, r]\)\(f\)

时间复杂度应该是 \(O(n^2 2^n)\) 的。

基于中点的序列分治

主要就是讨论跨过中点的区间的贡献,一般的维护信息方式是从中间向两边扩展。

CF549F Yura and Developers

给定数组 \(a_{1 \sim n}\) 以及模数 \(k\) ,求满足以下条件的区间 \([l, r]\) 的数量:

  • \(r - l + 1 \geq 2\)
  • \(\sum_{i = l}^r a_i \equiv \max_{i = l}^r a_i \pmod{k}\)

\(n \leq 3 \times 10^5\)\(k \leq 10^6\)

考虑分治,讨论如何计算跨过中点的贡献。先求出以 \(i\) 为左端点且区间最大值出现在左区间的所有右端点的答案,可以用指针维护可行右端点的区间。那么得到 \(s_r \equiv mx + s_{i - 1}\) ,直接用桶存一下即可。

右区间同理,注意不能统计左区间也有最大值的情况,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e5 + 7, K = 1e6 + 7;int a[N], s[N], cnt[K];ll ans;
int n, k;void solve(int l, int r) {if (l == r)return;int mid = (l + r) >> 1;solve(l, mid), solve(mid + 1, r);int j = mid + 1;for (int i = mid, mx = a[i]; i >= l; mx = max(mx, a[--i])) {for (; j <= r && a[j] <= mx; ++j)++cnt[s[j]];ans += cnt[(s[i - 1] + mx) % k];}for (--j; j > mid; --j)--cnt[s[j]];for (int i = mid + 1, mx = a[i]; i <= r; mx = max(mx, a[++i])) {for (; j >= l && a[j] < mx; --j)++cnt[s[j - 1]];ans += cnt[(s[i] - mx % k + k) % k];}for (++j; j <= mid; ++j)--cnt[s[j - 1]];
}signed main() {scanf("%d%d", &n, &k);for (int i = 1; i <= n; ++i)scanf("%d", a + i), s[i] = (s[i - 1] + a[i]) % k;solve(1, n);printf("%lld", ans);return 0;
}

P8317 [FOI2021] 幸运区间

给出长度为 \(n\) 的序列,每个位置上有 \(d\) 个数。需要选出 \(k\) 个数。

如果一个区间内每个位置上的 \(d\) 个数至少有一个出现在选出的 \(k\) 个数中,则是一个幸运区间。

求最长的幸运区间。

\(n \leq 10^5\)\(d \leq 4\)\(k \leq 3\)

考虑分治,每次从中点开始,暴力向两端扩展,不能扩展时加入一个数继续扩展。由于 \(d, k\) 都很小,加入的数直接暴搜即可,时间复杂度 \(O(nd \log n + nd^k)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, D = 5;int a[N][D];
bool chose[N];int n, d, k, ansl, ansr;void dfs(const int L, const int R, int cnt, int l, int r) {while (l > L) {bool flag = false;for (int i = 1; i <= d; ++i)flag |= chose[a[l - 1][i]];if (flag)--l;elsebreak;}while (r < R) {bool flag = false;for (int i = 1; i <= d; ++i)flag |= chose[a[r + 1][i]];if (flag)++r;elsebreak;}if (r - l + 1 == ansr - ansl + 1 ? l < ansl : r - l + 1 > ansr - ansl + 1)ansl = l, ansr = r;if (cnt == k)return;if (l > L) {for (int i = 1; i <= d; ++i)chose[a[l - 1][i]] = true, dfs(L, R, cnt + 1, l - 1, r), chose[a[l - 1][i]] = false;}if (r < R) {for (int i = 1; i <= d; ++i)chose[a[r + 1][i]] = true, dfs(L, R, cnt + 1, l, r + 1), chose[a[r + 1][i]] = false;}
}void solve(int l, int r) {if (l > r)return;int mid = (l + r) >> 1;solve(l, mid - 1), solve(mid + 1, r);for (int i = 1; i <= d; ++i)chose[a[mid][i]] = true, dfs(l, r, 1, mid, mid), chose[a[mid][i]] = false;
}signed main() {int T;scanf("%d", &T);for (int task = 1; task <= T; ++task) {scanf("%d%d%d", &n, &d, &k);for (int i = 1; i <= n; ++i)for (int j = 1; j <= d; ++j)scanf("%d", a[i] + j);ansl = ansr = n + 1, solve(1, n);printf("Case #%d: %d %d\n", task, ansl - 1, ansr - 1);}return 0;
}

CF526F Pudding Monsters

给定一个 \(n \times n\) 的棋盘,其中有 \(n\) 个棋子,每行每列恰好有一个棋子。

对于所有的 \(1 \leq k \leq n\),求有多少个 \(k \times k\) 的子棋盘中恰好有 \(k\) 个棋子。

\(n \le 3 \times 10^5\)

首先转化为统计值域连续区间的数量,即有多少区间满足 \(\max - \min = r - l\)

考虑直接分治:

  • 最大最小值均在左半段:\(mx - mn = j - i \Rightarrow j = i + mx - mn\)
  • 最小值在左半段,最大值在右半段:\(mx_j - mn_i = j - i \Rightarrow mn_i - i = mx_j - j\)
  • 最大最小值均在右半段:与上面对称。
  • 最大值在左半段,最小值在右半段:与上面对称。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e5 + 7;int a[N], mx[N], mn[N], cnt[N << 1];ll ans;
int n;void solve(int l, int r) {if (l == r) {++ans;return;}int mid = (l + r) >> 1;solve(l, mid), solve(mid + 1, r);mn[mid] = mx[mid] = a[mid];for (int i = mid - 1; i >= l; --i)mn[i] = min(mn[i + 1], a[i]), mx[i] = max(mx[i + 1], a[i]);mn[mid + 1] = mx[mid + 1] = a[mid + 1];for (int i = mid + 2; i <= r; ++i)mn[i] = min(mn[i - 1], a[i]), mx[i] = max(mx[i - 1], a[i]);for (int i = mid; i >= l; --i) {int j = i + mx[i] - mn[i];if (mid + 1 <= j && j <= r && mn[i] < mn[j] && mx[j] < mx[i])++ans;} // min in left, max in leftfor (int j = mid + 1; j <= r; ++j) {int i = j - mx[j] + mn[j];if (l <= i && i <= mid && mn[j] < mn[i] && mx[i] < mx[j])++ans;} // min in right, max in rightfor (int i = mid, k = mid + 1, j = mid + 1; i >= l; --i) {for (; j <= r && mn[j] > mn[i]; ++j)++cnt[mx[j] - j + N];for (; k < j && mx[k] < mx[i]; ++k)--cnt[mx[k] - k + N];ans += cnt[mn[i] - i + N];} // min in left, max in rightfor (int i = mid + 1; i <= r; ++i)cnt[mx[i] - i + N] = 0;for (int j = mid + 1, k = mid, i = mid; j <= r; ++j) {for (; i >= l && mn[i] > mn[j]; --i)++cnt[mx[i] + i];for (; k > i && mx[k] < mx[j]; --k)--cnt[mx[k] + k];ans += cnt[mn[j] + j];} // min in right, max in leftfor (int i = l; i <= mid; ++i)cnt[mx[i] + i] = 0;
}signed main() {scanf("%d", &n);for (int i = 1; i <= n; ++i) {int x, y;scanf("%d%d", &x, &y);a[x] = y;}solve(1, n);printf("%lld", ans);return 0;
}

基于断点的序列分治

断点:当前分治区间内合法子区间一定不包含的点。

主要思想就是每次按断点将区间分治处理,若没有端点则该区间合法。

寻找断点常用中途相遇法,找一个目标位置时可以从两边开始找,复杂度分析和启发式分裂是一样的。

若还要维护每个点的贡献,则可以钦定处理当前区间时仅存在当前区间的贡献,并在回溯时删除。

每次先删除断点和小区间的贡献,再递归大区间,再加入小区间的贡献,再递归小区间。

值得一提的是这样维护可以支持下标的大小关系不改变。

UVA1608 不无聊的序列 Non-boring sequences

给定 \(a_{1 \sim n}\) ,判断是否存在一个区间,其不存在唯一元素。

\(n \leq 2 \times 10^5\)

考虑分治,每次用唯一元素的位置将其分为两个区间。

采用中途相遇法,从两端同时开始找,若前驱后继都不在该区间内,则其是该区间的唯一元素。

复杂度分析和启发式分裂的复杂度一样,为 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;int a[N], lst[N], nxt[N];int n;bool solve(int l, int r) {if (l >= r)return false;int pl = l, pr = r, mid = -1;for (int pl = l, pr = r; pl <= pr && mid == -1; ++pl, --pr) {if (lst[pl] < l && nxt[pl] > r)mid = pl;else if (lst[pr] < l && nxt[pr] > r)mid = pr;}return ~mid ? solve(l, mid - 1) || solve(mid + 1, r) : true;
}signed main() {int T;scanf("%d", &T);while (T--) {scanf("%d", &n);map<int, int> mp;for (int i = 1; i <= n; ++i) {scanf("%d", a + i);lst[i] = (mp.find(a[i]) == mp.end() ? 0 : mp[a[i]]), mp[a[i]] = i;}mp.clear();for (int i = n; i; --i)nxt[i] = (mp.find(a[i]) == mp.end() ? n + 1 : mp[a[i]]), mp[a[i]] = i;puts(solve(1, n) ? "boring" : "non-boring");}return 0;
}

金牌歌手

给定序列 \(a_{1 \sim n}, b_{1 \sim n}\) ,其中 \(b\) 单调不升。求最长区间 \([l, r]\) ,满足区间内的任意元素在区间内的出现次数均 \(\geq b_{r - l + 1}\)

\(n \leq 10^6\)

由于 \(b\) 单调不升,若一个数字使得 \([l, r]\) 不合法,那么 \([l, r]\) 内所有包含该数字的子区间必然不合法。

考虑分治,每次找到一个不合法的位置将区间分为两个部分,问题转化为求当前分治区间内某个数的出现次数。

考虑维护一个桶 \(cnt\) ,钦定每次分治处理时 \(cnt\) 恰为当前区间内数字的出现次数,并在回溯时清空。那么只要每次将小区间里的数字一个个删除,然后递归大区间,再一个个加入小区间里的数,再递归小区间即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;int a[N], b[N], cnt[N];int n;int solve(int l, int r) {if (l > r)return 0;int mid = -1;for (int pl = l, pr = r; pl <= pr && mid == -1; ++pl, --pr) {if (cnt[a[pl]] < b[r - l + 1])mid = pl;else if (cnt[a[pr]] < b[r - l + 1])mid = pr;}if (mid == -1) {for (int i = l; i <= r; ++i)--cnt[a[i]];return r - l + 1;} else if (mid - l > r - mid) {for (int i = mid; i <= r; ++i)--cnt[a[i]];int res = solve(l, mid - 1);for (int i = mid + 1; i <= r; ++i)++cnt[a[i]];return max(res, solve(mid + 1, r));} else {for (int i = l; i <= mid; ++i)--cnt[a[i]];int res = solve(mid + 1, r);for (int i = l; i < mid; ++i)++cnt[a[i]];return max(res, solve(l, mid - 1));}
}signed main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d", a + i), ++cnt[a[i]];for (int i = 1; i <= n; ++i)scanf("%d", b + i);printf("%d", solve(1, n));return 0;
}

二维分治

一般的分治方式是每次切割边长较大者的中线。

CF364E Empty Rectangles

有一个 \(n \times m\) 的01矩阵,询问有多少个子矩阵和为 \(k\)

\(n, m \leq 2500\)\(k \leq 6\)

考虑跨中线的答案,以竖直切割为例。枚举 \(y\) 坐标作为上下边界,维护 \(left_i, right_i\) 分别表示中线左右矩形中 \(1\) 的个数 \(< i\) 时最多扩展到的位置,统计答案时枚举 \(k\)\(1\) 的分布即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2.5e3 + 7, K = 11;int s[N][N];
char str[N];ll ans;
int n, m, k;inline int query(int xl, int xr, int yl, int yr) {return s[xr][yr] - s[xl - 1][yr] -  s[xr][yl - 1] + s[xl - 1][yl - 1];
}void solve(int xl, int xr, int yl, int yr) {if (xl == xr && yl == yr) {ans += (query(xl, xr, yl, yr) == k);return;}if (xr - xl > yr - yl) {int mid = (xl + xr) >> 1;solve(xl, mid, yl, yr), solve(mid + 1, xr, yl, yr);for (int l = yl; l <= yr; ++l) {vector<int> left(k + 2, xl), right(k + 2, xr);left[0] = mid + 1, right[0] = mid;for (int r = l; r <= yr; ++r) {for (int i = 1; i <= k + 1; ++i) {while (query(left[i], mid, l, r) >= i)++left[i];while (query(mid + 1, right[i], l, r) >= i)--right[i];}for (int i = 0; i <= k; ++i)ans += 1ll * (left[i] - left[i + 1]) * (right[k - i + 1] - right[k - i]);}}} else {int mid = (yl + yr) >> 1;solve(xl, xr, yl, mid), solve(xl, xr, mid + 1, yr);for (int l = xl; l <= xr; ++l) {vector<int> down(k + 2, yl), up(k + 2, yr);down[0] = mid + 1, up[0] = mid;for (int r = l; r <= xr; ++r) {for (int i = 1; i <= k + 1; ++i) {while (query(l, r, down[i], mid) >= i)++down[i];while (query(l, r, mid + 1, up[i]) >= i)--up[i];}for (int i = 0; i <= k; ++i)ans += 1ll * (down[i] - down[i + 1]) * (up[k - i + 1] - up[k - i]);}}}
}signed main() {scanf("%d%d%d", &n, &m, &k);for (int i = 1; i <= n; ++i) {scanf("%s", str + 1);for (int j = 1; j <= m; ++j)s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + (str[j] == '1');}solve(1, n, 1, m);printf("%lld", ans);return 0;
}

笛卡尔树分治

即最值分治,每次按最值的位置将区间分为两部分。

需要保证每层的复杂度仅与小区间有关,否则会退化到平方级别。

通常是按照启发式分裂的套路,枚举短区间,算长区间的贡献。

P4755 Beautiful Pair

给定序列 \(a_{1 \sim n}\) ,求有多少对 \(i \leq j\) 满足 \(a_i \times a_j \leq \max_{k = i}^j a_k\)

\(n \leq 10^5\)

构建笛卡尔树结构,设当前分治到区间 \([l, r]\) ,当前笛卡尔树上的节点在原序列上的编号是 \(p\)

根据分治套路,我们只需要计算跨过 \(p\) 的答案,然后 \([l, p - 1]\)\([p + 1, r]\) 分治处理。

以左边为短区间为例,考虑枚举左边,算右边的贡献。可以枚举点对的左端点 \(l\) ,则 \(a_r \leq \frac{a_p}{a_l}\)

时间复杂度 \(O(n \log^2 n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;int a[N];ll ans;
int n;template <class T = int>
inline T read() {char c = getchar();bool sign = c == '-';while (c < '0' || c > '9')c = getchar(), sign |= c == '-';T x = 0;while ('0' <= c && c <= '9')x = (x << 1) + (x << 3) + (c & 15), c = getchar();return sign ? (~x + 1) : x;
}namespace SMT {
const int S = N << 5;int s[S], lc[S], rc[S];
int rt[N];int tot;int insert(int x, int nl, int nr, int k) {int y = ++tot;lc[y] = lc[x], rc[y] = rc[x], s[y] = s[x] + 1;if (nl == nr)return y;int mid = (nl + nr) >> 1;if (k <= mid)lc[y] = insert(lc[x], nl, mid, k);elserc[y] = insert(rc[x], mid + 1, nr, k);return y;
}int query(int x, int y, int nl, int nr, int k) {if (nl == nr)return s[y] - s[x];int mid = (nl + nr) >> 1;if (k <= mid)return query(lc[x], lc[y], nl, mid, k);elsereturn (s[lc[y]] - s[lc[x]]) + query(rc[x], rc[y], mid + 1, nr, k);
}
} // namespace SMTnamespace CST {
int lc[N], rc[N];int root;inline void build() {static int sta[N];for (int i = 1, top = 0; i <= n; ++i) {int k = top;while (k && a[sta[k]] < a[i])--k;if (k)rc[sta[k]] = i;elseroot = i;if (k < top)lc[i] = sta[k + 1];sta[top = ++k] = i;}
}void solve(int x, int l, int r) {if (x - l <= r - x) {for (int i = l; i <= x; ++i)ans += SMT::query(SMT::rt[x - 1], SMT::rt[r], 1, 1e9, a[x] / a[i]);} else {for (int i = x; i <= r; ++i)ans += SMT::query(SMT::rt[l - 1], SMT::rt[x], 1, 1e9, a[x] / a[i]);}if (lc[x])solve(lc[x], l, x - 1);if (rc[x])solve(rc[x], x + 1, r);
}
} // namespace CSTsigned main() {n = read();for (int i = 1; i <= n; ++i)SMT::rt[i] = SMT::insert(SMT::rt[i - 1], 1, 1e9, a[i] = read());CST::build(), CST::solve(CST::root, 1, n);printf("%lld", ans);return 0;
}

其他

树上分治:点分治、边分治。

在线转离线:主要是 cdq 分治、分治 FFT 以及线段树分治。

强制在线:建立分治结构(如线段树、猫树),本质是 DS 题。

基于答案的分治:整体二分。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/863859.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

特殊数据类型的深度分析:JSON、数组和 HSTORE 的实用价值

title: 特殊数据类型的深度分析:JSON、数组和 HSTORE 的实用价值 date: 2025/1/4 updated: 2025/1/4 author: cmdragon excerpt: 随着数据管理需求的多样化,许多现代数据库系统开始支持特殊数据类型,以满足更多复杂应用场景的需求。在 PostgreSQL 中,JSON、数组和 HSTORE…

[.NET] Aspire 9 新特性及优化

随着 .NET 9 的发布,.NET Aspire 迎来了一系列重大更新和改进。这些更新不仅让 Aspire 变得更加成熟,也使其终于具备了投入生产环境的条件。本文将详细介绍 Aspire 的最新特性和改进。🚀Aspire 9 新特性及优化 摘要 随着 .NET 9 的发布,.NET Aspire 迎来了一系列重大更新和…

MapReduce--国家气候数据中心

NCDC:https://www.ncei.noaa.gov/中国气象数据网:https://data.cma.cn/

ABB IRB6640机械手减速机维修步骤

ABB IRB6640机械手减速机维修涉及多个步骤和注意事项,以下是一些关键的abb机械臂维修步骤和建议:一、故障排查检查故障代码:首先,通过查看机械手的故障显示屏或控制柜的显示屏,记录显示的错误代码。这些代码通常能指示减速机或其他部件的故障类型。检查电源和电缆:确保机…

Pycharm 2024.3.1.1 安装激活详细图文教程(激活至2026,实际上永久,亲测!)以及常见问题处理

申明:本教程 Pycharm补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版 !卸载老版本 Pycharm 首先,如果小伙伴的电脑上有安装老版本的 Pycharm , 需要将其彻底卸载掉,如下所示(没有安装则不用管,直接安装即…

java8--类Scanner--文件内容输入--错误的使用方法

Scanner in = new Scanner("mygile.txt"); String str = in.nextLine(); System.out.println(str);效果图:ps: 1.没有用类Paths的静态方法get指定路径,直接在创建Scanner新对象时传入文件名做参数,使用时不会启动输入步骤,不会从键盘获取输入,执行的是赋值操作…

Webstorm 2024 安装激活详细使用教程(激活至2026,实测是永久,亲测!)

开发工具推荐:Webstorm 安装激活详细使用教程(激活至2026,实际上永久,亲测!)申明:本教程 Webstorm补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版 ! Webstorm是JetBrains公司推出的一款功能强大的JS集成…

24. 两两交换链表中的节点(中)

目录题目法一、迭代法二、递归 题目给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。法一、迭代 var swapPairs = function(head) {let dummy = {next:head}let p1 = dummywhile(p1.ne…

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)PDF、EPUB免费下载

周志明JVM新作,新增内容近50%,大厂面试知识点全覆盖。与《Java编程思想、EffectiveJava、Java核心技术》堪称“Java四大名著”适读人群 :1.使用Java技术体系的中-高级开发人员 2.系统调优师 3.平台架构师 4.准备进互联网大厂面试的Java开发人员 周志明JVM新作,新增内容近50…

使用 ActiViz.Net (商业版)实现读三维地震数据可视化

一、背景 在地震勘探中,SEG-Y 格式(简称 SEGY)是最常用的数据格式之一,用于存储地震波的原始数据。地震数据通常包含了时间序列信息,并通过不同的采样和测量方法来记录地下结构的属性。为了有效地可视化这些数据,尤其是地震波的幅度和传播情况,三维可视化是一个非常有用…

使用 ActiViz.Net 实现读三维地震数据可视化

一、背景 在地震勘探中,SEG-Y 格式(简称 SEGY)是最常用的数据格式之一,用于存储地震波的原始数据。地震数据通常包含了时间序列信息,并通过不同的采样和测量方法来记录地下结构的属性。为了有效地可视化这些数据,尤其是地震波的幅度和传播情况,三维可视化是一个非常有用…

通过网址或html生成一个简单apk

GitHub - pzx521521/apk-editor 通过网址或html生成一个简单apk 之前发过一个使用apktool版本的,简单来说就是一个对apk反编译和编译的过程相比使用apktool有以下优点:[x] 没有环境依赖,完全不需要 jdk/jre [x] 无需中间调用apktool,容易部署到服务器/docker使用 Online Demo 修…