CF1814D Balancing Weapons

CF1814D Balancing Weapons

原题明显可以转化为:

给定一个长度为 nn 的数组,初始为 pip_i。可以调整元素的值,但第 ii 个元素必须是 aia_i整数 倍,并且 必须严格大于 0\boldsymbol 0(保证 pip_i 初始满足此要求)。求至少修改多少个元素,可以让数组的极差不超过 kk


首先观察到答案和数组内部顺序无关,即所给数列可看做可重集。这是很经典重要的信息,提示我们第一步可以向两个方向思考:

  • 将数列排序。
  • 对数列求出权值数组,在权值数组上考虑问题。

不过,我的做法并不需要做上面任意一步,但上面这个思维是必须要有的。本题有别的做法必须先对这个数组排序,因为排序后保留不改的元素是一段区间,这是一个非常良好的性质。


首先,修改一定有解。设 LL 为所有 aia_ilcm\operatorname{lcm},我们一定可以将所有 pip_i 同时修改为 LL(注意这里并没有规定 pip_i 的上界,即使 LL 可能很大也无所谓)。那也就是说,修改 nn 个元素一定可以达成目的。

接下来考虑修改少于 nn 个元素的情况,这样以来一定存在一个元素没动。因此,考虑对这 nn 个元素,依次钦定第 ii 个元素不动考虑问题,就能把修改少于 nn 个元素的情况全都考虑到。

接下来钦定第 xx 个元素不动。那么必要条件是所有元素必须落入 [pxk,px+k][p_x - k, p_x + k]。当然,这个条件不充分,因为这样只能保证极差不超过 2k2k。下面,我们称这个区间为 S(x)S(x)。这个区间是建立在数组值域上的,而非下标。

在下面,区间 [l,r][l, r] 的长度被认为是 rlr - l,即一维坐标轴上点 ll 到点 rr 这条线段的长度,而非这个区间包含的整数数量 rl+1r - l + 1。那么,若干个数落在长度为 kk 的区间内,可以推得这些数的极差不超过 kk

为让所有元素极差不超过 kk,我们期望所有元素落在 S(x)S(x) 内部的一个长度为 kk 的小区间内。这样的小区间数量为 kk,可以通式地表示为 [l,l+k][l, l + k],其中 pxklpxp_x -k \le l \le p_x

全局小区间的数量不超过 nknk,这个量级可以承受。考虑依次钦定这 nknk 个小区间中的每一个,计算将所有元素落入这个小区间的最小代价,然后再对求出的所有最小代价取最小值,作为全局最小代价(答案)。这样一定可以考虑到所有情况,因为任何一种少于 nn 个元素的修改方式,都能在 nknk 个小区间中找到一个小区间 PP,使得这种修改方式会将所有元素落入 PP 内。

关于 nknk 这个量级是否可承受:注意到这里确实没有限制 k\sum k,但是只要限制一个 n\sum n 就已经足够了。设每组数据规模分别为 n1,n2,n3,n_1, n_2, n_3, \ldotsk1,k2,k3,k_1, k_2, k_3, \ldots,最终的代价有 n1k1+n2k2+n3k3+Knn_1k_1 + n_2k_2 + n_3k_3 + \ldots \le K\sum n,其中 K=max(ki)K = \max(k_i),因此不用限制 k\sum k

nknk 已经将近到极限了,所以计算每个区间最小代价的复杂度不能超过 log\log 了(可以均摊)。然而事实上这题卡 log\log 的常数所以我就讲讲均摊 Θ(1)\Theta(1) 计算每个区间的方法吧。

枚举区间本身就已经需要区间长度的复杂度,直接爆炸了。所以关键就是找到区间和区间之间的关系加速计算,使得复杂度均摊,降低。

注意到,共 nknk 个小区间大体是分成 nn 组的,同一个 xxS(x)S(x) 内部的 kk 个小区间是一组。不同组的区间之间基本无关,而同组区间关系密切:[pxk,px][p_x - k, p_x][pxk+1,px+1][p_x - k + 1, p_x +1]k1k - 1 的长度完全重合,而 [pxk+1,px+1][p_x - k + 1, p_x +1][pxk+2,px+2][p_x - k + 2, p_x + 2] 之间也有这种关系……得益于这种高度的重合性,用 双指针 加速计算同组内区间的答案:

维护两个指针 l=pxkl = p_x - kr=pxr = p_x,然后同步向右移动 llrrkk 步,就扫描到了 S(x)S(x) 内部的这 kk 个小区间。如果我们能做到这 2k2k 次指针移动中,每次移动都能快速更新辅助计算区间最小代价的信息(这里要求 Θ(1)\Theta(1)),就能做到 Θ(k)\Theta(k) 计算出 kk 个小区间的答案,也即均摊 Θ(1)\Theta(1)

当然,这里需要提前知道 [pxk,px][p_x - k, p_x] 的信息作为初始状态。不过也可以这么理解:先令 l=pxkl = p_x - kr=l1r = l - 1,也即一个空区间,空区间内部信息一般很平凡。然后再令 rr 向右移动 k+1k +1 步,使得信息慢慢更新到 [pxk,px][p_x - k, p_x],然后再让 llrr 同步向右。这样以来就是 3k3k 次指针移动,影响较小。虽然我最后实际代码中,是直接计算初始区间,然后直接令 l=pxkl = p_x - kr=pxr = p_x 的;但是这个思想可以一定程度简化问题,后面的讲解可以不再考虑初始区间,只考虑指针移动时区间信息的变化,以及怎么快速维护这样的信息变化。

接下来我们关心小区间最小代价的计算方法。显然,已经在小区间内部的 pip_i 无需修改,不在的必须修改,所以只要统计小区间内部 pip_i 的数量 cc,代价就是 ncn - c。因此,我们应该维护小区间内部 pip_i 的数量 cc

那么双指针在移动时可以 Θ(1)\Theta(1) 地快速更新 cc 值吗?这不难做到。考虑记录 cnp(j)=i=1n[pi=j]\mathrm{cnp}(j) = \sum_{i = 1}^n [p_i = j],这里我们只关心 jS(x)j \in S(x),也即 cnp\mathrm{cnp} 看做长度为 2k+12k + 1 的数组(代码实际实现时下标手动平移到 02k0 \sim 2k 即可)。这个记录复杂度是 Θ(n)\Theta(n) 的。

注意虽然 S(x)S(x) 的长度是 2k2k,但是 S(x)S(x) 所含整数元素数量是 2k+12k + 1,所以 cnp\mathrm{cnp} 的长度(数组的长度指所含元素数)为 2k+12k + 1

对于夹在两个双指针之间的区间 [l,r][l, r],其 cc 值是 j=lrcnp(j)\sum\limits_{j = l}^r\mathrm{cnp}(j)。这个信息用双指针维护非常简单:当 rrr1r' - 1 移动到 rr' 时,cc 自增 cnp(r)\mathrm{cnp}(r')llll' 移动到 l+1l' + 1 时,cc 自减 cnp(l)\mathrm{cnp}(l')

上面的做法比较简单,但有漏洞:上面我们直接默认任何一个不在小区间内的元素,都能落入当前小区间的内部。但这个命题并不正确,因为本题中元素的调整方法在正整数域离散——必须以 aia_i 为步长。举个例子,ai=5a_i = 5,小区间 [7,9][7, 9]pip_i 无论如何调整都无法落入该小区间内。

如果出现了这种情况,小区间的 cc 可以正常计算,但因为所有元素无法都落入该小区间内,该小区间应当是不合法的,这个小区间的 nc\boldsymbol{n - c} 不应更新答案。所以,我们还需 快速维护一个区间是否合法。

一个小区间合法,等价于所有元素都存在一个正倍数点落在这个小区间中。

我们尝试标记所有元素的所有落在 S(x)S(x) 内部的正倍数点。对于 aia_i 的正倍数点 jS(x)j \in S(x),我们记录 jj 这个位置是 aia_i 的正倍数点。注意,一个位置可以是多个 ai\boldsymbol{a_i} 的正倍数点。

