☰
ST表与滑动窗口解决最长友好子数组问题
2026/10/14 8:26:56 网站建设 项目流程

1. 问题背景与算法思路解析

这道题目来自Codeforces竞赛(编号1548B),要求我们找出数组中满足特定条件的最长子数组。题目中定义了一个"友好数组"的概念:如果存在一个整数m>1,使得数组a中从第l到第r个元素在模m下同余,那么这个子数组就是"好的"。

1.1 数学转换关键

理解这个问题的关键在于数学上的转换。当a[l] ≡ a[l+1] ≡ ... ≡ a[r] (mod m)时,意味着相邻元素的差值都能被m整除。因此我们可以:

  1. 先计算相邻元素的差值数组b,其中b[i] = |a[i+1] - a[i]|
  2. 原问题转化为在b数组中寻找最长的连续子数组,使得这些元素的gcd大于1

这个转换非常巧妙,它将原本看似复杂的同余问题简化为了更易处理的gcd问题。

1.2 算法选择依据

为了高效解决这个问题,我们需要:

  1. 快速计算任意区间内的gcd
  2. 高效地寻找满足条件的最长子数组

ST表(稀疏表)是解决这类区间查询问题的理想选择,因为它:

  • 预处理时间复杂度为O(nlogn)
  • 每次查询时间复杂度为O(1)
  • 特别适合静态数据的多次查询

相比之下,线段树虽然也能实现相同功能,但实现更复杂且常数因子更大,对于这种不需要动态更新的场景,ST表是更优的选择。

2. 代码实现详解

2.1 预处理阶段

const int N=2e5+5; int T,n,a[N],b[N],mx[N][35];

这里定义了:

  • N为数组最大可能大小(2e5+5)
  • mx数组是ST表的实现,第二维35足够覆盖2e5的范围(因为log2(2e5)≈18)
for(int i=1;i<n;i++){ b[i]=abs(a[i+1]-a[i]); mx[i][0]=b[i]; }

这部分计算差分数组b并初始化ST表的第一层(即每个长度为1的区间的gcd就是元素本身)。

2.2 ST表构建

for(int i=1;i<=20;i++){//ST表维护区间gcd for(int j=1;j+(1<<i)-1<=n;j++){ mx[j][i]=__gcd(mx[j][i-1],mx[j+(1<<(i-1))][i-1]); } }

ST表的构建采用动态规划思想:

  • 外层循环遍历不同的区间长度(2^i)
  • 内层循环遍历所有可能的起始位置
  • 每个区间[j,j+2^i-1]的gcd等于两个半区间的gcd的gcd

这里20次循环足够,因为2^20远大于题目中的数据范围。

2.3 查询函数实现

int f(int l,int r){ int len=r-l+1; int d=__lg(len); return __gcd(mx[l][d],mx[r-(1<<d)+1][d]); }

这个查询函数实现了O(1)时间复杂度的区间gcd查询:

  1. 计算区间长度len
  2. 找到最大的d使得2^d ≤ len
  3. 查询两个可能有重叠的区间[l,l+2^d-1]和[r-2^d+1,r]的gcd
  4. 返回这两个gcd的gcd

这种查询方式利用了ST表的特性,即使区间有重叠也不影响最终结果,因为gcd操作具有结合律。

3. 滑动窗口求解

3.1 双指针算法

int j=1,ans=1;//左端点 for(int i=1;i<n;i++){//右端点 while(j<=i&&f(j,i)==1) j++;//gcd不大于1(也就是等于1),左端点就还要加 ans=max(ans,i-j+2);//注意要加2,因为差分数组会忽略b[i](也就是最右边的那个数) }

这部分使用滑动窗口技术寻找最长满足条件的子数组:

  1. 维护一个窗口[j,i],保证窗口内所有元素的gcd>1
  2. 右指针i每次向右移动一位
  3. 左指针j根据当前gcd值调整位置
  4. 当gcd变为1时,向右移动j直到gcd再次>1或j超过i

3.2 边界条件处理

这里需要注意两点:

  1. 结果需要加2,因为差分数组比原数组少一个元素
  2. 初始ans设为1,因为至少一个元素总是满足条件(虽然题目中m>1,但单元素情况可以视为任何m都满足)

4. 算法复杂度分析

  1. 预处理阶段:

    • 差分数组计算:O(n)
    • ST表构建:O(nlogn)
  2. 查询阶段:

    • 每个右端点i最多导致左端点j移动n次(均摊分析)
    • 每次查询是O(1)
    • 总复杂度:O(n)

因此整体算法复杂度为O(nlogn),完全可以处理n=2e5的数据规模。

5. 实现细节与优化技巧

5.1 ST表大小选择

int mx[N][35];

这里第二维35是经过精心考虑的:

  • 2^30 ≈ 1e9,远大于题目中的n上限2e5
  • 实际只需要⌈log2(2e5)⌉+1=19就够了
  • 多分配一些空间可以避免边界问题

5.2 内置函数使用

代码中使用了两个GCC内置函数:

  1. __lg():快速计算一个数的二进制对数
  2. __gcd():计算两个数的gcd

这些内置函数通常经过高度优化,比自己实现的版本更快。

5.3 输入输出优化

scanf("%lld",&T);

在竞赛编程中,使用scanf/printf而不是cin/cout可以显著提高IO速度,特别是处理大量数据时。

6. 常见问题与调试技巧

6.1 为什么结果要加2?

这是因为:

  • 差分数组b的长度是n-1
  • 当我们在b中找到长度为L的满足条件的子数组时
  • 对应的原数组a中的子数组长度是L+1
  • 但题目中单元素情况也算,所以最终结果是max(ans,1)

6.2 如何处理全1数组?

如果所有b[i]都是1,那么:

  • 任何区间的gcd都是1
  • 滑动窗口会不断移动左指针j
  • 最终ans保持初始值1
  • 这是正确的结果,因为任何单元素都满足条件

6.3 为什么不用线段树?

虽然线段树也能实现区间gcd查询,但:

  1. 预处理时间是O(n)
  2. 每次查询时间是O(logn)
  3. 总复杂度会是O(nlogn),与ST表相同
  4. 但ST表的常数因子更小,实际运行更快
  5. ST表代码更简洁

7. 扩展思考与变种问题

7.1 动态版本问题

如果题目允许修改数组元素,那么:

  • ST表不再适用(它是静态数据结构)
  • 需要使用线段树来实现
  • 每次修改时间复杂度O(logn)
  • 查询复杂度O(logn)

7.2 多条件查询

如果题目增加其他查询条件,如同时要求区间和大于某个值:

  • 可以维护多个ST表或线段树
  • 查询时需要综合多个条件
  • 可能需要更复杂的滑动窗口逻辑

7.3 三维版本问题

如果将问题扩展到二维或三维数组:

  • 差分数组的定义会更复杂
  • ST表可能需要扩展到高维
  • 算法复杂度会显著增加

在实际编码比赛中,理解这种问题的数学本质并找到合适的转换方法是关键。这道题展示了如何将同余问题转化为gcd问题,再通过高效的数据结构加以解决。ST表在处理静态区间查询问题时是一个非常有力的工具,值得深入掌握。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询