目录
[题目链接:1.近似gcd - 蓝桥云课](#题目链接:1.近似gcd - 蓝桥云课)

题目链接:1.近似gcd - 蓝桥云课
注:下述题目描述和示例均来自蓝桥云客
题目描述
小蓝有一个长度为 n 的数组 A=(a1,a2,⋯,an), 数组的子数组被定义为从 原数组中选出连续的一个或多个元素组成的数组。数组的最大公约数指的是数 组中所有元素的最大公约数。如果最多更改数组中的一个元素之后, 数组的最 大公约数为 g, 那么称 g 为这个数组的近似 GCD。一个数组的近似 GCD 可能 有多种取值。
具体的, 判断 g 是否为一个子数组的近似 GCD 如下:
-
如果这个子数组的最大公约数就是 g, 那么说明 g 是其近似 GCD。
-
在修改这个子数组中的一个元素之后 (可以改成想要的任何值), 子数 组的最大公约数为 g, 那么说明 g 是这个子数组的近似 GCD。
小蓝想知道, 数组 A 有多少个长度大于等于 2 的子数组满足近似 GCD 的 值为 g 。
输入格式
输入的第一行包含两个整数 n,g 用一个空格分隔, 分别表示数组 A 的长 度和 g 的值。
第二行包含 n 个整数 a1,a2,⋯,an 相邻两个整数之间用一个空格分隔。
输出格式
输出一行包含一个整数表示数组 A 有多少个长度大于等于 2 的子数组的近 似 GCD 的值为 gg 。
样例输入
5 3
1 3 6 4 10
样例输出
5
样例说明
满足条件的子数组有 5 个:
[1,3] : 将 1 修改为 3 后, 这个子数组的最大公约数为 3 , 满足条件。
[1,3,6] : 将 1 修改为 3 后, 这个子数组的最大公约数为 3 , 满足条件。
[3,6]:这个子数组的最大公约数就是 3 , 满足条件。
[3,6,4] : 将 4 修改为 3 后, 这个子数组的最大公约数为 3 , 满足条件。
[6,4]: 将 4 修改为 3 后, 这个子数组的最大公约数为 3 , 满足条件。
评测用例规模与约定
对于 20% 的评测用例, 2≤n≤ ;
对于 40% 的评测用例, 2≤n≤;
对于所有评测用例, 2≤n≤,1≤g,
≤
。

解法一:我称之为逆天
我们需要找出所有长度大于等于2的子数组,然后让这个子数组的近似GCD等于给定的g。那什么是近似GCD呢?就是说,最多可以修改其中一个元素,使得修改后的子数组的GCD等于g。这样的话,这个子数组的近似GCD就是g。那我们需要计算满足条件的子数组数量。
首先,我们要理解题意。比如,子数组中的元素,最多只能改一个元素,就能让整个子数组的GCD变成g,那么这个子数组就符合条件。那这个条件如何判断呢?
首先,一个子数组的近似GCD为g的条件可能是什么?可能的两种情况:要么这个子数组的原始GCD已经是g,在一个就是可以通过修改一个元素使得GCD变为g。
那我们需要遍历所有可能的子数组,检查每个子数组是否满足上述条件。但这样的话,时间复杂度会很高,因为子数组的数量是O(n²)级别的,对于较大的n来说可能无法处理,这样的方法肯定不行。所以我们就需要找到一种优化的方法。
那问题的关键就在于如何高效地计算满足条件的子数组数量。那有没有办法不用遍历每个子数组呢?
我们需要明确:当子数组的近似GCD是g时,意味着该子数组中最多有一个元素不被g整除。或者说,子数组中最多有一个元素不能整除g,或者即使有多个元素不整除g,但是它们可以被调整(比如替换其中一个元素为g的倍数),使得整个数组的GCD变成g。更准确的条件应该是:子数组中必须满足至少所有元素都整除g,或者存在恰好一个元素不整除g,这样我们修改那个元素为一个能被g整除的数,那么整个数组的GCD就可能变成g。
另一个角度,修改后的数组的GCD必须是g。那修改后的数组所有元素必须是g的倍数,并且它们的GCD是g。也就是说,修改后的数组中的每个元素都是g的倍数,并且整个数组的GCD恰好是g。
比如,假设我们有一个子数组,其中所有元素都是g的倍数,但它们的GCD是2g。这时,原来的GCD是2g,但题目要求的近似GCD是g。这时不满足条件,因为即使允许修改一个元素,比如将其中一个元素改成g,那么整个数组的GCD可能变成g。例如数组是[2g, 2g],修改其中一个为g,GCD变成g。
这个时候,这个子数组的近似GCD是g吗?是的。所以这时候,这个子数组应该被算作符合条件的。
这说明,我们需要的条件是:在修改最多一个元素之后,整个子数组的GCD是g。所以原来的子数组的GCD可能不是g,但修改之后可以变成g。
那么问题转化为:对于每个子数组,是否存在最多一个元素,使得修改该元素为某个值后,整个子数组的GCD等于g?
如何快速判断这一点?可能的思路是:对于每个子数组,统计其中无法被g整除的元素的数量。如果这个数量是0或者1又怎么办?
比如,假设子数组中的元素全能被g整除。此时,它们的GCD可能等于g或更大的数。例如,如果所有元素都是2g的倍数,那么它们的GCD至少是2g,所以此时的GCD不是g。这时候,这个子数组的近似GCD是否为g?比如,假设我们修改其中一个元素为g,那么整个数组的GCD将变为gcd(2g, 2g, ..., g) = g。所以这种情况下,这样的子数组应该被算作满足条件。
所以,如果子数组中所有元素都是g的倍数,那么可以通过修改其中一个元素为g,使得整个数组的GCD变为g。
那问题就简化为,所有满足以下条件的子数组:
**子数组中,最多有一个元素不是g的倍数。**或者,更准确地说,子数组中,最多有一个元素无法被g整除,或者所有元素都被g整除但它们的GCD可以通过修改一个元素变为g。
这时候,如何处理两种情况:
-
**子数组中所有元素都是g的倍数。**此时,无论它们的GCD是多少,只要可以通过修改一个元素,使得整个数组的GCD变为g。比如,原来的GCD是kg,这时候是否可以通过修改其中一个元素为g,使得新的GCD变为g。例如,数组是[2g, 2g],原来的GCD是2g。修改其中一个元素为g,那么新数组的GCD久是g了。所以这种情况下,所有元素都是g的倍数的子数组,无论原来的GCD是否为g,都符合条件。
-
**子数组中恰好有一个元素不是g的倍数。**此时,可以修改这个元素为g的倍数,那么剩下的元素都是g的倍数。比如,假设子数组中有一个元素不是g的倍数,其他都是g的倍数。修改这个元素为某个g的倍数,例如g。此时整个数组的GCD是原数组中其他元素的GCD和g的GCD。如果原数组中的元素的GCD是g,则总GCD是g。否则,比如原数组的GCD是kg,那么总GCD是gcd(kg, g) = g。所以无论原数组中其他元素的GCD是否为g,修改那个非g倍数的元素为g后,总GCD是g。所以这种情况下,这样的子数组是符合条件的。
那么最后,综合以上两种情况,满足条件的子数组的条件是:子数组中无法被g整除的元素的数量<=1。并且,当所有元素都能被g整除时,它们的GCD是否是g并不影响,因为可以通过修改一个元素为g来使得总GCD变为g。例如,原数组中所有元素都是2g,那么它们的GCD是2g。修改其中一个元素为g,那么总GCD是g。所以这样的子数组符合条件。
所以,结论是:只要子数组中无法被g整除的元素的数量<=1,那么该子数组满足条件。此时,无论原来的GCD如何,都可以通过修改最多一个元素,使得总GCD为g。这样,这样的子数组都被视为近似GCD为g的子数组?
那这样的话,我们的问题可以转化为:找出所有长度>=2的且其中无法被g整除的元素的个数<=1的,子数组。
我们试着推导一下?看样例输入:
样例输入是5 3,数组是1 3 6 4 10。
各个元素能否被3整除?
1%3=1 →不行。
3%3=0 →可以。
6%3=0 →可以。
4%3=1 →不行。
10%3=1 →不行。
现在,找出所有长度>=2的子数组,其中无法被g整除的元素数量<=1。
比如,第一个子数组是[1,3]。无法被3整除的元素数量是1个(1),符合条件。所以该子数组符合条件。样例说明中的第一个例子确实被算作正确。
再看第二个子数组[1,3,6]。无法被3整除的元素是1,数量为1。符合条件。
第三个子数组[3,6],无法被整除的元素数量是0,符合条件。
第四个子数组[3,6,4]:无法被整除的是4,数量是1。符合条件。
第五个子数组[6,4],无法被整除的是4,数量是1。符合条件。
还有没有其他符合条件的子数组?
比如,子数组[6,4,10],无法被整除的是4和10,数量是2。超过1,所以不符合条件。所以总数是5,与样例输出一致。
这说明上述结论是正确的。那问题就转化为统计所有长度>=2的子数组,其中无法被g整除的元素个数<=1。
既然简化了问题。那么,现在的问题是,如何高效地统计这样的子数组的数量。那现在,如何高效地计算所有长度>=2的子数组,其中无法被g整除的元素个数不超过1个?
这个问题可以转化为滑动窗口的问题吗?
比如,我们可以遍历数组中的每个元素,维护一个窗口,其中包含最多一个无法被g整除的元素。当窗口右端扩展时,如果新元素无法被整除,则计数器加1。如果计数器超过1,则移动左指针,直到计数器<=1。然后,对于每个窗口的右端点,统计所有可能的左端点,使得窗口内的元素个数>=2,并且无法被整除的数量<=1。
所以滑动窗口中的不能被g整除的元素的数目不能超过1。当窗口满足条件时,统计其中所有长度>=2的子数组的数量。
具体的思路是:
使用双指针left和right,维护一个窗口,其中非g倍数的数目<=1。当窗口中的非g数目超过1时,移动左指针。
对于每个右指针的位置,找到最大的左指针的位置,使得窗口内非g倍数的数目<=1。然后,对于这个窗口,计算其中所有长度>=2的子数组的数量。
例如,当前窗口是[left, right],其中非g数目<=1。那么,以right为右端点的所有可能的子数组是[left, right], [left+1, right], ..., [right-1, right]。其中,长度>=2的子数组数量是 right - left。例如,如果窗口是[0,3],那么以3为右端点的子数组可能有 [0,3](长度4),但需要长度>=2,所以从left到right的长度是3-0+1=4。但我们需要子数组的长度>=2,所以符合条件的子数组数目是 (right - left) - 0 +1 -1 = right - left。例如,当窗口长度为k(k>=2),则其中符合条件的子数组数目为 k-1。比如,窗口长度为2的话,符合数目是1(子数组本身)。窗口长度为3的话,数目是2(比如子数组[start, end], [start+1, end], [start+2, end],其中长度>=2的有两个)。
因此,总的时间复杂度可以是O(n),通过滑动窗口的方式。
那如何实现?
-
预处理数组,将每个元素转换为是否是g的倍数。例如,创建一个数组bad,其中bad[i] = 0表示a[i]是g的倍数,1表示不是。
-
使用滑动窗口,维护当前窗口[left, right]内的bad之和。当sum超过1时,移动左指针,直到sum<=1。
-
计算可能的起始点left,使得[left, right]内的sum <=1。然后,该窗口内的以right结尾的子数组数目是max(0, right - left)。注意,因为子数组必须长度>=2,所以起始点必须<=right-1。所以,起始点的范围是[left, right-1],数目是 right - left.
-
将所有这些数目累加起来,就是最终的结果。
例如,样例输入中的数组转换后的bad数组是:
原数组:1,3,6,4,10 → bad数组是1,0,0,1,1.
然后,滑动窗口的过程:
right=0(bad[0]=1):sum=1。left=0。此时,right - left =0-0=0。因为子数组长度必须>=2,所以不符合。
right=1(bad[1]=0):sum=1。left=0。right-left=1-0=1。所以符合一个。对应的子数组是[0,1],即原数组的前两个元素。符合条件。
right=2(bad=0):sum=1。left=0。对应的子数组是[0,2], [1,2]。原数组中的这三个元素分别是1,3,6。它们的bad之和是1(只有第一个元素是bad)。所以所有子数组的bad数目<=1。例如,[0,2]的数目是1,符合条件。子数组长度是3,符合条件。所以贡献是2。这时候总共有1+2=3。诸如此类。
那这样,在样例输入中,各步骤的应该是:
right=0:不符合,因为长度不够。
right=1:符合一个。即子数组[0,1]。
right=2:符合两个。即子数组[0,2]、[1,2]。其中,这三个元素的bad数目是1,所有以right=2结尾的子数组起始点可以是0或1。对应的子数组长度是3或2,都符合条件。
right=3:bad[3]=1,sum=2。此时需要移动左指针,直到sum<=1。例如,当left移动到1时,sum变为0+0+1=1。此时,sum=1。此时,right=3,left=1。符合条件的数量是两个。对应的子数组是[1,3]、[2,3]。例如,这两个子数组对应的原数组是3,6,4 → bad数目是1(4),所以符合条件。所以总贡献现在为1+2+2=5。
然后,right=4:bad=1。sum现在1+1=2。所以移动left到2,此时sum=0+1+1=2。继续移动left到3,sum=1+1=2。继续移动left到4,sum=1。此时,窗口是[4,4]。sum=1。但是子数组长度必须>=2。所以right=4时,left=4,不符合要求。所以总数还是5.
则所有的情况为[0,1],[0,2],[1,2],[1,3],[2,3]映射到[1,3,6,4,10]也就是[1,3],[1,6],[3,6],[3,4],[6,4]
看到这里是不是以为自己也是天才了

Java写法:
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取输入:数组长度n和目标近似GCD值g
int n = scanner.nextInt();
int g = scanner.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
// 预处理:生成bad数组,标记每个元素是否能被g整除
// bad[i] = 1 表示该元素需要修改,否则为0
int[] bad = new int[n];
for (int i = 0; i < n; i++) {
bad[i] = (a[i] % g != 0) ? 1 : 0;
}
int left = 0; // 滑动窗口左边界
int currentSum = 0; // 当前窗口内bad值的总和(即需要修改的元素数量)
long result = 0; // 统计符合条件的子数组总数
// 滑动窗口遍历数组
for (int right = 0; right < n; right++) {
currentSum += bad[right]; // 将右指针指向的元素加入窗口
// 当窗口内bad值超过1时,右移左指针直到满足条件
while (currentSum > 1) {
currentSum -= bad[left];
left++;
}
// 计算以当前right结尾的符合条件的子数组数量
// 窗口范围是[left, right],需要长度>=2,因此有效子数组数量为 right - left
// 例如:窗口长度为3(right - left +1 =3)时,有2个长度>=2的子数组以right结尾
result += Math.max(0, right - left);
}
// 输出结果
System.out.println(result);
scanner.close();
}
}
AC情况

