Luogu P3081 [USACO13MAR] Hill Walk G 题解 [ 紫 ] [ 李超线段树 ] [ 平衡树 ] [ 离散化 ]
Hill Walk G:比较典的动态开点李超板子。
Sol.1 动态开点李超线段树
先考虑如何计算答案。显然每次移动的时候都找到这段山的终点,然后找到比他低的一座山,接着走即可。沿途记录一下走过的山的数量。
那么在行走的过程中,我们就需要维护比 \(ty\) 小的最大值。普通的李超树显然是无法维护的,但是因为我们走过的山是唯一的,所以可以在行走的过程中动态插入终点比 \(ty\) 小的线段,具体可以将所有山按左端点的 \(x\) 排序后用一个指针判断是否需要插入。其中 \(ty\) 表示线段终点的纵坐标。
上述做法全部都是基于“所有线段不交”的性质,如果题目没有保证这个性质,那么我们行走的过程中动态维护比 \(ty\) 小的线段就是不行的,因为无法保证之前比自己低的线段以后依然比自己低。
因为值域是 \(10^9\) 且插入的是线段,所以时间复杂度 \(O(n\log^2 V)\),空间复杂度 \(O(n\log V)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 100005;
const ldb eps = 1e-9;
int n, cnt, ys[N];
struct Hill{
ll sx, sy, tx, ty;
bool operator < (const Hill & t) const{
if(sx != t.sx) return sx < t.sx;
return sy < t.sy;
}
}a[N];
struct Line{
ldb k, b;
int id;
}line[N];
ldb gety(int id, ldb x)
{
return (line[id].k * x + line[id].b);
}
struct Node{
int ls, rs, id;
};
int cmp(ldb a, ldb b)
{
if(a - b > eps) return 1;
if(b - a > eps) return -1;
return 0;
}
struct Lichao{
Node tr[40 * N];
int idx = 0, root = 0;
void update(int &p, int ln, int rn, int ql, int qr, int id)
{
if(p == 0) p = ++idx;
int mid = (ln + rn) >> 1;
if(ql <= ln && rn <= qr)
{
if(cmp(gety(id, mid), gety(tr[p].id, mid)) == 1) swap(id, tr[p].id);
if(cmp(gety(id, ln), gety(tr[p].id, ln)) == 1) update(lc(p), ln, mid, ql, qr, id);
if(cmp(gety(id, rn), gety(tr[p].id, rn)) == 1) update(rc(p), mid + 1, rn, ql, qr, id);
return;
}
if(ql <= mid) update(lc(p), ln, mid, ql, qr, id);
if(qr >= mid + 1) update(rc(p), mid + 1, rn, ql, qr, id);
}
int query(int p, int ln, int rn, int pos)
{
if(p == 0) return 0;
if(ln == pos && rn == pos) return tr[p].id;
int mid = (ln + rn) >> 1;
int res;
if(pos <= mid)
{
res = query(lc(p), ln, mid, pos);
if(cmp(gety(tr[p].id, pos), gety(res, pos)) == 1) res = tr[p].id;
}
else
{
res = query(rc(p), mid + 1, rn, pos);
if(cmp(gety(tr[p].id, pos), gety(res, pos)) == 1) res = tr[p].id;
}
return res;
}
}tr1;
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
line[0] = {0, -1e18};
for(int i = 1; i <= n; i++)
cin >> a[i].sx >> a[i].sy >> a[i].tx >> a[i].ty;
sort(a + 1, a + n + 1);
int cur = 1, ans = 0, p = 2;
ldb k = 1.0 * (a[1].ty - a[1].sy) / (a[1].tx - a[1].sx);
ldb b = a[1].ty - k * a[1].tx;
line[++cnt] = {k, b, 1};
ys[1] = cnt;
while(1)
{
ans++;
ll vx = a[cur].tx, vy = a[cur].ty;
while(p <= n && a[p].sx <= vx)
{
if(cmp(a[p].sy, gety(ys[cur], a[p].sx)) == 1)
{
p++;
continue;
}
k = 1.0 * (a[p].ty - a[p].sy) / (a[p].tx - a[p].sx);
b = a[p].ty - k * a[p].tx;
line[++cnt] = {k, b, p};
ys[p] = cnt;
tr1.update(tr1.root, 0, 1e9, a[p].sx, a[p].tx - 1, cnt);
p++;
}
int tmp = tr1.query(tr1.root, 0, 1e9, vx);
int v = line[tmp].id;
if(v == 0) break;
cur = v;
}
cout << ans;
return 0;
}
Sol.2 离散化李超线段树
大体做法和动态开点一致,同样需要用到“线段不交”的性质,否则线段间的端点也需要加入离散化,复杂度就会爆炸。注意在计算纵坐标的时候需要根据离散化前的横坐标计算。
时间复杂度 \(O(n\log ^2n)\),空间复杂度 \(O(n\log n)\)。口胡的,没有代码。
Sol.3 平衡树
和李超树做法大致相同,都是在路径上对比 \(ty\) 小的线段进行维护。如果用平衡树去维护这个过程,那么在插入线段的时候直接在平衡树上二分找到插入的位置,走到某个线段终点的时候直接删除线段即可。查询的时候也是在平衡树上二分就可以了。在平衡树上二分这里同样需要用到“线段不交”的性质。
时间复杂度 \(O(n\log n)\),空间复杂度 \(O(n)\)。口胡的,没有代码。

浙公网安备 33010602011771号