我们不妨调整 S(x)S(x) 的定义为 [ max(pxk,1),px+k][\boldsymbol{\max(p_x - k, 1)}, p_x + k]。这样以来,如果一个数的倍数点落在这个区间内,它一定是正数。那么正数这个条件就不用强调了。同时本题不会涉及到 <1<1 的值,所以这样调整能保证不会丢掉不应该丢的情况,也同时能减少该大区间内长度为 kk 的小区间的数量,有助于减少常数(本题卡常)。

这里先给出枚举且仅枚举到 tt 在区间 [L,R][L, R] 内的倍数点的思路(这是个常用技能):

不小于 LL 的最小 tt 倍数点为 u=Lt×tu = \left\lceil\dfrac L t \right\rceil \times t,不大于 RR 的最大 tt 倍数点为 v=Rt×tv = \left\lfloor\dfrac R t \right\rfloor \times t

uuvv 按照 tt 的步长跳跃即可枚举到所有 tt[L,R][L, R] 内的倍数点,并且无多余枚举。

考虑最坏的情况,ai=1a_i = 1。这样以来,S(x)S(x) 里的所有位置里,每个位置都是所有元素的倍数点。这样以来标记复杂度直接卡满到 2nk2nk,我们要处理 nn 组小区间,这样总复杂度直接 n2kn^2k 了,不可取。

这意味着若坚持这个做法,我们势必要放弃一些倍数点的记录。下面我给出两种思路,都需要发掘一些性质。

【思路一】

性质:如果存在 aika_i \le k,则任何一个在正整数域上连续的长度为 kk 的小区间,都会存在一个 aia_i 的倍数点。

该性质可以被一个更强的性质推出:任何一个长度为 aia_i 的小区间内,每个数 modai\bmod a_i 之后恰好构成 [0,ai1][0, a_i - 1] 的整数集。比较明显,不证明了。

因此,aika_i \le k 的元素不会影响任何一个小区间的合法性,只需检验是否所有 ai>ka_i > k 的元素都存在倍数点落入小区间内。下面,我们称 ai>ka_i > k 的元素为 重要元素

这里又有特别好的性质:单个重要元素的倍数点在任何一个长度不超过 k\boldsymbol k 的小区间最多出现一次。假设它出现了,并且是 tt,则 t+ait + a_itait- a_i 都不落在小区间内,其它的更不会。

这个性质又可以推出 单个重要元素的倍数点在任何 S(x)\boldsymbol{S(x)} 最多出现两次,因为 S(x)S(x) 的长度不超过 2k2k,拆分成两个长度不超过 kk 的小区间,用上面的性质证明即可。

于是,考虑预处理 cnm(j)\mathrm{cnm}(j)jj 是多少个 重要元素 的倍数点(只关心 jS(x)j \in S(x))。根据上面的讨论,这个预处理标记的过程复杂度是 2n2n

小细节:因为单个重要元素 aia_i 的倍数点在 S(x)S(x) 最多出现两次,所以只要尝试标记 ai\boldsymbol{a_i} 的不小于 S(x)\boldsymbol{S(x)} 左端点的最小倍数点】 以及 ai\boldsymbol{a_i} 的不超过 S(x)\boldsymbol{S(x)} 右端点的最大倍数点】,就能将 aia_iS(x)S(x) 内部的倍数点都标记好了。这里【尝试标记点 u\boldsymbol u,意思是如果点 uu 落在 S(x)S(x) 内就标记,否则忽略。

然后维护信息:当前区间内,存在 ss 个重要元素的倍数点,这里需保证单个重要元素的多个倍数点只被计数一次。

因为长度为 kk 的小区间内部,单个重要元素的倍数点并不会出现多次。因此,当 rl=kr - l = k 时,信息 s=j=lrcnm(j)s = \sum\limits_{j = l}^r\mathrm{cnm}(j)

维护同 cnp\mathrm{cnp}:在 rrr1r' - 1 转移到 rr' 时,ss 自增 cnm(r)\mathrm{cnm}(r')llll' 转移到 l+1l' + 1 时,ss 自减 cnm(l)\mathrm{cnm}(l')

