第一课 基本算法

二分

ll l = L, r = R, mid, Res = -1; 
while (l <= r)
{
    ll mid = l + r >> 1; 
    if (check(mid)) Res = mid, r = mid - 1;
    else l = mid + 1; 
}
return Res; 

P1462

最大值最小。很常见的套路是二分这个值 \(x\),这样交费在 \(x\) 以上的城市就过不了了,暴力加边跑最短路即可。时间复杂度 \(O(m\log W)\)

P1419

有点难做这个题。先考虑二分答案 \(x\),很好的 trick 是给序列每个数都减去 \(x\),这样只需考虑是否存在区间和 \(\ge 0\) 且长度在 \([S,T]\) 间。ds 维护即可。单调队列或者 sgt 都可以。时间复杂度 \(O(n\log W)\)

P1083

区间修改区间查询 max 用 sgt 随便做。

由于只需要求出到哪天就断了。可以二分这个东西。这样过程中的区间修改用差分维护即可。时间复杂度 \(O(n\log m)\)

注意差分之后做前缀和就是原来的值了。差分数组初始化时不能把原来的 \(r_i\) 加上去。

P1314

看着挺乱的。其实可以发现 \(y\) 具有单调性。\(W\) 越大,\(y\) 越小。于是可以二分。

二分出 \(W\) 之后考虑咋计算 \(y\)。我们发现其实每个矿物的贡献是一定的。线性预处理出每个矿物的贡献。在每个区间进行计算的时候前缀和处理即可。

时间复杂度 \(O((n + m) \log W)\)

P1868

注意到 \(x, y \le 10^6\)。按天数 dp 是可行的。时间复杂度 \(O(T+n)\)

更通用的做法是按区间 dp。右端点排序,每次二分找出前面第一个不相交的区间即可。时间复杂度 \(O(n\log n)\)

P1493

法一

常见的套路是确定 \(A_0\)\(B_0\) 然后来计数。具体我们将梨子按 \(A\) 升序排。确定某个 \(A_i = A_0\) 后把 \([i,n]\) 提出来,记其长度为 \(m\)。随后再按 \(B\) 升序排,确定 \(B_j = B_0\) 后我们把式子拆开:$$ C_1(A_i - A_0) + C_2 (B_i - B_0) \le C_3 C_1A_i + C_2B_i \le C_3 + C_1A_0 + C_2B_0 $$

这样右边就是常数,令其为 \(k\)。只需要在 \([j+1,m]\) 中查有多少个数 \(\le k\)。这个用离散化后主席树或者离线 bit 都可以做。时间复杂度 \(O(n^2\log n)\)。常数很大。不过还是轻松通过了。Record

法二

考虑简单点的做法。仍然是确定 \(A_0\) 后将 \(B_0\) 升序排序。你会发现右侧其实是个关于 \(B_0\) 的一次函数。随着 \(B_0\) 减小,满足条件的 \(j\) 只会越来越少。那用大根堆维护即可。代码好些很多。时间复杂度 \(O(n^2\log n)\)

P4165

值域较小。考虑怎么做到二次方。

钦定 \(A_0\),仍然是拆式子:

\[C_1(A_i - A_0) + C_2(B_i - B_0) \le C_3 \]

\[C_1A_i - C_1A_0 + C_2B_i - C_3 \le C_2B_0 \]

\[\frac{C_1A_i - C_1A_0 + C_2B_i - C_3}{C_2} \le B_0 \]

那么得到了 \(B_0\) 的范围,\(B_0 \in [\frac{C_1A_i - C_1A_0 + C_2B_i - C_3}{C_2}, B_i]\)。当我们固定了 \(A_0,B_i\),可以推出 \(B_0\) 的取值范围。我们给这个范围内的 \(B\) 加一。那么遍历完所有 \(B_i\) 之后,每个 \(B_i\) 上的值就表示当前这个 \(A_0\)\(B_i = B_0\) 时,能选多少个球员。那么取最值即可。加一的操作用差分维护。时间复杂度 \(O(n^2 + W)\),其中 \(W\) 为值域。

三分

