P9368 [ICPC 2022 Xi’an R] Streets

根据题意,我们可以想到的一个式子,假设选择 x1x2,y1y2x_1\le x_2, y_1\le y_2,则

max(x2x1)(y2y1)s.t.(x2x1)(b1+b2)+(y2y1)(a1+a2)C \begin{array}{rc} \max&(x_2-x_1)(y_2-y_1)\\ s.t.&(x_2-x_1)(b_1+b_2)+(y_2-y_1)(a_1+a_2)\le C \end{array}

这个式子里,a1+a2,b1+b2a_1+a_2,b_1+b_2 都是单独给定的,无法进行合并,而且我们发现 x2x1,y2y1x_2-x_1,y_2-y_1 都是以这个形式一起出现的。而且,倘若 Δx\Delta x 值相同,那么我们肯定希望选择 ai+aja_i+a_j 最小的那对 xi,xjx_i,x_j.

所以,我们考虑处理出 f(dx)=minxjxi=dxai+ajf(dx)=\min\limits_{|x_j-x_i|=dx} a_i+a_jg(dy)=minyjyi=dybi+bjg(dy)=\min\limits_{|y_j-y_i|=dy}b_i+b_j. 这一步可以在 O(n2+m2)O(n^2+m^2) 的时间内完成。

所以,我们的目标就变成了

maxdxdys.t.dxg(dy)+f(dx)dyC \begin{array}{rc} \max &dx\cdot dy\\ s.t. &dx\cdot g(dy)+f(dx)\cdot dy\le C \end{array}

我们有 TT 次询问。一个想法是,如果给定 (dy,g(dy))(dy,g(dy)),那么我们就需要找一个满足约束条件且最大的 dxdx。改写式子可以得到:

f(dx)g(dy)dydx+Cdy f(dx)\le -\frac{g(dy)}{dy}\cdot dx+\frac{C}{dy}

(dx,f(dx))(dx, f(dx)) 标在坐标系上,所以其实我们想要的是直线 y=g(dy)dyx+Cdyy=-\frac{g(dy)}{dy}x+\frac{C}{dy} 这条直线下方 xx 最大的那个点。如果可以以 O(F)O(F) 的时间求出单次查询,那么我们遍历 (dy,g(dy))(dy,g(dy)) 就可以对每一次查询求出其答案,TT 次查询的总时间复杂度就是 O(TVF),V=105O(TVF),V=10^5.

那么很显然,我们不能直接遍历 dxdx,这样 O(F)=O(V)O(F)=O(V) 会直接 TLE.

这里,我初始的想法是直接对 (dx,f(dx))(dx,f(dx)) 构造凸包后,直接在凸包上二分,找到一个在凸包上的点。

但是这样会有一个错误的点,就是虽然点 (x,f(x))(x',f(x')) 不在凸包上,但是在直线下方,且比找到的凸包上的点靠右。

Illustration
Illustration

图里,蓝色边是凸包上的边,其两个端点是凸包上的点;黄色线是约束条件变形而来的直线;zi色的点就是刚刚说的“可能会被遗漏的点”。

那么我们怎么才能算上这样的紫色点呢?我们把优化问题转化成存在性问题:既然我们要找直线下方最靠右的点 (x,f(x))(x',f(x')),也就是说当 dxxdx\le x' 的时候,凸包上都至少有一个点在直线下方(要么是 xx' 在凸包上,要么是 dx0<xdx_0\lt x' 在凸包上);当 dx>xdx\gt x' 的时候,没有点在直线下方。

所以,我们就可以通过二分把优化问题转化成存在性问题。二分答案 xmidx_{mid},判断 dxxmiddx\ge x_{mid} 形成的凸包中,是否有点在直线下方。

判断凸包上是否有点在一条直线下方,可以先在凸包上找到点 (x,f(x))(x',f(x')),使得直线平移后会和凸包在这个点相切,再判断这个点是否在直线下方. 时间复杂度是 O(logV)O(\log V) 的.

这个算法的时间复杂度由二分套二分决定:O(V)O(V) 枚举 (dy,g(dy))(dy,g(dy));对于每个 dydyO(logV)O(\log V)dx[1,V]dx\in[1,V] 里二分最靠右的点,每次二分里的 check_mid 还需要 O(logV)O(\log V) 在凸包上二分找点。不过,考虑到同时维护凸包的话,把点从凸包里弹出、插入又需要 O(V)O(V),所以整体时间复杂度会来到 O(T(V2logV+Vlog2V))O(T(V^2\log V+V\log^2 V))

能不能再优化一下呢?答案其实具有不劣性:因为我们希望 maxdxdy\max dx\cdot dy,如果对于 dydy 来说 dx0dx_0 是可行的,那么对于 dy1dy-1 来说,我们其实不需要关心 dxdx0dx\le dx_0 的所有 dxdx,因为 (dy1)dx(dy1)dx0<dydx0(dy-1)dx\le (dy-1)dx_0\lt dy\cdot dx_0 根本不可能更新 dydy 计算出的答案。这就是答案的不劣性(虽然是我自己起的名字)

所以,上文里,我们并不需要每次都“在 dx[1,V]dx\in[1,V]” 里二分最靠右的点,而是可以维护一个下界的指针 pp 并且从大到小枚举 dydy。假设 dydy 对应最大的 dxdxdx0dx_0,那么对于 dy1dy-1,我们只需要从 dx0+1dx_0+1 开始 find 对应的 dxdx' 即可。