不是哥们儿
沟槽的蓝桥杯还是没有发现吗,我这个方法连基本的题目都没满足。

我的答案连续吗,沟槽的测试用例居然还是过了简直逆天。


解法二:维护滑动窗口
-
滑动窗口维护 :当遇到无法被g整除的元素时,将左边界
left
移到上一个bad元素的下一个位置,确保窗口内最多一个bad元素。 -
结果累加 :窗口内所有以
right
结尾的子数组数量为right - left
(因为长度≥2的子数组有right - left
个)。
Java写法:
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
long g = scanner.nextLong();
long result = 0;
int lastBadPos = -1; // 上一个无法被g整除的元素位置
int left = 0; // 滑动窗口左边界
// 1 3 6 4 10
for (int right = 0; right < n; right++) {
long num = scanner.nextLong();
// 若当前元素无法被g整除,更新窗口左边界
if (num % g != 0) {
left = lastBadPos + 1; // 左边界移到上一个bad元素的下一位
lastBadPos = right; // 更新最后一个bad元素的位置
}
// 计算以right结尾的合法子数组数量
// 子数组长度至少为2,即区间长度 >=2 (right - left >=1)
if (right - left >= 1) {
result += right - left;
}
}
System.out.println(result);
scanner.close();
}
}
AC情况

但是,这个方法相比于第一个要更合理,但是它仍然给不出你完全契合测试用例的解释,只能勉强解释通,由于篇幅限制我就不再赘述,大家可以根据代码推一推。
总结
所以我说这个沟槽的蓝桥杯的这道题太离谱了铁铁,我根本不能接受。因为它真的找不到一个完美的解释,但是又可以逃脱测试用例的安排,通关。
