问题分析
题目要求在区间 [1, n] 内找到一些连续的自然数,其乘积包含至多 k 个不同的素因子。我们的目标是找到这样的连续子数组(即连续的自然数),使得其乘积的素因子数量不超过 k,并且这个子数组的长度最大。
关键点:
- 每个自然数的素因子是整数的因子,可以是素数或它们的组合。
- 子数组是连续的,并且我们要保证其乘积的素因子数量不超过
k。
思路分析
1. 素因子的定义与问题的关系
我们需要对于每一个自然数,计算它的素因子,并且使用滑动窗口(双指针法)来遍历连续的自然数区间,维护每个区间的素因子集合,直到素因子数量超过 k。
2. 滑动窗口方法
- 使用两个指针,
left和right,表示当前滑动窗口的左右边界。 - 每次滑动窗口右边界向右扩展,将新数字的素因子加入窗口中。
- 如果当前窗口内的素因子数量超过
k,则通过移动左边界来缩小窗口,直到窗口内的素因子数量不超过k。 - 在每一步,记录当前窗口的长度,更新最大长度。
3. 素因子计算
对于每个数,计算其素因子,并维护一个集合记录当前窗口中的素因子。可以通过简单的素数筛法,预先计算所有数的素因子,也可以通过逐个判断每个数字的因子。
算法设计
1. 计算素因子
使用 埃拉托斯特尼筛法 (Sieve of Eratosthenes)来计算每个数字的素因子。我们可以在筛法中为每个数字记录它的素因子集合。
2. 滑动窗口
使用双指针法来维护一个窗口,确保窗口内所有数字的乘积的素因子数量不超过 k。每次右指针扩展时,将当前数字的素因子加入窗口;如果窗口内的素因子数量超过 k,则左指针向右移动,直到满足条件。
复杂度分析
-
预处理素因子的时间复杂度:
- 我们使用埃拉托斯特尼筛法来计算每个数字的素因子,这个过程的时间复杂度是 O(n log log n) 。这是因为我们对于每个素数
p会遍历它的倍数,累积的复杂度接近于O(n log log n)。
- 我们使用埃拉托斯特尼筛法来计算每个数字的素因子,这个过程的时间复杂度是 O(n log log n) 。这是因为我们对于每个素数
-
滑动窗口的时间复杂度:
- 我们通过两个指针
left和right来维护窗口。每次右指针向右移动时,左指针可能会向右移动,整个过程的时间复杂度为 O(n) 。 - 对于每个数,计算它的素因子并更新窗口中的素因子信息,这个操作的复杂度是与素因子数量相关的,最坏情况下每个数字的素因子数量是对数级别的,复杂度大约是 O(log n) 。
- 我们通过两个指针
因此,总的时间复杂度是 O(n log log n + n log n) ,其中 O(n log log n) 是计算素因子的时间,O(n log n) 是处理滑动窗口的时间。
C++代码实现
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
// 计算1到n中每个数的素因子
vector<unordered_set<int>> getPrimeFactors(int n) {
vector<unordered_set<int>> primeFactors(n + 1);
// 使用类似筛法的方法来记录素因子
for (int i = 2; i <= n; ++i) {
if (primeFactors[i].empty()) { // i是素数
for (int j = i; j <= n; j += i) {
primeFactors[j].insert(i); // 添加素因子i
}
}
}
return primeFactors;
}
int solution(int n, int k) {
// 获取1到n的素因子信息
auto primeFactors = getPrimeFactors(n);
int maxLength = 0;
int left = 1;
unordered_set<int> uniquePrimeFactors; // 当前窗口内的素因子集合
// 使用滑动窗口从1到n遍历
for (int right = 1; right <= n; ++right) {
// 将右边界数字的素因子加入窗口
for (int factor : primeFactors[right]) {
uniquePrimeFactors.insert(factor);
}
// 当窗口中的素因子数量超过k时,收缩左边界
while (uniquePrimeFactors.size() > k) {
// 左边界的素因子移除
for (int factor : primeFactors[left]) {
uniquePrimeFactors.erase(factor);
}
++left; // 收缩窗口
}
// 更新最大长度
maxLength = max(maxLength, right - left + 1);
}
return maxLength;
}
Java实现
-
数据结构:
- 使用
List<Set<Integer>>存储每个数字的素因子,每个集合 (Set<Integer>) 用来存储一个数字的素因子。 HashSet用于存储窗口中的素因子集合。
- 使用
-
性能:
HashSet提供了较高的查找和删除效率,因此在处理素因子集合时比 C 语言的数组更加高效。- Java 提供的
removeAll方法使得在收缩窗口时移除素因子更加简洁和高效。
import java.util.*;
public class Main {
// 计算1到n中每个数的素因子
public static List<Set<Integer>> getPrimeFactors(int n) {
List<Set<Integer>> primeFactors = new ArrayList<>();
for (int i = 0; i <= n; i++) {
primeFactors.add(new HashSet<>());
}
// 使用筛法记录素因子
for (int i = 2; i <= n; ++i) {
if (primeFactors.get(i).isEmpty()) { // i是素数
for (int j = i; j <= n; j += i) {
primeFactors.get(j).add(i); // 添加素因子i
}
}
}
return primeFactors;
}
public static int solution(int n, int k) {
// 获取1到n的素因子信息
List<Set<Integer>> primeFactors = getPrimeFactors(n);
int maxLength = 0;
int left = 1;
Set<Integer> uniquePrimeFactors = new HashSet<>(); // 当前窗口内的素因子集合
// 使用滑动窗口从1到n遍历
for (int right = 1; right <= n; ++right) {
// 将右边界数字的素因子加入窗口
uniquePrimeFactors.addAll(primeFactors.get(right));
// 当窗口中的素因子数量超过k时,收缩左边界
while (uniquePrimeFactors.size() > k) {
// 左边界的素因子移除
uniquePrimeFactors.removeAll(primeFactors.get(left));
++left; // 收缩窗口
}
// 更新最大长度
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
public static void main(String[] args) {
System.out.println(solution(10, 3) == 6); // True
System.out.println(solution(20, 5) == 12); // True
System.out.println(solution(100, 4) == 10); // True
}
}
C语言实现
-
数据结构:
- 使用了
primeFactors二维数组来存储每个数字的素因子。 - 使用
factorCount数组记录每个数字的素因子的数量。 - 用
uniquePrimeFactors数组来保存窗口内的所有不同的素因子。
- 使用了
-
性能:
- 与 C++ 的
unordered_set相比,手动管理素因子的集合稍显繁琐。尤其是在处理素因子的插入、删除时,需要遍历数组来查找重复项。 - 滑动窗口的思想是相同的,但是 C 语言需要手动处理内存,尤其是在删除窗口左边界的素因子时。
- 与 C++ 的
#include <stdio.h>
#include <stdlib.h>
#define MAX_N 100
// 计算1到n中每个数的素因子
void getPrimeFactors(int n, int primeFactors[MAX_N + 1][MAX_N], int factorCount[MAX_N + 1]) {
for (int i = 2; i <= n; ++i) {
if (factorCount[i] == 0) { // i是素数
for (int j = i; j <= n; j += i) {
primeFactors[j][factorCount[j]++] = i; // 添加素因子i
}
}
}
}
// 判断集合中是否包含某个素因子
int contains(int factors[MAX_N], int size, int value) {
for (int i = 0; i < size; ++i) {
if (factors[i] == value) return 1;
}
return 0;
}
int solution(int n, int k) {
int primeFactors[MAX_N + 1][MAX_N]; // 存储每个数字的素因子
int factorCount[MAX_N + 1] = {0}; // 存储每个数字的素因子的数量
getPrimeFactors(n, primeFactors, factorCount);
int maxLength = 0;
int left = 1;
int uniquePrimeFactors[MAX_N]; // 当前窗口内的素因子集合
int uniqueCount = 0;
// 使用滑动窗口从1到n遍历
for (int right = 1; right <= n; ++right) {
// 将右边界数字的素因子加入窗口
for (int i = 0; i < factorCount[right]; ++i) {
int factor = primeFactors[right][i];
if (!contains(uniquePrimeFactors, uniqueCount, factor)) {
uniquePrimeFactors[uniqueCount++] = factor; // 添加新素因子
}
}
// 当窗口中的素因子数量超过k时,收缩左边界
while (uniqueCount > k) {
// 左边界的素因子移除
for (int i = 0; i < factorCount[left]; ++i) {
int factor = primeFactors[left][i];
for (int j = 0; j < uniqueCount; ++j) {
if (uniquePrimeFactors[j] == factor) {
for (int l = j; l < uniqueCount - 1; ++l) {
uniquePrimeFactors[l] = uniquePrimeFactors[l + 1];
}
uniqueCount--;
break;
}
}
}
++left; // 收缩窗口
}
// 更新最大长度
maxLength = maxLength > (right - left + 1) ? maxLength : (right - left + 1);
}
return maxLength;
}
优化
1. 优化素因子的存储和计算
在前面的方案中,我们为每个数字存储了一个素因子集合。每次更新滑动窗口时,我们遍历每个数字的素因子并修改窗口状态。考虑到素因子可能相对较少,我们可以对存储结构和操作进行优化。
改进建议:使用一个全局素因子计数器
我们可以用一个全局计数器来记录所有数的素因子,并且在更新窗口时直接修改这个计数器,而不是存储每个数的所有素因子集合。
- 使用一个字典来记录当前窗口中各个素因子的出现次数。
- 维护一个变量来跟踪窗口中独特素因子的数量,避免在每次更新时重新计算。
- 通过一个优化的
map结构来记录每个数字的素因子,而不是一个集合,这样就能快速更新窗口内每个素因子的频次。
2. 优化素因子计算
对于 n 较大的情况,我们仍然可以使用筛法来获取素因子,但我们可以避免重复的计算。比如在 滑动窗口扩展时,我们仅需关注新进入窗口的数字的素因子,不需要重新计算整个窗口的素因子。
改进建议:只计算新进入数字的素因子
对于右指针右移时,只需计算新进入的数字的素因子,不用重新计算整个区间的素因子集合。通过这样做,我们可以减少不必要的重复工作。
3. 优化窗口收缩逻辑
在滑动窗口的收缩过程中,我们可以进一步优化。我们可以避免每次左指针移时都扫描整个窗口的素因子,而是通过一些技术手段减少每次更新的时间。
改进建议:使用高效的数据结构来维护素因子状态
- 可以使用哈希表来追踪每个素因子的频次,这样在窗口收缩时,只需删除或减少对应素因子的频次。
- 如果我们使用哈希表的方式,我们就可以避免在每次收缩时重新计算窗口内的所有素因子。
小结
- 滑动窗口优化:我们通过减少每次更新窗口时重复计算素因子的次数,提高了算法效率。
- 素因子计算优化:通过只计算新进入数字的素因子而不是每次重新计算整个窗口的素因子,减少了计算量。
- 空间优化:可以通过优化存储结构(如位标志)来减少内存开销
总结
这个实现通过结合 滑动窗口 和 素因子筛法,有效地解决了问题。预处理阶段通过筛法计算出每个数的素因子集合,滑动窗口则用来高效地计算符合条件的最大子数组长度。优化后的算法能够在合理的时间内处理大规模输入,适应了复杂度要求较高的问题。
关键思路:
-
素因子的预处理:
- 使用 埃拉托斯特尼筛法 来计算从
1到n每个数字的素因子。 - 对每个数字,记录它所有的素因子集合。这一步预处理的时间复杂度为
O(n log log n)。
- 使用 埃拉托斯特尼筛法 来计算从
-
滑动窗口:
- 使用滑动窗口方法来遍历区间
[1, n],维护一个窗口[left, right],其中right指针扩展窗口,left指针收缩窗口。 - 在窗口扩展过程中,加入新数字的素因子,更新窗口内的素因子种类数。
- 当窗口内的素因子种类数超过
k时,左指针收缩窗口,直到素因子种类数小于或等于k。
- 使用滑动窗口方法来遍历区间
-
优化素因子的存储与更新:
- 使用 哈希表
unique_factors来记录当前窗口内每个素因子的出现次数。 - 维护一个变量
total_factors来记录窗口内的素因子种类数。 - 每次更新窗口时,只有新进入的数字的素因子需要被计算,而不需要重新计算整个窗口的素因子。
- 当素因子的出现次数为零时,减少
total_factors,表示该素因子已经不在当前窗口中。
- 使用 哈希表
主要操作和优化:
-
素因子的预计算:
- 使用 埃拉托斯特尼筛法 预先计算所有数的素因子集合,减少了每次查询的复杂度。
- 这样可以在滑动窗口的过程中直接获取每个数字的素因子,而无需每次重新计算。
-
滑动窗口扩展与收缩:
- 当窗口的素因子种类数超过
k时,收缩左边界,并减少对应素因子的频次。 - 收缩时仅处理窗口内已经存在的素因子,而不是每次都重新计算整个窗口的素因子集合。
- 当窗口的素因子种类数超过
-
动态更新窗口:
- 通过哈希表
unique_factors来维护窗口内素因子的频次,确保在每次更新窗口时可以高效判断是否超出了k个素因子。
- 通过哈希表