由于当 dydy 递减的时候,查找的下界也会至少 +1+1,所以我们最多进行 O(V)O(V) 次 find。但是考虑到如果在 [dx0+1,V][dx_0+1,V] 上使用二分,不断地从凸包里删点、加点很可能超时。我们还需要想办法优化掉凸包的频繁删点、加点。

凸包的删点加点的复杂度来源就是虽然二分中 midmidmid\to mid' 可以 O(1)O(1) 跳跃,但是对应的凸包却需要 O(midmid)O(mid'-mid) 的时间删点加点进行维护,这部分的时间很有可能达到 O(V2)O(\frac{V}{2})

我们在预处理凸包的时候,记录下尝试加入 dxdx' 时删掉了哪些点 p[dx]p[dx'](这说明点集 p[dx]p[dx'] 加上原本就在凸包里的点就是 dxdxdx\ge dx' 对应的凸包)。那么直接考虑顺序枚举 dxdx,这样我们的凸包只需要弹掉 dxdx,加入 dx+1dx+1p[dx+1]p[dx+1]。而每个点最多加入、弹出 stack 各一次,因此这样维护的话总体是 O(V)O(V) 的,即均摊 O(1)O(1)。于是优化掉凸包维护后,整体的时间复杂度就是 O(TVlogV)O(TV\log V)

Code
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
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <iostream>
#include <ostream>
#include <vector>
using i64 = long long;
using vi = std::vector<int>;
constexpr int NM = 5e3 + 5;
constexpr int V = 1e5 + 5;
constexpr int inf = 1e9;

int n, m, T;
i64 C;
int x[NM], y[NM], a[NM], b[NM];
int vdx[V], vdy[V];
int root{-1};

int stp = -1;
vi stack, tstack;
vi popped[V];

bool left_side(int i, int j, int k) {
assert(i <= j && j <= k);
int xs = j - k;
int ys = vdx[j] - vdx[k];
int xx = i - k;
int yx = vdx[i] - vdx[k];
return 1ll * xs * yx >= 1ll * ys * xx;
}

i64 cost(int dx, int dy) { return 1ll * dx * vdy[dy] + 1ll * dy * vdx[dx]; }

std::ostream &operator<<(std::ostream &os, vi &v) {
os << "[";
for (auto x : v) os << x << ',';
os << "]" << '\n';
return os;
}

int main() {
std::cin >> n >> m >> T;
for (int i = 1; i <= n; i++) std::cin >> x[i];
for (int i = 1; i <= n; i++) std::cin >> a[i];
for (int i = 1; i <= m; i++) std::cin >> y[i];
for (int i = 1; i <= m; i++) std::cin >> b[i];
for (int i = 0; i < V; i++) vdx[i] = vdy[i] = inf;
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++) vdx[x[j] - x[i]] = std::min(vdx[x[j] - x[i]], a[j] + a[i]);
for (int i = 1; i <= m; i++)
for (int j = i; j <= m; j++) vdy[y[j] - y[i]] = std::min(vdy[y[j] - y[i]], b[j] + b[i]);

for (int dx = V - 1; dx >= 0; dx--) {
if (vdx[dx] == inf) continue;
while (stp >= 1 && left_side(dx, stack[stp], stack[stp - 1])) {
popped[dx].push_back(stack.back());
stack.pop_back();
stp--;
}
if (root == -1) root = dx;
stack.push_back(dx);
stp++;
}

tstack.resize(stack.size());
std::copy(stack.begin(), stack.end(), tstack.begin());

while (T--) {
std::cin >> C;
int now_dx = 0;
i64 ans = 0;
stack.clear();
for (auto v : tstack) stack.push_back(v);

auto next = [&] {
if (now_dx > root) return;

// 维护凸包,先弹掉顶部的点
assert(stack.back() == now_dx && "stack not match");
stack.pop_back();
// 再加入点
for (auto it = popped[now_dx].rbegin(); it != popped[now_dx].rend(); it++)
stack.push_back(*it);

now_dx++; // 移动到下一个可以得到的 dx
while (now_dx <= root && vdx[now_dx] == inf) now_dx++;

if (now_dx <= root)
assert(stack.back() == now_dx && "stack not match after next()");
};

// 二分,判断是否有点 x, f(x) 在直线下方,且 x >= dx.
auto BS = [&](int dx, int dy) {
if (dx == 0) return true;
std::reverse(stack.begin(), stack.end());
stack.push_back(V - 1);
int l = -1, r = stack.size() - 1;
while (l + 1 < r) {
int mid = l + ((r - l) >> 1);
int middx = stack[mid];
int nxtdx = stack[mid + 1];
if (cost(middx, dy) > cost(nxtdx, dy)) l = mid;
else r = mid;
}
bool res = cost(stack[r], dy) <= C;
stack.pop_back();
std::reverse(stack.begin(), stack.end());
return res;
};

for (int dy = V - 1; dy >= 1 && now_dx <= root; dy--) {
if (vdy[dy] == inf) continue; // 跳过无法取到的 dy.
// std::fprintf(stderr, "dy=%d, now_dx=%d\n", dy, now_dx);
while (now_dx <= root && BS(now_dx, dy)) {
ans = std::max(ans, 1ll * dy * now_dx);
next();
}
}

std::cout << ans << '\n';
}
}