Loading

CDQ 分治,李超树与斜率优化问题

P4027,及一类类似问题:

给定 \(a_i,b_i,x_i,y_i\),对于每个 \(i\) 求出 \(f_i = \max\limits_{j=1}^{i} \{a_ix_j+b_iy_j\}\)

本文仅写给作者自己看,不保证其中“或许”“应该”的正确性。

一个经典问题

给定 \(n\) 个二维向量,多次给定一个向量,求该向量与开始给定向量点乘 / 叉乘最大值。

点乘可以交换两维数值后取反第一维以转化为叉乘。现在来讨论叉乘。

叉乘的几何意义是两向量张出平行四边形的有向面积,取询问向量 \(x\) 作为底,则要找到到给定向量的投影距离(生造一个词 qwq)最小的向量。

找出凸包,则用平行 \(x\) 的直线切凸包,切到的点则是最优点。这是 wqs 二分的内容(?

感性理解,给定向量的斜率单调变化时,切点也单调变化。于是若斜率有序,可以维护一个指针做到均摊 \(O(1)\)

如果有转移时间的限制呢

把式子写成 \(\begin{bmatrix} x_j \\ y_j \end{bmatrix} \cdot \begin{bmatrix} a_i \\ b_i \end{bmatrix}\)\(\begin{bmatrix} -y_j \\ x_j \end{bmatrix} \times \begin{bmatrix} a_i \\ b_i \end{bmatrix}\) 的形式,就能化回上述形式。

但是,现在要一边插入一边查询,插入在首位时可以使用单调队列维护,插入在中间只能用平衡树维护。码量太大了,不想学。

在具有时间轴时,cdq 分治允许在离线的条件下,通过一些处理,使得让按其他维度排序成为现实。

另一个方向是李超树。仔细想了想,李超树确实可以处理此种叉积最大的问题,所以斜率优化能做的李超树应当都能做。李超树支持在线插入查询,所以它可以拿来在这个问题上代替 cdq。

上面那句话很怪,事实上应该是,cdq 是为了解决非在线插入的最大叉积问题。

CDQ

CDQ 分治的关键是把一个点得到的贡献拆成区间,区间对区间转移,以去掉两边区间内的时间限制。

整体二分的关键是同时用值域分离答案和询问。它的好处是可以直接按时间轴排序,要求是修改之间与判定答案互不干涉。

—— 整体二分小记

就此问题不想写代码,但是打算了解一下 cdq 的思路。

(没学过 cdq,下面来随便口胡一下)

一类问题后面的计算依赖前面的计算(分治 fft),感觉上是把贡献拆成了 \(\log\) 个小区间分别贡献。数轴上共有 \(n\) 个小区间,区间长度之和为 \(n \log n\),每个数被包含于 \(\log\) 个小区间中,要给 \(\log\) 个区间贡献。

考虑批量转移,一个区间内的点对后面的一个区间做贡献,这样,被贡献的点之间是独立的,也就让对另一个维度排序成为可能。

一个点需要被它前面的 \(\log\) 个区间转移到,这是一段前缀,不妨进行二进制拆分,每一步拆出最多能拆出的二进制位,那么一个区间 \([i,i+2^k)\)\([i+2^k,i+2^{k+1})\) 批量产生贡献。

一个点作为转移点前需要保证已经被更新完成。因此可以像线段树上 dfs 式进行更新。

提到了线段树,那么好像严格的二进制拆分也不必要了,直接线段树式拆分就行了。

考虑处理区间 \(solve(l,r)\) 的过程。

  1. \(mid \gets (l+r)/2\)
  2. \(solve(l,mid)\)
  3. \([l,mid]\)\((mid,r]\) 贡献
  4. \(solve(mid+1,r)\)

那么,只要能解决批量转移的部分,让其与整体数据规模无关,就能在解决静态问题多一只 \(\log\) 的代价下解决整个问题。

不想写,感觉上比整体二分还好理解。

说回来

单来考虑左区间往右区间贡献的部分。此时是一个离线问题,把左边的点插进凸包,右边的点按询问斜率的顺序排序,这样即有序。

很明显的与整个数据集的规模无关。

整体二分就像值域维度的 CDQ 分治。在二分的答案 \(mid\) 的两侧,左侧(的修改)对右侧(的查询)贡献。

它保证了操作在时间上是连续的,而通过在往下划分区间时处理前面的所有影响的方式来处理修改对询问的影响。于是,递归到的区间是一个单独的子问题,解决它的复杂度与整个数据集无关。

一者让时间连续,拆分值域上左侧对右侧的影响;一者让值域连续,拆分时间上左侧的右侧的影响。这就是 CDQ 与整体二分。

CDQ:基于时间的整体分治算法; 整体二分:基于值域的整体分治算法

——lyd 算进

我理解分治 fft 了。

干脆来想想 P3332 用 CDQ 分治怎么做。

P3332:

给定 \(n\) 个集合,要求支持区间加元素,求区间并集 \(k\) 小值。

那么,拆分时要考虑的是前面操作对后方查询的影响。

什么影响呢?要查询的 \(k\) 变小了。

好吧做不了。但是因为 \(k\) 小了,恰恰启示值域考虑,也就是整体二分,研究值域上左边对右边的影响。

我有一个小小的目标,我要找到一道题,让整体二分和 CDQ 分治都能做。

整体二分?

现在尝试用 CDQ 分治的思路去看看整体二分。

划分。一个个从前到后的贡献被放到区间对区间的贡献中,让区间内部的时间顺序变得不重要。

整体二分呢?一个个比它小的数字被贡献到它上,同样被放到区间对区间的贡献中。

回家写。

upd:

现在反而感觉二者没有联系了。

二者相似的唯一一点是都有区间对区间的转移,其余地方并不相似。

就这样吧。

还没说李超树呢

李超树,维护多条直线,查询与 \(x=k\) 交点最往上的直线编号。支持动态插入。

广义一点,维护多个向量,查询与给定向量点乘叉乘最大最小的向量。

恰好拿来动态维护。

非常好写。维护直线的复杂度单次 \(O(\log n)\),维护线段的复杂度单次 \(O(\log^2 n)\)

复杂度还比该题(别忘了开头有题)的 CDQ 做法复杂度低。

有题就有代码。

#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
const int M = 1e5 + 5;
int n, s[M << 2]; double A[M], B[M], r[M], k[M], b[M], x[M], y[M];
#define f(i, t) (b[t] + k[t] * x[i])
void upd(int o, int l, int r, int t) {
    int mid = l + r >> 1;
    if(f(mid, s[o]) < f(mid, t)) swap(s[o], t);
    if(f(l, s[o]) < f(l, t)) upd(o<<1, l, mid, t);
    if(f(r, s[o]) < f(r, t)) upd(o<<1|1, mid+1, r, t);
}
double query(int o, int l, int r, int p) {
    if(l == r) return f(p, s[o]);
    int mid = l + r >> 1;
    return max(f(p, s[o]), p <= mid ? query(o<<1, l, mid, p) : query(o<<1|1, mid+1, r, p));
}
/*
x_i = f_ir_i / (a_ir_i + b_i)
y_i = f_i / (a_ir_i + b_i)
f_i = max(f_{i-1}, max(a_i x_j + b_i y_j))
    = max(f_{i-1}, b_i max(a_i / b_i * x_j + y_j))
let k_j = x_j, b_j = y_j, x_i = a_i / b_i
then f_i = max(f_{i-1}, b_i max(k_j * x_j + b_j))
use Li-chao segement tree to maintain it
*/
int main(){
    double f;
    scanf("%d %lf", &n, &f);
    for(int i = 1; i <= n; i++) {
        scanf("%lf %lf %lf", &A[i], &B[i], &r[i]);
        x[i] = A[i] / B[i]; y[i] = x[i];
    }
    sort(x+1, x+n+1);
    for(int i = 1; i <= n; i++) {
        f = max(f, B[i] * query(1, 1, n, lower_bound(x+1, x+n+1, y[i]) - x));
        double tx = f * r[i] / (A[i] * r[i] + B[i]), ty = f / (A[i] * r[i] + B[i]);
        k[i] = tx; b[i] = ty; 
        upd(1, 1, n, i);
    }
    printf("%lf\n", f);
}

李超树多可爱啊!

posted @ 2022-12-01 16:47  purplevine  阅读(176)  评论(0编辑  收藏  举报