二分是在单调函数上使用。三分则是凸性函数。

while (r - l > eps)
{
	ld m1 = l + (r - l) / 3.0;
	ld m2 = r - (r - l) / 3.0; 
	// [l, m1, m2, r]
	
	if (func(m2) > func(m1)) l = m1;
	else r = m2; 
}
ld Res = (m1 + m2) * 0.5; // (l + r) * 0.5 也可以

P1883

本题难点在于证明 \(F(x)\) 为单谷函数。

由于 \(a \ge 0\)。所以 \(f(x)\) 要么是开口向上的二次函数,要么是一次函数。

考虑反证:假设 \(F(x)\) 有两个极值点。坐标分别为 \((x_1,y_1)\)\((x_2,y_2)\),不妨令 \(y_1 \le y_2\)。记两点分别为 \(f_p(x)\)\(f_q(x)\) 上。

\(p \ne q\)。且 \(f_p(x) = a_px^2 + b_px+c\)\(f_q(x) = a_qx^2 + b_qx + c\)。有 \(a_p \ne 0\)\(a_q \ne 0\)

由于 \((x_2,y_2)\)\(f_q(x)\) 的极值点,故 \(\forall x_0 \ne x_2\),有 \(f_q(x_0) > y_2 \ge y_1\),将 \(x_1 = x_0\) 代入,有 \(f_q(x_1) > y_1\),故 \((x_1,f_q(x_1))\) 必定在 \(f_q(x)\)\(F(x)\) 上,矛盾!

P3745

法一

由于我们只关心最晚的时间。考虑枚举这个时间 \(t\),那么 \(C\) 相关的不满意度很好计算。考虑如何把 \(b_i \ge t\)\(b_i\) 变成 \(t\)。自然想到分讨 \(A,B\) 关系。若 \(A \ge B\),直接减就是对的。否则可以用前面一部分的 \(b_i\) 来抵消,再直接减。维护两个前缀和即可。时间复杂度 \(O(n\log n + T)\),其中 \(T\)\(t_i\) 值域。

法二

注意到最晚时间越靠后,\(C\) 相关的不愉快度只会增大,而 \(A\)\(B\) 相关的不愉快度只会减小。于是这是一个凸函数,使用三分求最值即可。代码实现有些麻烦。时间复杂度 \(O(n\log T)\)

倍增

  1. 倍增主要利用的是任何数都可以被二进制分解这个性质。如果每次操作只跟上一次操作有关的话就可以使用倍增优化这个过程。一种操作做 \(n\) 次肯定能拆成做几个 \(2^p\) 次。枚举 \(p\)\(\log n \sim 0\),如果做 \(2^p\) 次后还要做就做,同时令 \(n \leftarrow n - 2^p\)。容易证明最后 \(n=1\),即已经到了不能做的极限状态。
  2. st 表擅长处理静态区间问题。把区间拆成若干个长度为 \(2\) 的幂次的区间,利用倍增思想进行转移。值得注意的是这类区间问题需要注意不重不漏,例如区间最大值,最小值这类重复计算不会出错的问题。这是因为 st 表计算区间 \([l,r]\) 时是使用 \([l,l+2^k-1]\)\([r-2^k+1,r]\) 的结果,其中 \(k=\log (r-l+1)\),结合而成的。这样类似区间和,区间 gcd 的问题 st 表是无法维护的。
  3. st 表带修是线性的。当然在开头、末尾可以做到 \(O(\log)\) 次。也可以结合根号分治,设定重构的阈值。小的重构,大的分裂利用小的计算,以做到根号复杂度。
  4. 实际上,倍增法唯一的要求是有可合并性。假设我们当前要删除 / 修改一些东西,我们发现操作有可合并性,那么我们对于每个大小为 \(2^i\) 段预处理出来一个答案,等到询问的时候,将预处理出的东西合并即可。
  5. 当操作次数确定的时候,可以直接使用二进制分解去做。这个时候因为 \(f_{i,p}\) 只和 \(f_{i,p-1}\)\(f_{f_{i,p-1},p-1}\) 有关,只保留上一层的 \(f\) 即可。可以使用滚动数组优化。这是一个示例:
ll p = 1; 
for (ll i = 1; i <= n; ++ i ) f[i][0] = nxt[i], g[i] = (m & 1) ? nxt[i] : i; 
for (ll j = 1; j <= log2(m); ++ j )
{
	for (ll i = 1; i <= n; ++ i ) 
		f[i][p] = f[f[i][p ^ 1]][p ^ 1]; 
	if ((m >> j) & 1)
	{
		for (ll i = 1; i <= n; ++ i )
			g[i] = f[g[i]][p]; 	
	}	
	p ^= 1; 
}

P3865

ST 表板子。记 \(f_{i,j}\) 表示 \([i, i+2^j-1]\) 的最值。枚举 \(j\) 转移即可。\(f_{i,j} = \max\{f_{i,j-1}, f_{i + 2^{j-1}, j-1}\}\)。在查询 \([x,y]\) 时,记 \(k = \log (y - x + 1)\),答案即为 \(\max\{f_{i, k}, f_{y - 2^k + 1, k}\}\)

当然 st 表也是可以这么写:\(f_{i,j}\) 表示 \([i - 2^j + 1,i]\) 的最值。

P1198

各种 ds 随便做的。不过这是 st 带修板子题。

\(f_{i,j}\) 表示 \([i - 2^j + 1,i]\) 的最值。每次在插入 \(n + 1\) 的时候,容易发现这对前面的 \(f\) 没有影响。于是在 \(\log n\) 时间内完成插入。总的时间复杂度 \(O(n\log n)\)

当然也可以翻转序列,这样就是正常的 \([i,i+2^j-1]\) 了。

P10680

好题。比较典。需要结合根分做。

注意到这个神秘的 \([l, l+2^k-1]\) 以及这个神秘的合并操作,启发我们使用 st 表。用 st 表可以高效合并两个一半区间。

问题在于它是带修的。这题里修改是线性的。考虑根分。查询是 \(O(1)\),修改是 \(O(n)\),使用根分均摊复杂度。只维护 \(2^j \le \sqrt n\) 的部分,此时 \(j = 10\)。所以当 \(j \le 10\) 时,暴力重构,否则分裂下去。可以证明时间复杂度 \(O(n\sqrt n)\),不会证。

P7167

倍增板子。维护 \(f_{i,j}\) 表示从盘子 \(i\) 往后跳 \(2^j\) 步到哪个盘。单调栈预处理出 \(f_{i,0}\)

再用 \(g_{i,j}\) 记录中间的和。直接转移即可。那么询问时递减枚举 \(j\),若 \(V > g_{R,j}\) 就令 \(V \leftarrow V - g_{R,j}\) 即可。时间复杂度 \(O((n+q)\log n)\)

P4155

这么能串咋不串完。

一开始没想清楚。我们把士兵和边防站破环为链。对于第 \(i\) 个士兵,要考虑的就是 \([i, i + n - 1]\) 范围内的士兵。

考虑单个士兵咋做。把士兵按左端点递增排序。假设当前选了某个士兵 \(i\),覆盖范围是 \([l,r]\),那么下一步选择的必定是左端点 \(\le r\) 的士兵中右端点最大的。不妨记这个士兵为 \(j\)。也就是说,对于士兵 \(i\),下一步选的肯定是 \(j\)。同理士兵 \(j\) 也可以得到他下一步必定选的士兵 \(k\)。那么就有 \(O(n^2)\) 的暴力了。

每个士兵都要花费线性时间跳一次,显然不划算。考虑直接倍增。\(f_{i,j}\) 表示士兵 \(i\) 往后选 \(j\) 次到达的士兵。转移是容易的。查询 \(i\) 的时候 \(j\) 递减来选,选到最后一个在 \(i\) 左侧的士兵。查询代码:

void zmjjkk(ll x)
{
//	cout << "wkzkbl " << x << ' ' << s[x].id << '\n'; 
	ll p = x, Res = 0; 
	for (ll i = log2(2 * n); ~i; -- i )
	{
		if (f[x][i] && s[f[x][i]].r < s[p].l + m) 
			x = f[x][i], Res += (1 << i); 
	}
	Res += 2; 
	kskbl[s[p].id] = Res; 
//	cout << "mpwzbyqsl " << Res << '\n'; 
}

