CF1814D Balancing Weapons

发布时间 2023-07-11 18:15:07作者: dbxxx

CF1814D Balancing Weapons

原题明显可以转化为:

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

一个小区间合法,等价于所有元素都存在一个非 \(0\) 的倍数点落在这个小区间中。

我们尝试标记所有落在 \(S(x)\) 内部非 \(0\) 倍数点。对于 \(a_i\) 的倍数点 \(j \in S(x)\),我们记录 \(j\) 这个位置是 \(a_i\) 的倍数点。注意,一个位置可以是多个 \(a_i\) 的倍数点。

我们不妨调整 \(S(x)\) 的定义为 \([\max(p_x - k, 1), p_x + k]\)。这样以来,如果一个数的倍数点落在这个区间内,它一定直接非零。那么非零这个条件就不用强调了。而且这样以来会减少长度为 \(k\) 的小区间的数量,同时保证正确性,有助于减少常数(本题卡常)。

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

不小于 \(L\) 的最小 \(t\) 倍数点为 \(u = \left\lceil\dfrac L t \right\rceil \times t\),不大于 \(R\) 的最大 \(t\) 倍数点为 \(v = \left\lfloor\dfrac R t \right\rfloor \times t\)

\(u\)\(v\) 按照 \(t\) 的步长跳跃即可枚举到所有 \(t\)\([L, R]\) 内的倍数点,并且无多余枚举。

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

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

【思路一】

性质:如果存在 \(a_i \le k\),则任何一个长度为 \(k\) 的小区间,都会存在一个 \(a_i\) 的倍数点。

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

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

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

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

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

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

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

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

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

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

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

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

【思路二】

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

  • 不调整。
  • 调整到不大于 \(p_x\) 的最大 \(a_i\) 倍数点 \(u_i\)(如果未落入 \(S(x)\) 就忽略)。
  • 调整到不小于 \(p_x\) 的最小 \(a_i\) 倍数点 \(v_i\)(如果未落入 \(S(x)\) 就忽略)。

证明:

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

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

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

对于 \(v_i\),同理可证将 \(p_i\) 调整到 \(v_i + ta_i\) 不劣于 \(v_i\)\(u_i\)\(v_i\) 之间一定不存在其它 \(a_i\) 倍数点了,所以所有 \(p_i\) 的调整方案都已被讨论,\(u_i\)\(v_i\) 两种调整方案不劣于其它调整方案。

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

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

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

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

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

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

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

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

然后进行如下维护:

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

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

那么这个做法也做完了,复杂度同理是 \(\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;
}

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