而一个区间合法,当且仅当 ss 与重要元素的数量相等,该判定平凡。

那么本题就做完了。考虑复杂度。

处理每组区间,预处理 cnp\mathrm{cnp}cnm\mathrm{cnm},复杂度 Θ(n)\Theta(n);双指针扫描复杂度 Θ(k)\Theta(k)。共有 nn 组区间,所以复杂度为 Θ(n2+nk)\Theta(n^2 + nk)

【思路二】

该思路的关键在于发掘:钦定 pxp_x 不动时,对于任意一个 pip_i,做以下三种策略之一不劣于做其它策略:

  • 不调整。
  • 调整到不大于 pxp_x 的最大 aia_i 倍数点 uiu_i(如果未落入 S(x)S(x) 就忽略)。
  • 调整到不小于 pxp_x 的最小 aia_i 倍数点 viv_i(如果未落入 S(x)S(x) 就忽略)。

证明:

考虑钦定 pxp_x 不动的策略,你钦定所有元素落入的小区间肯定是包含 pxp_x 的,左端点在 pxp_x 左侧,右端点在 pxp_x 右侧的一个区间。设它是 [l,l+k][l, l + k],即 lpxl \le p_xl+kpxl + k \ge p_x

一个元素一旦被调整,就一定会产生 11 的代价,而与它调整的目标位置无关。

如果我们将 piuitaip_i \ne u_i - ta_i 调整到 uitaiu_i - ta_i,它能落入 [l,l+k][l, l + k],那么有 uitailu_i - ta_i \ge l,于是 uilu_i \ge l。又因为 uipxl+ku_i \le p_x \le l + k,所以 uiu_i 也一定能落入 [l,l+k][l, l + k]。因为 piuitaip_i \ne u_i - ta_i,所以将 pip_i 调整到 uitaiu_i - ta_i 也会产生代价,不劣于将 pip_i 调整到 uiu_i

对于 viv_i,同理可证将 pip_i 调整到 vi+taiv_i + ta_i 不劣于 viv_iuiu_iviv_i 之间一定不存在其它 aia_i 倍数点了,所以所有 pip_i 的调整方案都已被讨论,uiu_iviv_i 两种调整方案不劣于其它调整方案。

当然注意,如果 pi=uitaip_i = u_i - ta_i,它可能比 pip_i 调整到 uiu_i 优秀,因为保持 pip_i 不动没有代价,调整到 uiu_i 则有。因此,还有一种策略是不调整,也需要考虑。

综上,我们手动强化条件,任何一个 pip_i 都只能保持 pip_i,或者调到 uiu_i,或者调到 viv_i。根据上面的讨论,这样的强化不会让原题的最优解发生变动,是可取的。如果这里 pip_iuiu_iviv_i 三个点中,某些点不落在 S(x)S(x) 里,就直接忽略。这三个点之间可能有相同的点,可以去重一下。

这样以来,任何一个 aia_i 都只用标记最多三个倍数点,我们称之为 重要倍数点

我们尝试沿用思路一的策略,设 cnm(j)\mathrm{cnm}(j)jj 是多少个 aia_i 的某个重要倍数点。然后考虑维护双指针所夹区间内【有多少个元素,存在一个重要倍数点落在该区间】,记这个值为 ss,则区间合法当且仅当 s=ns = n

然而,rl=kr -l = k 时,我们不保证 [l,r][l, r] 的答案 s=j=lrcnm(j)s = \sum_{j = l}^r \mathrm{cnm}(j),这是因为一个元素的重要倍数点可能落在长度为 kk 的小区间内多次,性质并不像思路一那么美好。

不过,这个问题其实等价于经典的数颜色问题。我们考虑 nn 种元素对应 nn 个颜色,jjaia_i 的某个重要倍数点等价于 jj 这个位置有 ii 这种颜色(一个位置可以有多种颜色)。然后问一个区间内部有多少种颜色。