P5465

不会。

由于任意点对的距离是固定的,原问题有前缀和性质。显然我们只需求 \(\sum_{y=l_i}^{r_i} dist(x_i,y)\) 的值。记 \(Ans_{x, q}\) 表示 \([q, x-1]\) 的答案。那么原答案就是 \(Ans_{x,l_i} - Ans_{x, r_i + 1}\)

容易发现对于点 \(x\),它下一步能跳的点总是一段区间。假设它能往左跳到最远的点为 \(p\),往右的点为 \(q\),那么 \([p,q]\) 它都能跳到。

考虑怎么求 \(p\)。第一次跳跃时,显然 \(p = l_x\),而 \(q = \max\{r_i\}\),其中 \(i > x\)\(l_i <= p\)。第二次跳跃时,\(p' = \min_{i=p}^{q}\),为了方便直接令 \(q \leftarrow n\),这是因为 \((q, n]\)\(l > x\),不影响答案。那么 \(p' = \min_{i=p}^{n} l_i\)

那么对于第 \(i\) 次跳跃,假设能往左跳到的最远的是 \(p\),那么第 \(i + 1\) 次跳跃能跳到最远的就是 \(\min_{j=p}^{n} l_j\)。这样每次跳跃就跟上一次跳跃有关,那么可以倍增了。记 \(f_{i,j}\) 表示 \(i\)\(2^j\) 步能到的最远的点。再用 \(g_{i,j}\) 表示 \([f_{i,j}, i-1]\) 的答案。

需要注意的是,这题能倍增是因为第二次操作及以后,操作的本质都是一样的。于是不妨忽略掉第一次操作。在查询时一开始就做一次操作即可。

预处理 \(f_{i,0} = \min_{p=i}^n l_p,g_{i,0} = i-f_{i,0}\),转移有:

\[f_{i,j} = f_{f_{i,j-1},j-1} \]

\[g_{i,j} = g_{i,j-1} + g_{f_{i,j-1}, j-1} + 2^{j-1}(f_{i,j-1} - f_{i,j}) \]

查询的时候咋办呢?假设当前求的是 \(Ans_{x,p}\)。在跳的时候,假设当前从 \(x\)\(t\) 次跳到 \(i\),从 \(i\) 跳到 \(f_{i,j}\) 的时候的贡献是什么呢?显然 \(g_{i,j}\) 要算上,并且从 \(x\) 跳到 \(i\) 的点还需要 \(t\) 次。贡献就是 \(g_{i,j} + (i - f_{i,j}) \times t\)。同时令 \(x \leftarrow f_{x,i}\)\(t \leftarrow t + 2^j\)

ll Q(ll x, ll L)
{
	if (l[x] <= L) return x - L; 
	ll Res = x - l[x], step = 1;
	x = l[x];  
	for (ll i = log2(n); ~i; -- i )
	{
		if (f[x][i] > L)
		{
			Res += g[x][i];
			Res += (x - f[x][i]) * step; 
			step += (1 << i); 
			x = f[x][i]; 
		}
	}
	Res += (x - L) * (step + 1);
	return Res; 
}

for (ll i = n - 1; i; -- i ) l2[i] = min(l2[i], l2[i + 1]); 
for (ll i = 2; i <= n; ++ i )
{
	f[i][0] = l2[i], g[i][0] = i - f[i][0];
}	
for (ll p = 1; p <= log2(n); ++ p )
{
	for (ll i = 2; i <= n; ++ i )
	{
		f[i][p] = f[f[i][p - 1]][p - 1]; 
		g[i][p] = g[i][p - 1] + g[f[i][p - 1]][p - 1] + (f[i][p - 1] - f[i][p]) * (1 << (p - 1)); 
	}
}

P5012

很好的题目。

首先 \(x\) 只有 \(n\) 个取值。那么 \(O(n^2)\) 很好做。可以获得 \(60\) 分。

