扫描线例题
洛谷 P5490 【模板】扫描线 & 矩形面积并
这里主要讲用代码实现的一些细节。也就是如何写线段树。
离散化后,我想要线段树维护的区间端点都是正数,因此在离散化之前我们在待离散化的数组里插入一个最小值,以遍在排序后将 0 这个位置占着:
a.clear();
a.push_back(-1); // 将第 0 个位置占着
// code push_back 一些数到 a 里
// /code
std::sort(a.begin(), a.end());
a.erase(std::unique(a.begin(), a.end()), a.end());
这样在我们二分找数的时候,总是会找到正下标。
对于线段树,我们写 左闭右开 的。因为每条横边都是左闭右开的,不管总坐标,假如你有一条横边是 \([1, 3]\),那么它的长度是 \(3 - 1 = 2\)(左闭右开) 而不是 \(3 - 1 + 1 = 3\)(全闭)。
于是对于线段树叶子节点对应的区间 \([l, r]\) 应该满足 \(r - l = 1\)。
于是:
CODE
constexpr int N = 1e5, M = 20;
std::vector<int> a;
std::array<int, 4> line[(N << 1) + 5];
int v[N << 3], w[N << 3];
void pushup(int cur, int l, int r) {
if (v[cur] != 0) {
w[cur] = a[r] - a[l];
}
else if (l + 1 == r) { // 叶子节点
w[cur] = 0;
}
else {
w[cur] = w[cur << 1] + w[cur << 1 | 1];
}
return;
}
// 左闭右开
void add(int cur, int l, int r, int sl, int sr, int val) {
if (sl <= l && r <= sr) {
v[cur] += val;
pushup(cur, l, r);
return;
}
int m = l + r >> 1;
if (sl < m) {
add(cur << 1, l, m, sl, sr, val);
}
if (sr > m) {
// 注意此处跟全闭的线段树不一样
add(cur << 1 | 1, m, r, sl, sr, val);
}
pushup(cur, l, r);
}
void solve() {
a.clear();
a.push_back(-1); // -1 占位
// the number of rectangle
int n = 0;
std::cin >> n;
for (int i = 0; i < n; i++) {
int x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
line[i] = { y1, x1, x2, 1 };
line[i + n] = { y2, x1, x2, -1 };
a.push_back(x1), a.push_back(x2);
}
// the number of horizontal line
n <<= 1;
std::sort(a.begin(), a.end());
a.erase(std::unique(a.begin(), a.end()), a.end());
std::sort(line, line + n,
[] (auto &u, auto &v) -> bool { return u[0] < v[0]; });
for (int i = 0; i < n; i++) {
line[i][1] = std::lower_bound(a.begin(), a.end(), line[i][1]) - a.begin();
line[i][2] = std::lower_bound(a.begin(), a.end(), line[i][2]) - a.begin();
}
i64 ans = 0;
int tot = a.size() - 1; // the number of different x
add(1, 1, tot, line[0][1], line[0][2], line[0][3]);
for (int i = 1; i < n; i++) {
ans += 1ll * (line[i][0] - line[i - 1][0]) * w[1];
add(1, 1, tot, line[i][1], line[i][2], line[i][3]);
}
std::cout << ans << '\n';
return;
}
洛谷 P1856 [IOI 1998 ] [USACO5.5] 矩形周长Picture
解题思路
两个方向都来一遍扫描线,依旧需要使用线段树维护出被覆盖的区域的长度,而对周长的贡献就是这次覆盖的长度与上次覆盖的长度的差的绝对值。
扫描线的代码和上一题是一模一样的。
CODE
constexpr int N = 1e5, M = 20, Inf = 1e9;
std::vector<int> a;
int v[N << 3], w[N << 3];
void pushup(int cur, int l, int r) {
if (v[cur] != 0) {
w[cur] = a[r] - a[l];
}
else if (l + 1 == r) {
w[cur] = 0;
}
else {
w[cur] = w[cur << 1] + w[cur << 1 | 1];
}
return;
}
// 左闭右开
void add(int cur, int l, int r, int sl, int sr, int val) {
if (sl <= l && r <= sr) {
v[cur] += val;
pushup(cur, l, r);
return;
}
int m = l + r >> 1;
if (sl < m) {
add(cur << 1, l, m, sl, sr, val);
}
if (sr > m) {
add(cur << 1 | 1, m, r, sl, sr, val);
}
pushup(cur, l, r);
}
void solve() {
a.clear();
// 注意此处,是插入 -Inf
a.push_back(-Inf);
// the number of rectangle
int n = 0;
std::cin >> n;
std::vector linex(n << 1, std::array<int, 4>{});
std::vector liney(n << 1, std::array<int, 4>{});
for (int i = 0; i < n; i++) {
int x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
linex[i] = { y1, x1, x2, 1 };
linex[i + n] = { y2, x1, x2, -1 };
a.push_back(x1), a.push_back(x2);
liney[i] = { x1, y1, y2, 1};
liney[i + n] = { x2, y1, y2, -1 };
a.push_back(y1), a.push_back(y2);
}
// the number of horizontal line or the vertical line
n <<= 1;
std::sort(a.begin(), a.end());
a.erase(std::unique(a.begin(), a.end()), a.end());
std::sort(linex.begin(), linex.end(),
[] (auto &u, auto &v) -> bool { return u[0] < v[0]; });
std::sort(liney.begin(), liney.end(),
[] (auto &u, auto &v) -> bool { return u[0] < v[0]; });
for (int i = 0; i < n; i++) {
linex[i][1] = std::lower_bound(a.begin(), a.end(), linex[i][1]) - a.begin();
linex[i][2] = std::lower_bound(a.begin(), a.end(), linex[i][2]) - a.begin();
liney[i][1] = std::lower_bound(a.begin(), a.end(), liney[i][1]) - a.begin();
liney[i][2] = std::lower_bound(a.begin(), a.end(), liney[i][2]) - a.begin();
}
i64 ans = 0;
int tot = a.size() - 1;
for (int i = 0, cur = 0; i < n; i++) {
add(1, 1, tot, liney[i][1], liney[i][2], liney[i][3]);
int d = std::abs(cur - w[1]);
// if (d) {
// std::cout << d << ' ';
// }
ans += std::abs(cur - w[1]);
cur = w[1];
}
// std::cout << '\n';
for (int i = 0, cur = 0; i < n; i++) {
add(1, 1, tot, linex[i][1], linex[i][2], linex[i][3]);
int d = std::abs(cur - w[1]);
// if (d) {
// std::cout << d << ' ';
// }
ans += std::abs(cur - w[1]);
cur = w[1];
}
// std::cout << '\n';
std::cout << ans << '\n';
return;
}
洛谷 P2163 [SHOI2007] 园丁的烦恼
二维数点模板题
解题思路
我们知道如何根据二维前缀和来求解区间和,但是这题的点太分散了(点数比坐标范围小了两个数量级),我们不可能真的开一个二维数组来搞前缀和。
但是我们只用关注题目给出的坐标 \((x_i, y_i)\),加上询问最多有 \(5N\) 个(树坐标,对于每个询问,根据二维前缀和的知识,我们要 4 个坐标才能求一个区间的和),我们只去求这些坐标的前缀和是可以接受的,现在就是要去求这些坐标的前缀和。
对于一个坐标 \((x_i, y_i)\),要求它的前缀和,即要求有多少个树的坐标 \((x_j, y_j)\) 满足 \(x_j \leq x_i \And y_j \leq y_i\)。这个问题的解决方法就是,在直角系上我们维护扫描线 \(x = i\) 从 1 往后依次扫描所有坐标,在扫描的过程中,我们记录每个扫到树的纵坐标,这样做的好处是,对于横坐标,前面扫描到过的树是天然满足条件的,于是只用找那些纵坐标满足条件的树。
CODE
constexpr int N = 5e5, M = 1e7, Inf = 1e9;
struct {
int x, y;
bool tree;
int id, co; // the index of a quiry and the coefficient
bool operator< (const auto &u) const {
if (x == u.x) {
if (y == u.y) {
return tree;
}
else {
return y < u.y;
}
}
else {
return x < u.x;
}
}
} node[N * 5 + 5];
int cnt = 0;
std::vector<int> h;
int hash(int x) {
return std::lower_bound(h.begin(), h.end(), x) - h.begin();
}
void add_node(int x, int y, bool tree, int id, int co) {
node[cnt++] = { x, y, tree, id, co };
}
// BIT 维护纵坐标
int tr[N * 5 + 5];
int lowbit(int u) {
return u & -u;
}
void add(int pos) {
while (pos < h.size()) {
tr[pos] += 1;
pos += lowbit(pos);
}
return;
}
int pre(int pos) {
int res = 0;
while (pos > 0) {
res += tr[pos];
pos -= lowbit(pos);
}
return res;
}
void solve() {
// -1 占位
h.push_back(-1);
int n = 0, m = 0;
std::cin >> n >> m;
for (int i = 0; i < n; i++) {
int x = 0, y = 0;
std::cin >> x >> y;
x++, y++;
add_node(x, y, true, 0, 0);
h.push_back(y); // 只用离散化纵坐标
}
for (int i = 0; i < m; i++) {
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
std::cin >> x1 >> y1 >> x2 >> y2;
// 自增的好处是,不用处理 0
x1++, y1++, x2++, y2++;
// 二维前缀和
add_node(x2, y2, false, i, 1);
add_node(x1 - 1, y1 - 1, false, i, 1);
add_node(x1 - 1, y2, false, i, -1);
add_node(x2, y1 - 1, false, i, -1);
h.push_back(y2), h.push_back(y1 - 1);
}
std::sort(h.begin(), h.end());
h.erase(std::unique(h.begin(), h.end()), h.end());
std::sort(node, node + cnt);
std::vector ans(m, 0);
for (int i = 0; i < cnt; i++) {
int y = hash(node[i].y);
if (node[i].tree) {
add(y);
}
else {
ans[node[i].id] += node[i].co * pre(y);
}
}
for (int i = 0; i < m; i++) {
std::cout << ans[i] << "\n";
}
return;
}
洛谷 P1972 [SDOI2009] HH的项链
OI Wiki 原话:
这类问题我们可以考虑推导性质,之后使用扫描线枚举所有右端点,数据结构维护每个左端点的答案的方法来实现,我们也可以将问题转换到二维平面上,变为一个矩形查询信息的问题。
解题思路
我们要做的是统计区间中不同的数的个数,于是,对于一个区间,我们只在每个数第一次出现时记录贡献。如何判断那些数是在区间中第一次出现的?我们去看每一个数上次是出现在什么位置(记第一次出现的数的上次出现是在位置 0 ),于是一个区间内不同的数的个数,就变成了区间内有多少个数的上一个数是在区间之外。为了便于描述,记 \(a[i]\) 表示第 \(i\) 个位置上的数, \(pre[i]\) 为在 \(i\) 之前最近的一个 \(a[i]\) 的位置。于是对于询问区间 \([l, r]\),我们要找在区间中有多少 \(i\) 满足 \(pre[i] \leq l - 1\)。此时我们可以看作有个二维平面,上面有若干点,其坐标为 \((i, pre[i])\),于是我们的询问又可以变成在矩形 \((l, 0) (r, l - 1)\)(左下角和右上角的坐标)中数点,到这一步就比上一题还简单了……
CODE
constexpr int N = 1e6, M = 1e6, MAX = 1e6, Inf = 1e9;
struct {
int x, y;
bool isnode;
int id, co; // the index of a quiry and the coefficient
bool operator< (const auto &u) const {
if (x == u.x) {
if (y == u.y) {
return isnode;
}
else {
return y < u.y;
}
}
else {
return x < u.x;
}
}
} node[N * 3 + 1];
int cnt = 0;
void add_node(int x, int y, bool isnode, int id, int co) {
node[cnt++] = { x, y, isnode, id, co };
}
int tr[MAX + 5];
int lowbit(int u) {
return u & -u;
}
void add(int pos) {
if (pos == 0) {
tr[0]++;
return;
}
while (pos < MAX ) {
tr[pos] += 1;
pos += lowbit(pos);
}
return;
}
int pre(int pos) {
int res = tr[0];
while (pos > 0) {
res += tr[pos];
pos -= lowbit(pos);
}
return res;
}
int pos[MAX + 5];
void solve() {
int n = 0;
std::cin >> n;
for (int i = 1; i <= n; i++) {
int a = 0;
std::cin >> a;
add_node(i, pos[a], true, 0, 0);
pos[a] = i;
}
int m = 0;
std::cin >> m;
for (int i = 0; i < m; i++) {
int x = 0, y = 0;
std::cin >> x >> y;
add_node(x - 1, x - 1, false, i, -1);
add_node(y, x - 1, false, i, 1);
}
std::sort(node, node + cnt);
std::vector ans(m, 0);
for (int i = 0; i < cnt; i++) {
if (node[i].isnode) {
add(node[i].y);
}
else {
ans[node[i].id] += node[i].co * pre(node[i].y);
}
}
for (int i = 0; i < m; i++) {
std::cout << ans[i] << '\n';
}
return;
}
洛谷 P1908 逆序对
此题的目的是为下一题做铺垫
解题思路
从后往前扫,每次统计有多少小于当前数的数。离散化后用树状数组维护就好了。
CODE
int tot;
int tr[N + 5];
int lowbit(int x) {
return x & -x;
}
void add(int pos) {
while (pos <= tot) {
tr[pos]++;
pos += lowbit(pos);
}
return;
}
int ask(int pos) {
int res = 0;
while (pos > 0) {
res += tr[pos];
pos -= lowbit(pos);
}
return res;
}
void solve() {
int n = 0;
std::cin >> n;
std::vector a(n, 0), h(1, 0);
for (int i = 0; i < n; i++) {
std::cin >> a[i];
h.push_back(a[i]);
}
// 离散化 注意 0 占位使得离散化后都是正数
std::sort(h.begin(), h.end());
h.erase(std::unique(h.begin(), h.end()), h.end());
tot = h.size() - 1;
for (int i = 0; i < n; i++) {
a[i] = std::lower_bound(h.begin(), h.end(), a[i]) - h.begin();
}
i64 ans = 0;
for (int i = n - 1; i >= 0; i--) {
add(a[i]);
ans += ask(a[i] - 1);
}
std::cout << ans << '\n';
return;
}
洛谷 P8593 「KDOI-02」一个弹的投
解题思路
首先得知道只有初始时在同一高度的炸弹在平抛的过程中才有可能碰在一起。所以第一步应该是按高度为所有的炸弹分类,高度相同的一起考虑。
下面我们只在高度相同的炸弹之间考虑,各个炸弹的高度在整个平抛过程中肯定是相同的,所以我们判断两颗炸弹会不会相遇只需要考虑水平方向的运动。有了初始坐标和速度,我们就可以确定终点的横纵标,于是对于两颗炸弹 \(M_i(x_0, x_t)\) 和 \(M_j(x_0^{'}, x_t^{'})\) (括号里面的两个数是初始位置的横坐标和终点的横坐标)只要满足 \((x_0 - x_0^{'})(x_t - x_t^{'}) \leq 0\) 我们就认为会相遇。于是若我们将所有的炸弹根据 \(x_0\) 排序,我们之后要做的起始跟在求逆序对里面要做的十分相似。
CODE
constexpr int N = 5e5;
constexpr double G = 9.8, Inf = 1e18;
struct MISSILE {
int id;
int y, x0;
double xt;
bool operator<(const MISSILE &a) const {
return (y == a.y ? x0 < a.x0 : y < a.y);
}
} ms[N + 5];
int a[N + 5];
int tot; // 记录离散化后有多少个不同的值
double h[N + 5]; // 离散化用
int tr[N + 5]; // BIT
void init() {
for (int i = 1; i <= tot; i++) {
tr[i] = 0;
}
}
int lowbit(int x) {
return x & -x;
}
void add(int pos) {
while (pos <= tot) {
tr[pos]++;
pos += lowbit(pos);
}
return;
}
int ask(int pos) {
int res = 0;
while (pos > 0) {
res += tr[pos];
pos -= lowbit(pos);
}
return res;
}
// 计算同一高度的炸弹
void cnt(int l, int r) {
// 离散化,-Inf 来占 0 的位置
tot = 0;
h[tot++] = -Inf;
for (int i = l; i < r; i++) {
h[tot++] = ms[i].xt;
}
std::sort(h, h + tot);
// 去重
int tmp = tot;
tot = 1;
for (int i = 1; i < tmp; i++) {
if (h[i] != h[i - 1]) {
h[tot++] = h[i];
}
}
tot--;
init();
for (int i = l; i < r; i++) {
int hv = std::lower_bound(h, h + tot + 1, ms[i].xt) - h;
a[ms[i].id] += ask(tot) - ask(hv - 1);
add(hv);
}
init();
for (int i = r - 1; i >= l; i--) {
int hv = std::lower_bound(h, h + tot + 1, ms[i].xt) - h;
a[ms[i].id] += ask(hv);
add(hv);
}
return;
}
void solve() {
int n = 0, m = 0;
std::cin >> n >> m;
for (int i = 0; i < n; i++) {
int x = 0, y = 0, v = 0;
std::cin >> x >> y >> v;
double xt = x + 1.0 * v * std::sqrt(2.0 * y / G);
ms[i] = { i, y, x, xt };
}
std::sort(ms, ms + n);
// 根据高度分类
for (int l = 0, r = 1; l < n; l = r) {
while (r < n && ms[r].y == ms[l].y) {
r++;
}
cnt(l, r);
}
i64 sum = 0;
// 先不用制导系统
for (int i = 0; i < n; i++) {
sum += a[i];
int dc = 0;
std::cin >> dc;
// 每个制导系统能发挥的作用
a[i] = std::min(a[i], dc);
}
// 取作用最大的几个
std::sort(a, a + n,
[](int &u, int &v) { return u > v; });
for (int i = 0; i < m; i++) {
sum -= a[i];
}
std::cout << sum << '\n';
return;
}
洛谷 P8844 [传智杯 #4 初赛] 小卡与落叶
解题思路
主要是树上询问转线上询问。
对于每次询问,我们只关心最近的一次修改,于是将每次询问和最近的一次修改组合成二元组 \(<v, deep>\),并按照 \(deep\) 的值降序排列。这样我们在回答询问时只用不断地染黄色色而不用重置成绿色。
对于询问,我们将子树转化为 dfs 序里面的一段,于是问子树里有多少是黄色的就是求得 dfs 序中对应的段有多少是黄色的,树状数组维护即可。
CODE
constexpr int N = 1e5, M = 1e5;
int n;
std::vector<int> g[N + 5], dep[N + 5];
int tot; // 需要输出的询问的个数
std::array<int, 3> qr[N + 5];
// L[i] 既是点 i 在 dfs 序中的位置,又是以 i 为根节点的子树在 dfs 序中的第一个位置
// R[i] 是以 i 为根节点的子树在 dfs 序中的最后一个位置
int tim, L[N + 5], R[N + 5];
void dfs(int cur, int fa = 0, int dp = 1) {
dep[dp].push_back(cur);
L[cur] = ++tim;
for (auto &to : g[cur]) {
if (to == fa) {
continue;
}
dfs(to, cur, dp + 1);
}
R[cur] = tim;
}
int tr[N + 5];
int lowbit(int x) {
return x & -x;
}
void add(int pos) {
while (pos <= n) {
tr[pos]++;
pos += lowbit(pos);
}
return;
}
int ask(int pos) {
int res = 0;
while (pos > 0) {
res += tr[pos];
pos -= lowbit(pos);
}
return res;
}
void solve() {
int m = 0;
std::cin >> n >> m;
for (int i = 0; i < n - 1; i++) {
int u = 0, v = 0;
std::cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1);
for (int i = 0, lst = n + 1; i < m; i++) {
int op = 0, x = 0;
std::cin >> op >> x;
if (op == 1) {
lst = x;
}
else {
qr[tot] = { tot, lst, x };
tot++;
}
}
// 染色深度深的在前面,这样对于后续的操作的是加法
sort(qr, qr + tot,
[] (auto &u, auto& v) -> bool { return u[1] > v[1]; });
int done = n + 1;
std::vector ans(tot, 0);
for (int i = 0; i < tot; i++) {
for (int d = qr[i][1]; d < done; d++) {
for (auto &v : dep[d]) {
add(L[v]);
}
}
done = qr[i][1];
ans[qr[i][0]] = ask(R[qr[i][2]]) - ask(L[qr[i][2]] - 1);
}
for (auto &i : ans) {
std::cout << i << '\n';
}
return;
}
浙公网安备 33010602011771号