除法分块求区间约数和

LL get_sum(LL n){
    LL ans = 0;
    for(LL l = 1,r;l <= n;l = r + 1){
        r = n / (n / l);
        ans += (n / l) * (l + r) * (r - l + 1) / 2;
        }
    return ans;
    }

 

<think>好的,我现在需要解决的问题是:使用C++编写一个算法,计算从1到N(N≤1e14)每个数的约数个数,并它们的累加。这看起来是一个数学算法结合的问题,我得仔细分析一下。 首先,我得理解问题。每个数的约数个数,然后。比如,对于N=3,1约数1个,2有2个,3有2个,总1+2+2=5。当N很大时,比如1e14,直接遍历每个数然后分解质因数约数个数显然不可行,因为时间复杂度太高。这时候需要更高效的数学方法。 传统的试除法对于每个数分解质因数,时间复杂度是O(N*sqrt(N)),这在N=1e14时完全不可行。所以必须找到数学规律或公式,将问题转换为更高效的计算方式。 我记得约数函数的性质。对于数n,其约数个数d(n) = (a₁+1)(a₂+1)...(a_k+1),其中n的质因数分解为p₁^a₁ * p₂^a₂ * ... * p_k^a_k。但直接应用这个公式需要分解每个数的质因数,这在N很大的时候不现实。 那累加的问题,总S(N) = Σ_{i=1}^N d(i) = Σ_{k=1}^N Σ_{d|k} 1 = Σ_{d=1}^N floor(N/d)。这是因为每个d在1到N中作为约数的次数等于有多少个数是d的倍数,也就是floor(N/d)。例如,d=1时,所有数都包含1,次数是N次;d=2时,次数是floor(N/2),依此类推。所以总等于Σ_{d=1}^N floor(N/d)。这样,问题转化为如何高效计算这个式。 现在,如何计算Σ_{d=1}^N floor(N/d)呢?直接计算的话,时间复杂度是O(N),对于N=1e14来说,这显然还是不可行的。必须寻找更优的方法,比如数学上的分块优化。 观察到当d在某个区间时,floor(N/d)的值是相同的。例如,当d在区间 [k, floor(N/(floor(N/k)))] 时,floor(N/d)的值等于k。可以利用这一点,将分成多个块,每个块内的floor(N/d)相同,从而减少计算次数。 具体来说,对于每个可能的k值,找到最大的d,使得floor(N/d) = k。这样的d的上界是floor(N/k)。然后,计算该区间内的d的数量,即从d_start到d_end的数量,乘以k,累加到总里。这样可以将时间复杂度从O(N)降低到O(sqrt(N)),因为当k超过sqrt(N)时,floor(N/k)的值会小于sqrt(N),此时可以分两种情况处理:一种是k <= sqrt(N),另一种是k > sqrt(N),此时对应的d的范围较小。 比如,当k <= sqrt(N)时,我们直接计算每个k对应的区间。当k > sqrt(N)时,对应的d的区间为d <= floor(N/k),而此时的floor(N/d)的值可能已经被处理过,或者需要另一种方式处理。或者,可能更简单的方式是将总分为两部分:当d <= sqrt(N)时,直接计算每个d对应的floor(N/d);当d > sqrt(N)时,对应的floor(N/d)的值k会小于sqrt(N),每个k对应的d的数目是floor(N/k) - floor(N/(k+1)),但需要注意避免重复计算。 或者,另一种思路是,计算总的时候,遍历k=1到sqrt(N),然后对于每个k,计算对应的floor(N/k),并处理可能的重复部分。 举个例子,假设N=10: d从110,对应的floor(10/d)分别是10,5,3,2,2,1,1,1,1,1。 总10+5+3+2+2+1+1+1+1+1=27。 按分块法,k的可能值包括1,2,3,5,10。例如: 当k=1时,对应的d的区间是[6,10],共5个d,贡献5*1=5。 当k=2时,对应的d是5,贡献1*2=2;d=4时k=2,贡献1*2=2;所以可能需要更仔细的分析。 可能更好的方法是,分块处理k的范围。例如,对于d从1到sqrt(N),直接计算floor(N/d)。对于较大的d,即当d > sqrt(N)时,floor(N/d)的值k < sqrt(N)。此时,每个k出现的次数是floor(N/k) - floor(N/(k+1))。但这里需要注意,当k较小的时候,比如k=1时,对应的d的范围是较大的,可能已经被之前的处理覆盖。 或者,正确的分块方法应该是这样的: 总S = Σ_{d=1}^N floor(N/d) = Σ_{k=1}^m k*(g(k) - g(k+1))),其中m是最大的k使得k <= N,而g(k)是满足floor(N/d) >=k的d的个数。或者这可能比较复杂。 另一种更直观的方法是,找到所有可能的k值,即floor(N/d)的可能取值,然后计算每个k出现的次数。例如,当d从1到N时,k的可能取值为1到N,但很多k值对应的区间是连续的。例如,当d在区间[N/(k+1)+1, N/k]时,floor(N/d) =k。因此,对于每个k,对应的d的数量是floor(N/k) - floor(N/(k+1))。这个数量乘以k就是该区间的贡献。 但这样的话,计算所有k的贡献总,时间复杂度是O(N),仍然不可行。所以需要找到k的范围,使得可以分块处理。例如,对于k <= sqrt(N),每个k对应的区间长度可能较大;而k>sqrt(N)时,对应的d的区间可能较小。或者,对于k<=Kd<=D的情况,其中K=sqrt(N),D=K。这时,可以将总分成两部分: S = Σ_{d=1}^D floor(N/d) + Σ_{k=1}^floor(N/(D+1))} k*(floor(N/k) - floor(N/(k+1))) ) 其中D=sqrt(N)。当d <=D时,直接计算每个d对应的floor(N/d);当d > D时,对应的k=floor(N/d) <= floor(N/(D+1)) <= D。因此,这部分的k值范围是1到D,可以遍历k=1到D,计算对应的数目,即floor(N/k) - D,但需要注意,这可能涉及到重复计算。 或者,正确的分块方式应该是这样的: 计算总S=Σ_{d=1}^N floor(N/d) = Σ_{d=1}^N τ(d),其中τ(d)是d作为约数的次数。但可能我混淆了概念。 回到正确的分块方法。例如,对于k=1到K(K=sqrt(N)),每个k对应的d的数目是floor(N/k) - floor(N/(k+1))。然后,剩下的部分是当k < K时对应的d的范围。或者,可能更简单的方式是将总分成两个部分: 1. 当d <= K时,直接计算每个d对应的floor(N/d),其中K=sqrt(N)。 2. 当d > K时,此时对应的k=floor(N/d) < K。因此,对于每个可能的k值(从1到K-1),计算有多少个d满足floor(N/d)=k。这相当于d在区间(N/(k+1), N/k]中。然后,每个这样的k对应的数目是floor(N/k) - floor(N/(k+1)),再乘以k。 因此,总S等于: Σ_{d=1}^K floor(N/d) + Σ_{k=1}^{K-1} k * (floor(N/k) - floor(N/(k+1))) ) 其中K=sqrt(N)。这样,两部分的总即可得到结果。但要注意,当K=sqrt(N),那么当k=1到K-1时,floor(N/k)的值可能大于K,但对应的d的范围是d > K的情况。这样,这种分块方法是否可行? 举个例子,假设N=10,K=3(因为 sqrt(10)=3.16,取3)。 第一部分:d=1,2,3,对应的floor(10/1)=10,floor(10/2)=5,floor(10/3)=3。总10+5+3=18。 第二部分:k=1到2(因为K=3,所以k从1到K-1=2)。 当k=1时,对应的d必须满足 floor(10/d)=1,即d>10/1=10,但是d最大只能是10,所以没有满足的d?或者可能我的分块方式有问题。 这说明我的分块思路可能有错误。需要重新分析。 正确的分块思路应该基于以下观察:对于d从1到N,floor(N/d)的可能取值是连续的。当d>sqrt(N),那么floor(N/d)的值一定小于等于sqrt(N)。例如,当N=10,sqrt(N)=3.16。当d=4,floor(10/4)=2;d=5,floor=2;d=5到d=10,floor的值从2到1。因此,对于d>3的情况,floor(N/d)的值是1或2。这时候,我们可以将总分为两部分: 1. 当d <= K(K=sqrt(N))时,直接计算每个d对应的floor(N/d)。 2. 当d > K时,floor(N/d)的值是k,其中k的范围是1到K',K' = floor(N/(K+1))。对于每个k,计算有多少个d满足floor(N/d)=k,并且d>K。这些d的数量等于floor(N/k) - K,但必须确保floor(N/k) >= K+1,否则数量为0。或者说,当k <= K'时,存在d>K使得floor(N/d)=k,数量为floor(N/k) - K。 比如,当k=2,N=10,K=3。floor(N/k)=5。此时d需要满足d >3且d <=5,所以数量是5-3=2(d=4,5)。 因此,第二部分的为Σ_{k=1}^{K'} k * (floor(N/k) - K)。其中K' = floor(N/K+1)。例如,N=10,K=3,K+1=4,floor(10/4)=2,所以K'=2。k从1到2: 当k=1时,floor(N/1)=10,数量是10-3=7(d=4到10中满足floor(10/d)=1的d有7个?但实际上当d>10时才会floor(10/d)=0,但d最大是10。所以当k=1时,d>3且floor(10/d)=1的条件是d>10,但d<=10,所以此时没有符合条件的d。因此,我的分块思路可能存在问题。 这说明我需要重新考虑分块的条件。正确的分块方式应该是这样的: 总S=Σ_{d=1}^N floor(N/d) = Σ_{k=1}^m k * (g(k) - g(k+1))),其中g(k)是最大的d使得floor(N/d) >=k。这可能不太容易处理。 另一种正确的方法是,遍历所有可能的k=1到sqrt(N),对于每个k,计算对应的d的数目。当k <= sqrt(N)时,floor(N/k) >=k,此时数目为floor(N/k) - floor(N/(k+1))。这部分可以处理较大的k。而对于k较大的情况,即当k > sqrt(N)时,其对应的d的数目较小,可以通过另一种方式计算。 或者,正确的分块方法是将总分为两个部分: S = Σ_{d=1}^K floor(N/d) + Σ_{k=1}^{M} k * (floor(N/k) - K ) 其中K=floor(sqrt(N)), M=floor(N/(K+1))。这样,当d>K时,对应的k=floor(N/d) <= floor(N/(K+1))=M。这部分的可以通过计算每个k=1到M,对应的d的数量是floor(N/k) - K,前提是floor(N/k) > K。例如,当k=1时,floor(N/1)=N,数量是N - K。但只有当k <= M时,floor(N/k) >= K+1吗? 例如,假设N=10,K=3,M=floor(10/(3+1))=2。对于k=1到2: 当k=1时,d>3且d <=10/1=10。数量为10-3=7。每个d对应的floor(10/d)=1,所以贡献是7*1=7。 当k=2时,d>3且d <=10/2=5。数量是5-3=2。贡献是2*2=4。 总的第一部分是d=1,2,3的floor(10/d)=10+5+3=18。第二部分是7+4=11。总18+11=29?但实际总应该是27。这说明哪里出错了? 实际计算时,当k=1时,对应的d必须满足floor(10/d)=1。即10/d >=1且 <2 →d>5。因为d是整数,所以d>=6。此时d>3的情况下,d=6,7,8,9,10,共5个d,贡献5*1=5。当k=2时,floor(10/d)=2的条件是10/d >=2且 <3 →d>=4且 <=5。此时d=4,5,共2个d,贡献2*2=4。所以第二部分的总应该是5+4=9,加上第一部分的18,总18+9=27,正确。那之前的公式哪里错了? 问题出在计算第二部分时的条件。当k=1时,对应的d的范围是d > K=3,并且d <= floor(N/k)=10。此时,floor(N/d)=k=1的条件是d > N/(k+1) =10/(1+1)=5。所以d必须满足d >5,d <=10。所以d的数量是10 -5=5。所以第二部分的总为Σ_{k=1}^M k*(floor(N/k) - floor(N/(k+1)))。此时,M= floor(N/(K+1))=floor(10/4)=2。对于k=1,数目是floor(10/1)-floor(10/2)=10-5=5。k=1的贡献是5*1=5。对于k=2,数目是floor(10/2)-floor(10/3)=5-3=2,贡献是2*2=4。总5+4=9。加上第一部分的18,得到27,正确。 所以正确的分块方式是: 总S = Σ_{d=1}^K floor(N/d) + Σ_{k=1}^M (k * (floor(N/k) - floor(N/(k+1))) ) 其中K=floor(sqrt(N)), M=floor(N/(K+1))。 这样,当计算k=1到M时,每个k对应的数目是floor(N/k) - floor(N/(k+1)),这部分对应的d的范围是d > K且满足floor(N/d)=k。因为当d > K时,floor(N/d) < K+1,而k的范围是1到M= floor(N/(K+1))。所以,这样的分块是正确的。 因此,算法的大致步骤如下: 1. 计算K = floor(sqrt(N))。 2. 计算第一部分的:sum1 = Σ_{d=1}^K floor(N/d)。 3. 计算M = floor(N/(K+1))。 4. 计算第二部分的:sum2 = Σ_{k=1}^M k * (floor(N/k) - floor(N/(k+1)))。 5. 总S = sum1 + sum2. 这样,时间复杂度为O(sqrt(N)),因为第一部分第二部分各需要O(sqrt(N))次循环。例如,当N=1e14时,sqrt(N)=1e7,循环次数为1e7,这在C++中是可接受的,但可能需要优化,否则可能会超时。例如,当N=1e14,K=1e7,循环1e7次可能需要较多时间,但可能可以通过优化代码来减少时间。 不过,当N=1e14时,sqrt(N)=1e7,循环次数是1e7次,这在现代计算机上可能需要约几秒的时间。例如,假设每次循环需要1纳秒,那么1e7次循环需要约0.01秒。但实际情况可能更复杂,因为每个循环中的计算可能包括除法、取整等操作。例如,在C++中,floor(N/d)可以直接用整数除法,因为当d是整数时,N/d的结果会自动向下取整。 那如何实现这个算法? 首先,计算sum1: sum1 = 0; for (int d=1; d<=K; ++d) { sum1 += N / d; } 然后,计算sum2: sum2 = 0; M = N/(K+1); for (int k=1; k<=M; ++k) { sum2 += k * ( (N/k) - (N/(k+1)) ); } 总S = sum1 + sum2. 比如,当N=10时,K=3,sum1=10+5+3=18,M=10/(3+1)=2.5→2。sum2=1*(10/1 -10/2) +2*(10/2 -10/3) →1*(10-5)+2*(5-3)=5+4=9。总18+9=27,正确。 现在,这个算法的时间复杂度是O(sqrt(N)),对于N=1e14来说,K=1e7,循环1e7次,这在C++中是可行的,但需要注意数据类型的溢出问题。因为当N=1e14时,sum1sum2都可能非常大,需要使用足够大的整数类型,比如unsigned long long。 例如,当N=1e14时,sum1的最大可能值是Σ_{d=1}^1e14 (1e14/d)。这个总的上界约为O(N log N),但具体值可能很大。例如,当d=1时,项是1e14;d=2时,项是5e13;依此类推。总会非常大,可能超过1e18。例如,当sum1=1e14 +5e13 + ...,这可能会超过1e18,需要用64位无符号整数来存储,如C++中的unsigned long long类型,其最大值为18446744073709551615,这足够存储当N=1e14时的总。 那现在的问题转化为如何在C++中高效地实现这个算法,并且处理大数问题。 代码的大致结构如下: #include <iostream> #include <cmath> // for sqrt函数 using namespace std; typedef unsigned long long ULL; int main() { ULL N; cin >> N; ULL K = (ULL)sqrt(N); // 计算K,这里可能存在精度问题,比如当N是一个完全平方数的时候,sqrt(N)可能返回浮点数的精确值,否则向下取整。例如,当N不是平方数时,K = floor(sqrt(N))。需要确保正确性。可能需要调整K的值,比如循环检查K*K是否超过N,然后减1。 // 修正K的值,确保 K <= sqrt(N) < K+1 while (K * K > N) K--; while ((K+1)*(K+1) <= N) K++; ULL sum1 = 0; for (ULL d = 1; d <= K; ++d) { sum1 += N / d; } ULL M = N / (K + 1); ULL sum2 = 0; for (ULL k = 1; k <= M; ++k) { ULL a = N / k; ULL b = N / (k + 1); sum2 += k * (a - b); } ULL total = sum1 + sum2; cout << total << endl; return 0; } 但是需要注意,计算K的时候可能存在误差,因为浮点数运算可能有精度问题。例如,当N接近1e14时,sqrt(N)可能无法精确转换为整数。例如,对于非常大的N,浮点数的精度可能不足以正确表示,因此更好的方法是使用二分查找来确定K的值,或者调整循环来确保正确性。 例如,修正K的初始计算: K = sqrt(N); // 转换为浮点数,然后转换为ULL,可能存在误差。 比如,当N=1e14时,sqrt(1e14)=1e7,正确。但当N=1e14-1时,sqrt(N)=9999999.9999999...,转换为ULL会得到9999999。这时,可能需要调整以确保K是最大的整数满足K^2 <= N。 正确的方法是: K = (ULL)sqrt(N); if ((K+1)*(K+1) <= N) K++; // 这种情况可能不会发生,因为sqrt返回的是向下取整的? 或者,在C++中,sqrt返回的是double类型,对于大整数来说,可能不够精确。例如,当N=1e18,sqrt(N)=1e9,正确。但对于更大的数,可能需要更精确的方法。 因此,更可靠的方法是使用二分法来寻找最大的K,使得 K^2 <=N。 例如: ULL find_K(ULL N) { ULL left = 0, right = 1e9; // 假设N的上限是1e28,那么right需要足够大,比如1e14。 while (left < right) { ULL mid = (left + right + 1) / 2; if (mid <= N/mid) { // mid^2 <= N → mid <= sqrt(N) left = mid; } else { right = mid -1; } } return left; } 这样可以避免浮点数的精度问题,得到正确的K值。 因此,修改代码中的K的计算部分: ULL K = find_K(N); 这将确保K是最大的整数满足 K^2 <= N。 综上,整个算法的正确性得到保证,时间复杂度为O(sqrt(N)),适用于N<=1e14的情况。 现在,测试一下这个算法是否正确。 测试案例1: N=3,预期总为d(1)+d(2)+d(3)=1+2+2=5。按照分块法: K=sqrt(3)=1.732,取K=1. sum1 = floor(3/1)=3. M=3/(1+1)=1.51. sum2=1*(3/1 -3/2)=1*(3-1)=2. 总3+2=5,正确。 测试案例2: N=10,预期总27,如之前分析正确。 测试案例3: N=1,总1。计算: K=1,sum1=1. M=1/(1+1)=0 → sum2=0. 总1,正确。 另一个测试案例: N=4,每个数的约数数目为1,2,2,3。总8。 分块计算: K=2. sum1=4/1 +4/2=4+2=6. M=4/(2+1)=1.333→1. sum2=1*(4/1 -4/2)=1*(4-2)=2. 总6+2=8,正确。 这说明算法是正确的。 现在,这个算法的C++实现需要考虑大整数的问题,使用unsigned long long,并且正确计算K的值。 现在,编写代码: #include <iostream> using namespace std; typedef unsigned long long ULL; ULL find_K(ULL N) { ULL left = 0, right = N; while (left < right) { ULL mid = (left + right + 1) / 2; if (mid <= N / mid) { // 避免溢出,使用除法 left = mid; } else { right = mid - 1; } } return left; } int main() { ULL N; cin >> N; ULL K = find_K(N); ULL sum1 = 0; for (ULL d = 1; d <= K; ++d) { sum1 += N / d; } ULL M = N / (K + 1); ULL sum2 = 0; for (ULL k = 1; k <= M; ++k) { ULL a = N / k; ULL b = N / (k + 1); sum2 += k * (a - b); } ULL total = sum1 + sum2; cout << total << endl; return 0; } 这样,当输入N=1e14时,程序应该能够正确计算总。需要注意的是,在计算sum1时,循环次数是K次,即大约1e7次,这在C++中应该是可以接受的,因为1e7次循环在普通计算机上大约需要几毫秒到几秒的时间,取决于优化等级。例如,开启O2优化后,这样的循环会很快。 此外,要注意数据类型的溢出。例如,sum1的最大可能值当N=1e14时,每个项N/d的总。例如,当d=1时,项是1e14;d=2时是5e13;...,总大概是O(N log N)。对于N=1e14,总可能达到约1e14 * log(1e14)。log(1e14)≈32.2,所以总约为3e15,这远小于ULL的最大值(约1e19),所以不会溢出。 因此,这个算法是可行的。</think>要高效计算1到N每个数的约数个数累加,可以利用数论分块法将时间复杂度优化到$O(\sqrt{N})$。以下是分步实现方法: ### 算法思路 总$S(N)=\sum_{i=1}^N d(i)$,其中$d(i)$表示$i$的约数个数。根据约数性质,总可转换为: $$ S(N) = \sum_{d=1}^N \left\lfloor \frac{N}{d} \right\rfloor $$ 即将问题转化为计算每个$d$在1到N中作为约数的次数之。 ### 分块优化 将计算分为两部分: 1. **直接计算**:当$d \leq \sqrt{N}$时,直接累加$\left\lfloor \frac{N}{d} \right\rfloor$。 2. **分块处理**:当$d > \sqrt{N}$时,$\left\lfloor \frac{N}{d} \right\rfloor$的值较小且成块分布,按块。 ### C++代码实现 ```cpp #include <iostream> using namespace std; typedef unsigned long long ULL; ULL find_K(ULL N) { ULL left = 0, right = N; while (left < right) { ULL mid = (left + right + 1) / 2; if (mid <= N / mid) { left = mid; } else { right = mid - 1; } } return left; } int main() { ULL N; cin >> N; ULL K = find_K(N); ULL sum1 = 0; for (ULL d = 1; d <= K; ++d) { sum1 += N / d; } ULL M = N / (K + 1); ULL sum2 = 0; for (ULL k = 1; k <= M; ++k) { ULL a = N / k; ULL b = N / (k + 1); sum2 += k * (a - b); } ULL total = sum1 + sum2; cout << total << endl; return 0; } ``` ### 代码解析 1. **确定分界点**:`find_K`函数通过二分法找到最大的$K$满足$K^2 \leq N$,即$K = \lfloor \sqrt{N} \rfloor$。 2. **计算前段**:累加$d \leq K$时的$\left\lfloor \frac{N}{d} \right\rfloor$值。 3. **分块处理后段**:对于$k$从1到$M = \lfloor \frac{N}{K+1} \rfloor$,计算每个$k$对应的区间贡献,利用$\sum k \cdot (\left\lfloor \frac{N}{k} \right\rfloor - \left\lfloor \frac{N}{k+1} \right\rfloor)$。 ### 示例分析 以$N=10$为例: - $K=3$,前段为$10 + 5 + 3 = 18$。 - $M=2$,后段为$1 \cdot (10-5) + 2 \cdot (5-3) = 5 + 4 = 9$。 - 总为$18 + 9 = 27$,与直接计算结果一致[^1]。 ### 性能分析 - **时间复杂度**:$O(\sqrt{N})$,适用于$N \leq 10^{14}$的规模。 - **空间复杂度**:$O(1)$,仅需常数空间。 ### 应用场景 该方法适用于需要高效计算大规模约数个数的场景,如数论研究、算法竞赛等。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值