直接做好像很困难,考虑预处理。递增枚举 \(x\),由于加入的数总是越来越多,那么用并查集维护连续段即可。这样对于每个 \(x\),可以得到两元组 \((t, y)\) 表示共 \(t\) 个连续段,连续段平方和为 \(y\),答案就是 \(\frac{y}{x}\)

那么询问的时候,在 \(t \in [l,r]\) 找最大的 \(\frac{y}{x}\)。那么这就是个 rmq 问题。可以使用数据结构解决。

建 st 表询问是 \(O(1)\),建表是 \(O(n\log n)\),空间会爆。考虑平衡一下复杂度。容易想到分块。分 \(\sqrt n\) 个块,平衡时间复杂度到 \(O(T\sqrt n)\)。可以通过。代码比较难写,细节比较多。时间复杂度 \(O(n\sqrt n + T\sqrt n)\)

P3509

比较板子。难点可能在于如何求每个点第 \(k\) 近的点。

首先记距点 \(i\)\(k\) 近的点集为 \(S_i\)。容易证明 \(S_i\)\(i\) 往左取若干个数,再往右取若干个得到的。不妨记其为 \(i\in [L,R]\)。那么第 \(k\) 近的点就是 \(L\) 或者 \(R\)

容易想到二分。假设第 \(k\) 近是 \(R\),二分这个 \(R\),在左端点求出 \(\le p_R\) 的数个数,判断是否为 \(k\) 个即可。同理假设为 \(L\) 再做一次。这样就是 \(O(n\log^2 n + n\log m)\) 了。有点难通过。

考虑更聪明的做法,注意到 \(S_i\)\(S_{i+1}\) 的差别不是很大。考虑怎么线性做。在 \(i \leftarrow i+1\) 的时候,原本 \(i\) 右侧的点集到 \(i+1\) 的距离变小,\(i\) 左侧的点集到 \(i + 1\) 的距离变大。因此只要不断把右边的点加进来,左边的点扔掉即可。这样就是线性的。时间复杂度 \(O(n + n\log m)\)

int l = 1, r = k + 1; nxt[1] = k + 1; 
for (int i = 2; i <= n; ++ i ) 
{
	while (r < n && p[r + 1] - p[i] < p[i] - p[l]) ++ r, ++ l; 
	if (p[r] - p[i] <= p[i] - p[l]) nxt[i] = l;
	else nxt[i] = r; 
}

然后因为本题的操作次数是确定的,可以直接用二进制分解做,从小到大枚举。又因为本题卡空间,使用滚动数组即可:

ll p = 1; 
for (ll i = 1; i <= n; ++ i ) f[i][0] = nxt[i], g[i] = (m & 1) ? nxt[i] : i; 
for (ll j = 1; j <= log2(m); ++ j )
{
	for (ll i = 1; i <= n; ++ i ) 
		f[i][p] = f[f[i][p ^ 1]][p ^ 1]; 
	if ((m >> j) & 1)
	{
		for (ll i = 1; i <= n; ++ i )
			g[i] = f[g[i]][p]; 	
	}	
	p ^= 1; 
}

分治

顾名思义。把一个大问题划分成几个子问题解决。

分治一般需要符合一下几个特征:

  1. 平衡子问题。子问题的规模大致相同。
  2. 独立子问题。子问题之间是相互独立的。如果子问题之间有联系就是 dp。
  3. 子问题可合并。独立的子问题的结果可以高效合并。

那么分治法的流程就是:划分子问题 -> 解决独立子问题 -> 合并子问题答案。

P1177

归并排序(Merge Sort)

先知道什么是归并。考虑一个问题:

给定两个有序的序列 \(a,b\),将两者合并成一个新序列 \(c\),使得 \(c\) 也是有序的。

这个还是比较好做。对于 \(a,b\) 分别维护指针 \(i,j\),表示 \(a_i\)\(b_j\) 已经在 \(c\) 中。那么不断比较 \(a_{i+1}\)\(b_{j+1}\) 即可。这本质上就是归并。

