1 条题解

  • 0
    @ 2025-8-24 21:33:05

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar DeepSkyCore
    bot @zwh2008 orz

    搬运于2025-08-24 21:33:05,当前版本为作者最后更新于2025-03-22 12:31:42,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    此题解非正解,复杂度 O(nlogn)O(n\log n),但是进行了有效的常数优化。

    显然有一个非常简单的做法:先筛出来 φ(x)\varphi(x),然后从小到大枚举 ii,枚举 ij=xij=x,把 fif_iφ(j)\varphi(j) 转移到 fxf_x 即可。由于因数数量是 O(nlogn)O(n\log n),所以这个复杂度也是 O(nlogn)O(n\log n)

    不过如果真的就这么写,自然是过不去的。这里提供一个常数非常小的写法,似乎在这题数据范围下比正解还快很多。

    先想想这个东西为什么会慢。算一算实际的转移次数,甚至都不到 10910^9,而时限 44 秒,所以问题出在内存访问不够快。

    首先考虑分块转移,也就是从小到大枚举 x[kB,(k+1)B)x\in [kB, (k+1)B),然后对这个区间再枚举 ij=xij=x 转移。这样我们扫描一个 200MB 的数组的次数减小了,就会快很多,大概只需要跑 3s。下面是一个参考实现,其中,计算除法的部分还用了一次整除分块:

    constexpr int B = 2e6;
    int n; cin>>n;
    vector<u32> f(n+1), lst(n+1, 2);
    f[1] = 1;
    for(int l = 1, r = min(B, n); l <= n; l = r+1, r = min(l + B - 1, n)){
    	for(u32 l0 = 1, r0, k; ; l0 = r0+1){
    		k = r / l0, r0 = r / k;
    		if(k == 1) break;
    		rep(i,l0,r0){
    			while(lst[i] <= k){
    				f[i*lst[i]] += f[i] * phi[lst[i]];
    				lst[i]++;
    			}
    		}
    	}
    }
    u32 ans = 0;
    rep(i,1,n){
    	ans ^= f[i];
    }
    cout<< ans <<'\n';
    

    不过就算这么搞,我们还是要多次扫描一遍 200MB 的数组,同时还需要在大概 8MB 的数组做随机访问,还能不能优化呢?

    考虑一个非常简单的结论:设 ij=xij=x,则 min(i,j)x\min(i,j)\le \sqrt x。因此我们在分块后,区间转移的过程只需要枚举较小的那个因数就行了。

    这么搞的话不需要多次扫 200MB 的数组,同时随机访问的内存也更密集。这样就跑得很快了,大概只需要 1.3s。同样给出参考实现:

    constexpr int B = 65536
    int n; cin>>n;
    vector<u32> f(n+1);
    f[1] = 1;
    
    int l = 1, r = min(n, B);
    rep(i,1,r/2){
    	for(int j=2; j <= r/i; j++){
    		f[j*i] += f[i] * phi[j];
    	}
    }
    l = r+1, r = min(l + B - 1, n);
    for(; l <= n; l = r+1, r = min(l + B - 1, n)){
    	rep(j,l,r){
    		f[j] += phi[j];
    	}
    	rep(i,2,B){
    		rep(j, max(i, (l-1)/i+1), r/i){
    			f[i*j] += f[i]*phi[j];
    			if(i != j) f[i*j] += phi[i]*f[j];
    		}
    	}
    }
    
    u32 ans = 0;
    rep(i,1,n){
    	ans ^= f[i];
    }
    cout<< ans <<'\n';
    

    在这题中,由于函数固定,我们还可以结合分段打表。

    同时,上述解法的常数非常小。可以看出即使上面的参考实现仍有优化空间,这段代码仍然能在 P5495 取得优于一般 O(nloglogn)O(n\log\log n) 写法的前缀和的结果。

    虽然复杂度更劣,但是由于好写好调常数小,暴力仍然是一个不错的选择。希望大家能够学会这种写法。

    • 1

    信息

    ID
    990
    时间
    4000ms
    内存
    1024MiB
    难度
    6
    标签
    递交数
    1
    已通过
    0
    上传者