专栏文章

[KOI 2025 #2] 序列与查询

P13523题解参与者 23已保存评论 22

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
22 条
当前快照
1 份
快照标识符
@mions7zq
此快照首次捕获于
2025/12/02 22:13
3 个月前
此快照最后确认于
2025/12/02 22:13
3 个月前
查看原文

子问题 1

当给定一个查询时,可以固定 ii 并增加 jj,从而枚举所有连续子区间并计算它们的和。输出其中的最大值即可。时间复杂度为 O(QN2)O(QN^2)

子问题 2

我们定义数组 Si=j=1i1AjS_i = \sum_{j=1}^{i-1} A_j。这可以通过递推式 Si+1=Si+AiXS_{i+1} = S_i + A_i - XO(N)O(N) 时间内计算出来。
当给定查询 XX 时,问题的答案是 max0j<iN(Si+iXSjjX)=max1iN(Si+iX(min0ji1Sj+jX))\max_{0 \le j < i \le N} (S_i + iX - S_j - jX) = \max_{1 \le i \le N} (S_i + iX - (\min_{0 \le j \le i-1} S_j + jX))。我们定义数组 Di=minj<i(Sj+jX)D_i = \min_{j<i} (S_j + jX)。这可以通过递推式 Di+1=min(Di,Si+iX)D_{i+1} = \min(D_i, S_i + iX)O(N)O(N) 时间内计算出来。问题的答案是 max1iN(Si+iXDi1)\max_{1 \le i \le N} (S_i + iX - D_{i-1}),并且可以在 O(N)O(N) 时间内计算。时间复杂度为 O(QN)O(QN)

子问题 3

我们定义 MlM_l 为长度为 ll 的子数组中和的最大值。数组 MM 可以在 O(N2)O(N^2) 时间内计算出来。现在,问题的答案是 maxl=1N(Ml+lX)\max_{l=1}^{N} (M_l + lX)。如果直接计算,每个查询需要 O(N)O(N) 的时间,需要进行优化。
当我们在二维平面上绘制 NN 个点 (xl,yl)=(l,Ml)(x_l, y_l) = (l, M_l) 时,对于查询 PP,我们想计算 xlP+ylx_l \cdot P + y_l 的最大值(输入中用 XX 表示,但为了避免混淆,这里用 PP 表示)。当我们求出这 NN 个点的凸包(convex hull)时,可以发现,取得最大值的点是在凸包上画一条斜率为 P-P 的直线时的切点。这样的切点可以通过求出凸包的上半部分(upper convex hull),并对每条直线使用二分搜索在 O(logN)O(\log N) 时间内找到。时间复杂度为 O(N2+QlogN)O(N^2 + Q \log N)

子问题 6

快速计算数组 MM 是很困难的,因此需要其他方法。
让我们考虑一个与子问题 3 的算法等价但效率稍低的方法:当在二维平面上给定 N(N+1)/2N(N+1)/2 个点 {(ij,SiSj)}0j<iN\{(i - j, S_i - S_j)\}_{0 \le j < i \le N} 时,对于查询 PP,问题答案就是 xlP+ylx_l \cdot P + y_l 的最大值。在求出所有 N(N+1)/2N(N+1)/2 个点的上凸包之后,可以使用与子问题 3 相同的二分搜索,在 O(logN)O(\log N) 时间内计算每个查询。但是,由于点的数量太多,求凸包并不容易。
我们用分治法来解决这个问题。定义函数 f(L,R)f(L, R) 返回点集 {(ij,SiSj)}Lj<iR\{(i-j, S_i - S_j)\}_{L \le j < i \le R} 的上凸包。定义 M=(L+R)/2M=(L+R)/2,然后调用 f(L,M)f(L, M)f(M+1,R)f(M+1, R),就可以得到 iM,jLi \le M, j \ge L 以及 i>M,j>Mi > M, j > M 情况下凸包的候选点。因此,我们计算点集 {(ij,SiSj)}LjM<iR\{(i-j, S_i - S_j)\}_{L \le j \le M < i \le R} 的上凸包,并与递归计算出的凸包候选点合并,然后再次计算这些点的上凸包并返回。
某个点 (ij,SiSj)(i-j, S_i - S_j) 在上凸包上,等价于存在一个斜率 pp,使得 (ij)p+SiSj(i-j)p + S_i - S_j 的最大值在该点唯一取得。(换句话说,所有其他点的 (ij)p+SiSj(i-j)p + S_i - S_j 的值都比该点小)。为此,Si+ipS_i + ipi[M+1,R]i \in [M+1, R] 中必须是最大的,而 Sj+jpS_j + jpj[L,M]j \in [L, M] 中必须是最小的。这等价于,我们只需要考虑在 M+1iRM+1 \le i \le R 中位于上凸包上的点 (i,Si)(i, S_i),以及在 LjML \le j \le M 中位于下凸包上的点 (j,Sj)(j, S_j)
当我们求出点集 {(i,Si)}i=M+1R\{(i, S_i)\}_{i=M+1}^R 的上凸包后,可以使用凸包上的相邻点来计算每个点取得最大值时的 pp 的范围。同样地,当我们求出点集 {(j,Sj)}j=LM\{(j, S_j)\}_{j=L}^M 的下凸包后,可以计算每个点取得最小值时的 pp 的范围。根据上述逻辑,我们只需计算那些 pp 的范围重叠的 (i,j)(i, j) 对即可,这与归并排序中的合并过程类似,可以用非常短的代码来解决。(这种计算被称为 Minkowski Sum。)
分治算法由于在求凸包的过程中需要排序,因此需要 T(N)=2T(N/2)+O(NlogN)=O(Nlog2N)T(N) = 2T(N/2) + O(N \log N) = O(N \log^2 N) 的时间。因此,时间复杂度为 O(Nlog2N+QlogN)O(N \log^2 N + Q \log N)

子问题 7

上述分治算法可以在 O(NlogN)O(N \log N) 的时间内实现。首先,在求点集 {(i,Si)}i=M+1R\{(i, S_i)\}_{i=M+1}^R 的上凸包时,可以自然地按 ii 递增的顺序处理各点,然后使用 Monotone Chain 算法在 O(N)O(N) 时间内计算上凸包。下凸包也同样。此外,没有必要让 f(L,R)f(L, R) 精确地返回上凸包:在合并过程中得到的点可以不按任何顺序收集起来,最后再统一计算一次上凸包即可。最终,在处理查询之前计算上凸包时,由于每个点的 xx 坐标都在 [1,N][1, N] 范围内,因此可以用将每个 xx 坐标对应的 yy 坐标最大值记录在数组中的方式来代替排序。时间复杂度为 O((N+Q)logN)O((N+Q) \log N)
Remark 1. 由于输入限制较大,在使用 CCW 计算凸包等时,可能会发生 long long 范围溢出的问题。这个问题有四种不同的解决方法(按便利性递增的顺序列出)。示例代码是用第二种方法实现的。
  • 不使用 CCW,而是使用比较斜率的函数。由于查询值都是整数,斜率可以用整数商进行不精确的比较,而不会产生问题。
  • double 类型计算 CCW 的值,如果绝对值小于等于 101810^{18},则再用 long long 重新进行精确计算(允许溢出)。
  • long double 类型计算 CCW 的值。
  • __int128 类型计算 CCW 的值:本次比赛使用的 Biko 评测环境支持该类型。
Remark 2. 关于这个问题,先前已知的解法是 Sakai 在 2024 年的论文 [Sak24],其预处理时间为 O(Nlog2N)O(N \log^2 N)。本题解的解法在保持相同的空间复杂度和查询时间的同时,将预处理时间改进为 O(NlogN)O(N \log N)。此外,与论文中的方法相比,本题解的解法要简单得多。
References
[Sak24] Yoshifumi Sakai. A data structure for the maximum-sum segment problem with offsets. In 35th Annual Symposium on Combinatorial Pattern Matching (CPM 2024), pages 26-1. Schloss Dagstuhl-Leibniz-Zentrum für Informatik, 2024.
CPP
#include <bits/stdc++.h>
using namespace std;
using lint = long long;
using pi = array<lint, 2>;
#define sz(v) ((int)(v).size())
#define all(v) (v).begin(), (v).end()
#define cr(v, n) (v).clear(), (v).resize(n);

pi operator+(const pi &a, const pi &b) { return pi{a[0] + b[0], a[1] + b[1]}; }
pi operator-(const pi &a, const pi &b) { return pi{a[0] - b[0], a[1] - b[1]}; }
int ccw(pi a, pi b) {
	double apx = 1.0 * a[0] * b[1] - 1.0 * a[1] * b[0];
	if (fabs(apx) > 1e18)
		return apx > 0 ? 1 : -1;
	lint ans = a[0] * b[1] - a[1] * b[0];
	if (ans == 0)
		return 0;
	return ans > 0 ? 1 : -1;
}
int ccw(pi a, pi b, pi c) { return ccw(b - a, c - a); }

struct cht {
	vector<pi> ch;
	bool bad(pi a, pi b, pi c) { return ccw(b - a, b - c) <= 0; }
	void init(vector<pi> &a) {
		for (auto &x : a) {
			if (sz(ch) && ch.back()[0] == x[0])
				continue;
			while (sz(ch) >= 2 && bad(ch[sz(ch) - 2], ch.back(), x))
				ch.pop_back();
			ch.push_back(x);
		}
	}
	lint query(lint x) {
		int s = 0, e = sz(ch) - 1;
		while (s != e) {
			int m = (s + e) / 2;
			if (ch[m][0] * x + ch[m][1] > ch[m + 1][0] * x + ch[m + 1][1])
				e = m;
			else
				s = m + 1;
		}
		return ch[s][0] * x + ch[s][1];
	}
} cht;

vector<lint> a, dap;

void dnc(int l, int r) {
	if (l == r)
		return;
	int m = (l + r) / 2;
	dnc(l, m);
	dnc(m + 1, r);
	vector<pi> L, R;
	for (int j = m + 1; j <= r; j++) {
		pi p{j, a[j]};
		while (sz(R) >= 2 && ccw(R[sz(R) - 2], R.back(), p) >= 0)
			R.pop_back();
		R.push_back(p);
	}
	for (int j = m; j >= l; j--) {
		pi p{-j, -a[j]};
		while (sz(L) >= 2 && ccw(L[sz(L) - 2], L.back(), p) >= 0)
			L.pop_back();
		L.push_back(p);
	}
	pi beg = L[0] + R[0];
	vector<pi> d1, d2;
	for (int k = 0; k + 1 < sz(L); k++) {
		d1.push_back(L[k + 1] - L[k]);
	}
	for (int k = 0; k + 1 < sz(R); k++) {
		d2.push_back(R[k + 1] - R[k]);
	}
	vector<pi> delta(sz(d1) + sz(d2));
	merge(all(d1), all(d2), delta.begin(), [&](const pi &a, const pi &b) { return ccw(a, b) < 0; });
	for (int i = 0; i < sz(delta); i++) {
		dap[beg[0] - 1] = max(dap[beg[0] - 1], beg[1]);
		beg = beg + delta[i];
	}
	dap[beg[0] - 1] = max(dap[beg[0] - 1], beg[1]);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n, q;
	cin >> n >> q;
	cr(a, n + 1);
	cr(dap, n);
	for (int i = 0; i < n; i++) {
		lint z;
		cin >> z;
		a[i + 1] = a[i] + z;
	}
	fill(all(dap), -1e18);
	dnc(0, n);
	vector<pi> points;
	for (int i = 0; i < n; i++) {
		points.push_back({i + 1, dap[i]});
	}
	cht.init(points);
	for (int i = 0; i < q; i++) {
		lint x;
		cin >> x;
		cout << cht.query(x) << "\n";
	}
}

评论

22 条评论,欢迎与作者交流。

正在加载评论...