那么考虑用分治的思想解决排序问题。每次将待排序的序列 \(c\) 划分成两个子序列 \(a,b\),对 \(a,b\) 继续划分下去。随后回溯时 \(a,b\) 分别已经有序了,此时再使用归并,合并 \(a,b\) 得到 \(c\)。这样就做完了。时间复杂度 \(O(n\log n)\)

void Msort(ll l, ll r)
{
	if (l == r) return ; 
	ll mid = l + r >> 1;
	Msort(l, mid); Msort(mid + 1, r); 
	
	ll i = l - 1, j = mid, len = 0;
	while (i < mid && j < r)
	{
		if (A[i + 1] <= A[j + 1]) ++ i, t[++ len] = A[i], Res += j - mid; 
		else ++ j, t[++ len] = A[j]; 
	}
	if (i < mid)
		for (++ i; i <= mid; ++ i ) t[++ len] = A[i], Res += r - mid; 
	if (j < r)
		for (++ j; j <= r; ++ j ) t[++ len] = A[j];
	for (ll k = 1; k <= len; ++ k ) A[l + k - 1] = t[k];  
}

归并排序的优点是它是稳定的,而且求逆序对很方便。

快速排序(Quick Sort)

这个思想本质上和分治是差不多的。在对序列 \(a\) 进行排序时,进行以下操作:

  1. 在序列里随机选一个元素 \(x\)
  2. 遍历区间,将 \(< x\) 的放进序列 \(a\),将 \(= x\)\(> x\) 的分别放进序列 \(b,c\)
  3. \(a,c\) 分别排序,随后按 \(a,b,c\) 的顺序还原序列。

平均时间复杂度 \(O(n\log n)\),最坏 \(O(n^2)\)

值得一提的是,stl 的 sort 进行了很多优化,使得其效率大大提升,可以认为它在大多数场景都优秀于归并排序。

void Qsort(ll l, ll r)
{
	if (l >= r) return ;
	ll v = A[rd(l, r) + l];
	ll lb = 0, lc = 0, ld = 0;
	for (ll i = l; i <= r; ++ i )
	{
		if (A[i] < v) B[++ lb] = A[i];
		if (A[i] == v) C[++ lc] = A[i];
		if (A[i] > v) D[++ ld] = A[i];
	}
	for (ll i = 1; i <= lb; ++ i ) A[l + i - 1] = B[i];
	for (ll i = 1; i <= lc; ++ i ) A[l + lb + i - 1] = C[i];
	for (ll i = 1; i <= ld; ++ i ) A[l + lb + lc + i - 1] = D[i];
	Qsort(l, l + lb - 1);
	Qsort(r - ld + 1, r);
}

P1045

考虑解决第一问。由于 \(2^P\) 末位 \(\ne 0\),所以 \(2^P-1\)\(2^P\) 位数肯定是一样的。只需求 \(2^P\) 的位数。进行一点推导:

\[\lceil \log_{10} 2^P \rceil = P\lceil \log_{10} 2 \rceil = P\log_{10} 2 + 1 \]

那么第一问做完。第二问就是高精快速幂板子。代码等学了高精再补 /dk

P1115

这也能分治。。。

考察一个区间 \([L,R]\),记其中点为 \(mid\),满足条件的最大子段有三种情况:

  1. 横跨 \(mid\)。这个可以在 \(mid\) 处枚举前后缀。
  2. \([L,mid]\) 中。这个可以分治解决。
  3. \((mid, R]\) 中。这个同样分治解决。

那么做完了。时间复杂度 \(O(n\log n)\)

P1429

感觉这个题不太像人类。。。

法一

各种人类智慧。将点按 \(x\) 排序。人类直觉,两点的 \(x\) 相差不会太多,那么枚举点 \(i\) 时往后枚举 \(B\) 个点当 \(j\) 维护 \((i,j)\) 答案。\(B \le 10\) 可以通过加强版。\(B\)\(350\) 可以在加强加强版得到 \(135\) 分(卡时间)这样我们在 \(O(nB)\) 的时间内可以得到这个问题的较优解,获得客观的分数。

法二