思路一保证了一种颜色只出现在目标区间一次,所以问题简化成对颜色数求和。但事实上,没有这个条件,数颜色种数这个问题用双指针维护也是相当经典简单的。

首先我们更改状态,cnm(j)\mathrm{cnm}(j) 修改成一个集合,它的内容是 jj 这个位置的所有颜色种类。然后我们在双指针维护时,新开一个数组 cnt(i)\mathrm{cnt}(i),下标域 [1,n][1, n] 的正整数,代表第 ii 种元素在当前双指针所夹区间内出现多少次。

然后进行如下维护:

  • rrr1r' - 1 转移到 rr’ 时,将 rr' 处的所有颜色 ii 进行如下操作:
    • 如果 cnt(i)=0\mathrm{cnt}(i) = 0,则 ss 自增 11
    • cnt(i)\mathrm{cnt}(i) 自增 11
  • llll' 转移到 l+1l' + 1 时,将 ll' 处的所有颜色 ii 进行如下操作:
    • cnt(i)\mathrm{cnt}(i) 自减 11
    • 如果 cnt(i)=0\mathrm{cnt}(i) = 0,将 ss 自减 11

上面的转移正确性比较明显。其复杂度虽然不能保证每次运动 Θ(1)\Theta(1),但可以保证指针运动的全过程中,复杂度不会超过标记出的所有颜色数,也即 3k3k。均摊下来还是 Θ(1)\Theta(1) 的。

那么这个做法也做完了,复杂度同理是 Θ(n2+nk)\Theta(n^2 + nk)


下面给出做法一的 AC 代码,最大 234 ms,无任何故意的卡常,还是比较优秀的。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-07-11 11:17:47 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-07-11 17:58:41
 */
#include <bits/stdc++.h>
#define int long long
inline int read() {
	int x = 0;
	bool f = true;
	char ch = getchar();
	for (; !isdigit(ch); ch = getchar())
		if (ch == '-')
			f = false;
	for (; isdigit(ch); ch = getchar())
		x = (x << 1) + (x << 3) + (ch ^ '0');
	return f ? x : (~(x - 1));
}
inline bool gmi(int &a, int b) {
	return b < a ? a = b, true : false;
}

const int N = 3005;
int a[N], p[N];
inline int solve() {
	int n = read(), k = read();
	int ans = n, cnt = 0;
	for (int i = 1; i <= n; ++i) {
		a[i] = read();
		cnt += a[i] > k;
	}
	for (int i = 1; i <= n; ++i)
		p[i] = read() * a[i];
	for (int x = 1; x <= n; ++x) {
		std :: vector <int> cnp((k << 1) + 1), cnm((k << 1) + 1);
		// 注意这里一定要开成 k * 2 + 1
		// 如果你平移到从 1 开始,那就得开成 k * 2 + 2
		int L = std :: max(p[x] - k, 1LL), R = p[x] + k;
		auto ins = [&](std :: vector <int>& v, int pos) {
			if (pos >= L && pos <= R)
				++v[pos - L];
		};
		for (int i = 1; i <= n; ++i) {
			ins(cnp, p[i]);	
			int u = (L + a[i] - 1) / a[i] * a[i], v = R / a[i] * a[i];
			if (a[i] > k) {
				ins(cnm, u);
				if (u != v)
					ins(cnm, v);
			}
		}
		int c = std :: accumulate(cnp.begin(), cnp.begin() + k + 1, 0LL);
		int s = std :: accumulate(cnm.begin(), cnm.begin() + k + 1, 0LL);
		for (int l = L, r = l + k; ;) {
			if (s == cnt)
				gmi(ans, n - c);
			++r;
			if (r > R)
				break;
			c += cnp[r - L] - cnp[l - L];
			s += cnm[r - L] - cnm[l - L];
			++l;
		} 
	}
	return ans;
}

signed main() {
	int T = read();
	while (T--)
		printf("%lld\n", solve());
	return 0;
}

如果您是从洛谷博客来这里的,觉得这篇题解解决了您的问题,写的不错的话,别忘了回到洛谷题解区,给我题解点个赞!

posted @   dbxxx  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示