考虑降维。一维怎么做。把坐标 \(p_i\) 先排序。然后考虑分治。

仍然是套路,当前在求 \([L,R]\) 的答案,中点为 \(mid\)。先分治下去求 \([L,mid]\)\((mid,R]\) 的答案。咋合并捏?

不妨记 \([L,mid]\) 的答案为 \(d_1\)\((mid,R]\) 的答案为 \(d_2\)。记 \(d = \min(d_1,d_2)\)。然后我们想一下怎么求跨越 \(mid\) 的答案。由于已经有了个 \(d\),那么离 \(p_{mid}\) 距离 \(\ge d\) 的点就可以扔掉了。具体地,只需要考虑 \([p_{mid} - d, p_{mid} + d]\) 的点,容易证明两边只用取 \(1\) 个点,不然距离肯定不会更优。这样合并就是 \(O(1)\) 的,总的时间复杂度 \(O(n\log n)\)

来考虑二维。仍然是坐标按 \(x\) 排序,求 \([L,R]\) 的答案,中点为 \(mid\),两边更优的答案为 \(d\)

较一维不同的是,这个时候 \([p_{mid} - d, p_{mid} + d]\) 变成了几条竖线:

微信图片_20260606190930_36_12

考虑这个样例。这时候取 \(mid = 5\)\(d = 2\)。要解决 \(x \in [3,7]\) 的所有点。

尝试暴力做一下,把所有点提出来然后按 \(y\) 排序,枚举点 \(i\),往后枚举 \(j\) 直到 \(i,j\) 距离 \(\ge d\)。你发现这样直接过了。这是因为往后最多只有 \(6\) 个点。这是个很不是人类的证明:

假设枚举到点 \(i(5,4)\),即图中标的紫点,现在要处理的是橙色矩阵的部分,即大小为 \(2d \times d\) 的矩阵。现在要证明这个矩阵里最多只能放 \(6\) 个点,这 \(6\) 个点两两之间距离 \(\ge d\)。然后这个构造有点搞笑:把这个 \(2d \times d\) 的矩阵划分成 \(6\) 个大小为 \(\frac{1}{2}d \times \frac{2}{3}d\) 的小矩阵。然后每个矩阵只能放 \(1\) 个点,这是因为这个小矩阵里两点的最大距离为对角线 \(\frac{5}{6}d < d\),于是我们就证明了这个大矩阵里只能放 \(6\) 个点。

实现的时候可以把距离平方,最后再开根。用 sort 提取序列的话是 \(O(n\log^2 n)\) 的,使用归并可以做到 \(O(n\log n)\)

法二

妙妙做法。不是分治。本质上和分治是一个思想:
x0z8rt3y

我们用可重集维护一个长为 \(d\) 的矩形(图中粉色矩形)。其中 \(d\) 为前面得到的最优解。
那么对于当前的点 \(i\),显然矩形内的点才能和它产生贡献。那么我们把矩形内的点按 \(y\) 为关键字排序,同样是 \(y\in[Y_{p_i} - d,Y_{p_i}+d]\) 的点能产生贡献。这个可以在可重集内二分来求。法一我们已经证明了这个点数不会很大,是 \(\log n\) 量级的。于是我们在 \(O(n\log n)\) 的时间复杂度内解决了问题。

P5094

比较典的题。绝对值很难办,把奶牛按 \(x\) 排序消掉它。然后奶牛 \(i\)\([1,i)\) 的答案。考虑点对 \((j,i)\) 的答案。分讨 \(V_i,V_j\) 的大小,开两个权值 bit 维护即可。

然后因为这个题出现在分治这里,考虑一下怎么分治。仍然是套路,假设当前在求解 \([L,R]\) 答案,记中点为 \(m\)。考虑求解跨越 \(m\) 点对的答案。两边分别排序双指针维护即可,这样比 bit 好写的多。时间复杂度 \(O(n\log^2 n)\)

void solve(ll l, ll r)
{
	if (l == r) return ; 
	ll mid = l + r >> 1;
	solve(l, mid); solve(mid + 1, r); 
	for (ll i = l; i <= mid; ++ i ) t1[i - l + 1] = cow[i]; 
	for (ll i = mid + 1; i <= r; ++ i ) t2[i - mid] = cow[i]; 
	m1 = mid - l + 1; m2 = r - mid; 
	sort(t1 + 1, t1 + m1 + 1, pp2);
	sort(t2 + 1, t2 + m2 + 1, pp2);
	for (ll i = 1; i <= m1; ++ i ) g1[i] = t1[i].first, g1[i] = add(g1[i], g1[i - 1]); 
	for (ll i = 1; i <= m2; ++ i ) g2[i] = t2[i].first, g2[i] = add(g2[i], g2[i - 1]); 
	
	ll j = 0; 
	for (ll i = 1; i <= m2; ++ i )
	{
		while (j < m1 && t1[j + 1].second < t2[i].second) ++ j; 
		if (j) Res = add(Res, t2[i].second * ((t2[i].first * j % mod) - g1[j] + mod) % mod); 
	}
	j = 0; 
	for (ll i = 1; i <= m1; ++ i )
	{
		while (j < m2 && t2[j + 1].second <= t1[i].second) ++ j; 
		if (j) Res = add(Res, t1[i].second * (g2[j] - (t1[i].first * j % mod) + mod) % mod); 
	}
}

ps: 脑抽上了个前缀和,直接维护和就好了。懒得改了(

P2048

牛逼题目。
问题实际上是求 \(k\) 个不同的区间,要求长度在 \([L,R]\) 中,使得 \(k\) 个区间的区间和之和最大。
不妨固定右端点为 \(i\),那么左端点的取值范围 \(j\in[i-R+1,i-L+1]\),只需要在这里面求 \(s_i - s_{j-1}\) 的最大值,其中 \(s\) 为前缀和。这个可以用 ds 求。
这是一个很牛逼的 trick:记五元组为 \((w,i,l,r,p)\) 表示右端点为 \(i\),左端点的取值范围在 \([l,r]\) 内,取到最优解的位置为 \(j\),最优解为 \(w\)。那么用大根堆维护五元组,取 \(k\) 次堆顶。每次取出堆顶后将五元组分裂成 \((w',i,l,p-1,p')\)\((w'', i,p+1,r,p'')\) 即可。\(w',p',w'',p''\) 可以用 ds 求。这样我们在 \(O(n\log^2 n)\) 的时间解决了问题。
牛逼 trick。后续一般直接称这个 trick 为炒鸡钢琴。

P7143

真正的分治好题。
注意到答案只跟区间的长度有关,记 \(f_i\) 表示区间长度为 \(i\) 的答案。
依然是分治。把长度为 \(n\) 的区间分裂为两个长度分别为 \(l=m,r=n-m\) 的区间 \(A,B\)
那么求解 \(f_n\) 时,依旧是只需要求解两个端点分别在 \(A,B\) 的答案。记其为 \([x,y]\)。那么答案由 \([x,m]\)\((m,r]\) 两部分构成。然后注意到这两个分别是关于 \(m\) 的后缀和 \(m+1\) 的前缀,使用前缀和优化。
\(s_n\) 表示 \(\sum_{i=1}^n cover(i,n)\)\(p_n\) 表示 \(\sum_{i=1}^n cover(1,i)\)
转移:\(s_n = s_l + s_r + l - 1\)\(p_n = p_l + p_r + r - 1\),减 \(1\) 是因为对于整个区间,我们用 \(l\)\(r\) 两个进行了覆盖,这是不对的。
那么 \(f_n\) 就很好转移:
\(f_n\) = \(f_l + f_r + l * p_r + r * s_l\)。然后因为不同区间的长度只有 \(\log_2 n\) 个,记忆化一下就在 \(O(T\log^2 W)\) 的时间内解决了问题,多带一个 \(\log\) 是因为 map。

P7562

不会珂朵莉树。

考虑没有字典序的限制该咋做。实际上是一个贪心裸题,考虑按左端点排序,然后每次选最靠左的右端点。

posted @ 2026-05-30 17:25  RainyRadio  阅读(4)  评论(0)    收藏  举报