杜教篩
問題一般是求$$\sum_{i=1}^{n}f(i)$$這樣的式子。
然后我們有一種很妙的想法,那就是構造兩個積性函數\(h,g\),使得\(h=f*g\)
然后嘗試推一下\(h\)的前綴和,發現:
如果記\(S(n)=\sum_{i=1}^nf(i)\),那么就可以寫成:$$\sum_{i=1}^{n}h(i)=\sum_{d=1}^{n}g(d)\cdot S(\lfloor\frac{n}{d}\rfloor)$$
把\(d=1\)提出來,得到我們想要的式子:$$g(1)S(n)=\sum_{i=1}^{n}h(i)-\sum_{d=2}^{n}g(d)\cdot S(\lfloor\frac{n}{d}\rfloor)$$
那么如果\(h\)的前綴和我們可以在一個比較優秀的時間復雜度內求出,則后面那一部分的時間復雜度在進行整除分塊后,可以達到一個優秀的時間復雜度\(O(n^{\frac{2}{3}})\)。
一些題目
-
題意:
求$$\sum _{i=a}^{b} \mu(i)(a\le b\le 10^{10})$$
-
解法一:
直接套用上面的卷積。我們知道$$\mu * 1=\epsilon$$
且\(1,\epsilon\)都是積性函數,所以可以直接得到:$$\displaystyle S(n)=1 - \sum _{i=2}^{n} S(\lfloor \frac{n}{i} \rfloor)$$
-
解法二:
還是來推一下式子,其實思路是一樣的,都用到了$$\sum_{d|n}\mu(d)=[n=1]$$這條式子。
提出$j=1$,得到$$\sum _{i=1}^{n} \mu(i)=1-\sum_{i=2}^{n} \sum_{j=1}^{\lfloor \frac{n}{i}\rfloor}\mu(j)$$然后就轉化為上面的式子。
-
Trick
這里有幾個需要注意的地方。一個是我們可以預處理一些\(\le \sqrt{n}\)的前綴和來加快速度(事實上,根據唐教的分析,如果不預處理,時間復雜度是\(O(n^{\frac{3}{4}})\)的),另外一個就是hash時候的技巧,與\(Min25\)篩不同,杜教篩的篩法需要用map記錄一些已經算過的前綴和來保證時間復雜度,而這個時候我們其實可以不用map,而是用兩個表去處理,按照是否\(\le \sqrt{n}\)來處理。時間復雜度優秀許多,具體參見代碼。
\(code_1\)(用map儲存):
注意map <LL,LL> :: iterator iter,iter = h.find(x),iter->second的用法
#include <iostream>
#include <cstring>
#include <cstdio>
#include <map>
#define F(i, a, b) for (int i = a; i <= b; i ++)
const int B = 5000000;
typedef long long LL;
using namespace std;
LL a, b, S[B + 1], mu[B]; int z[B];
bool bz[B + 1];
map <LL, LL> H;
map <LL, LL> :: iterator iter;
void Init() {
mu[1] = 1;
F(i, 2, B) {
if (!bz[i])
z[++ z[0]] = i, mu[i] = - 1;
F(j, 1, z[0]) {
LL x = 1ll * z[j] * i;
if (x > B) break;
bz[x] = 1;
if (i % z[j] == 0) {
mu[x] = 0;
break;
}
mu[x] = mu[z[j]] * mu[i];
}
}
F(i, 2, B)
mu[i] += mu[i - 1];
}
LL Solve(LL x) {
if (x <= B) return mu[x];
iter = H.find(x);
LL ans = iter -> second; //我竟然第一次知道要這樣用。。。
if (iter == H.end()) {
ans = 0;
for (LL i = 2, j; i <= x; i = j + 1) {
j = x / (x / i);
ans += (j - i + 1) * Solve(x / i);
}
ans = 1 - ans;
H[x] = ans;
}
return ans;
}
int main() {
scanf("%lld%lld", &a, &b);
Init();
printf("%lld\n", Solve(b) - Solve(a - 1));
}
\(code_2\)(推薦儲存方式):
#include <iostream>
#include <cstring>
#include <cstdio>
#include <map>
#define F(i, a, b) for (int i = a; i <= b; i ++)
const int B = 5000000;
typedef long long LL;
using namespace std;
LL a, b, n, S[B + 1], vis[B / 100], mu[B];
int z[B]; bool bz[B + 1];
map <LL, LL> H;
map <LL, LL> :: iterator iter;
void Init() {
mu[1] = 1;
F(i, 2, B) {
if (!bz[i])
z[++ z[0]] = i, mu[i] = - 1;
F(j, 1, z[0]) {
LL x = 1ll * z[j] * i;
if (x > B) break;
bz[x] = 1;
if (i % z[j] == 0) {
mu[x] = 0;
break;
}
mu[x] = mu[z[j]] * mu[i];
}
}
F(i, 2, B)
mu[i] += mu[i - 1];
}
LL Solve(LL x) {
if (x <= B) return mu[x];
LL y = n / x;
if (!vis[y]) {
LL ans = 0;
for (LL i = 2, j; i <= x; i = j + 1) {
j = x / (x / i);
ans += (j - i + 1) * Solve(x / i);
}
vis[y] = ans = 1 - ans;
}
return vis[y];
}
LL Doit(LL x) {
memset(vis, 0, sizeof vis), n = x;
return Solve(x);
}
int main() {
scanf("%lld%lld", &a, &b);
Init();
printf("%lld\n", Doit(b) - Doit(a - 1));
}
Min25篩
也是一種很優秀的求積性函數前綴和的篩法。據zzq大神說它的時間復雜度是\(O(\frac{n}{log_nlog_n})\)??
為了下面敘述方便,記\(P\)表示素數集合,記\(P_i\)表示第\(i\)個質數,其中\(|P|=\sqrt{n}\)。
此類問題的\(F(i)\)一般滿足:
則min25篩的想法是把所有的\(F(i)\)的和分成質數和合數去考慮,即總的貢獻 = 質數的貢獻 + 合數的貢獻,下面講解兩種方法:
遞歸版本
那么先考慮質數的貢獻,即\(i\)為質數,且保證括號內條件成立時的情況下的貢獻。
若記\(min(p)\)表示\(i\)的最小質因子以及$$g(n,j)=\sum_{i=1}^n[i\in P, min(p)\gt P_j]F(i)$$
則由定義,不難推出$$g(n,j)=\begin{cases} g(n,j-1)&{P_j}^2\gt n\ g(n,j-1)-F(P_j)[g(\frac{n}{P_j},j-1)-\sum_{i=1}^{j-1}F(P_i)]&{P_j}^2\le n\end{cases}$$
其中\(g(n,0)\)初值比較難理解。事實上,我們觀察這整一個遞推過程,實際上很類似埃氏篩,每一次都把一個\(P_j\)的貢獻給刪去,最終篩到\(\sqrt{n}\)時,保證了還沒有被質數篩去的數在\(n\)范圍內一定不是合數。
那么繼續考慮埃氏篩,實際上一開始我們是把所有數都看成質數的。那么\(g(n,0)\)的初值也就不難想了,可能是某個與\(n\)有關的多項式,例如在求質數個數中\(f(i)=1\),那么\(g(n,0)=n-1\).
現在回到原問題$$\sum_{i=1}^nF(i)$$上,我們要求質數的貢獻,實質上就是\(g(n,|P|)-g(P_{j-1},j-1)\)
如果記$$S(n,j)=\sum_{i=2}^nF(i)[i的最小質因子大於等於P_j]$$
那么可以推出$$S(n,j)=g(n,|P|)-\sum_{i=1}^{j-1}f(P_i)+\sum_{k\ge j}^{|P|}\sum_{e}(f({P_k}^e)S(\frac{n}{{P_k}^e},k+1)+f({P_k}^{e+1}))$$
事實上,后面那一部分式子就是合數的貢獻,枚舉質數\(k\),次數\(e\),然后與求\(g\)的思想是完全類似的,注意要加上一個\(f({P_k}^{e+1})\)就好了.
注意,這里我們所說的遞歸版本,實際上指的是\(s\)的求法是用遞歸,實際實現中,\(g\)的求法可以不用遞歸,具體請看代碼,並且求\(s\)時可以不用記憶化,時間復雜度依舊優秀。
遞推版本
實際上遞推版本比遞歸更慢。因為它求的是\(\sqrt{n}\)個\(\frac{n}{i}\)的前綴和。
具體細節與遞歸版本類似。我們直接設$$S'(n,i)=\sum_{i=2}^nF(i)[i的最小質因子不小於p_i或i是質數]$$.
可以得到:$$S'(n,i)=\left{
\begin{array}{}
S'(n,i+1)+\sum_{e\geq 1且\ p_i^{e+1}\ \leq n}\ \ F(p_i^e)[S(\lfloor\frac{n}{p_i^e}\rfloor,i+1)-g(p_i,i)]+F(p_i^{e+1})& &{p_i^2\leq n} \
S'(n,i+1)& &{p_i^2\geq n}
\end{array}\right.$$
初值\(S'(n,|P|+1)=g(n,|P |)\)
一些題目
-
事實上就是求\(1\sim n\)素數,這個可以直接通過求的\(g\)來得到,連\(s\)都不用求。
注意實現的時候只需要保存\(\frac{n}{i}\)的\(\sqrt{n}\)個值即可。
#include <bits/stdc++.h>
#define F(i, a, b) for (int i = a; i <= b; i ++)
const int N = 700000;
using namespace std;
long long n, m, id1[N], id2[N], w[N], g[N], z[N], sqr;
bool bz[N];
int main() {
scanf("%lld", &n), sqr = int(sqrt(n));
F(i, 2, sqr) {
if (!bz[i])
z[++ z[0]] = i, bz[i] = 1;
F(j, 1, z[0]) {
if (z[j] * i > sqr) break;
bz[z[j] * i] = 1;
if (i % z[j] == 0) break;
}
}
for (long long i = 1, j; i <= n; i = j + 1) {
j = n / (n / i); w[++ m] = n / i;
if (w[m] <= sqr) id1[w[m]] = m; else id2[n / w[m]] = m;
g[m] = w[m] - 1;
}
F(j, 1, z[0])
for (int i = 1; i <= m && z[j] * z[j] <= w[i]; i ++) {
int k = (w[i] / z[j] <= sqr) ? id1[w[i] / z[j]] : id2[n / (w[i] / z[j])];
g[i] -= g[k] - (j - 1);
}
printf("%lld\n", g[1]);
}
注意把\(f(p)=p-1\)拆成兩個函數\(g(p)=p,h(p)=1\),然后相減.
這樣可以保證所求是積性函數.
#include <bits/stdc++.h>
#define F(i, a, b) for (int i = a; i <= b; i ++)
typedef long long LL;
const int N = 3e5;
const LL ny = 5e8 + 4, Mo = 1e9 + 7;
using namespace std;
bool bz[N]; int id1[N], id2[N], sqr;
LL g[N], h[N], z[N], w[N], s[N], n, m;
LL S(LL x, int y) {
LL k = x <= sqr ? id1[x] : id2[n / x];
LL sum = (g[k] - h[k] - (s[y - 1] - (y - 1)) + (y == 1) * 2) % Mo;
for (LL i = y; i <= z[0] && z[i] * z[i] <= x; i ++)
for (LL e = 1, p = z[i]; p * z[i] <= x; e ++, p *= z[i])
sum = (sum + (z[i] ^ e) % Mo * S(x / p, i + 1) + (z[i] ^ (e + 1))) % Mo;
return sum;
}
int main() {
scanf("%lld", &n), sqr = int(sqrt(n));
F(i, 2, sqr) {
if (!bz[i])
z[++ z[0]] = i, s[z[0]] = (s[z[0] - 1] + i) % Mo;
F(j, 1, z[0]) {
if (z[j] * i > sqr) break;
bz[z[j] * i] = 1;
if (i % z[j] == 0) break;
}
}
for (LL i = 1, j; i <= n; i = j + 1) {
j = n / (n / i); w[++ m] = n / i;
if (w[m] <= sqr) id1[w[m]] = m; else id2[n / w[m]] = m;
g[m] = (((w[m] + 2) % Mo * ((w[m] - 1) % Mo)) % Mo * ny) % Mo;
h[m] = (w[m] - 1) % Mo;
}
F(j, 1, z[0])
for (LL i = 1; i <= m && z[j] * z[j] <= w[i]; i ++) {
LL k = (w[i] / z[j] <= sqr) ? id1[w[i] / z[j]] : id2[n / (w[i] / z[j])];
h[i] = (h[i] - (h[k] - (j - 1))) % Mo;
g[i] = (g[i] - z[j] * (g[k] - s[j - 1])) % Mo;
}
printf("%lld\n", n == 1 ? 1 : ((S(n, 1) + 1) % Mo + Mo) % Mo);
}
兩種版本的時間復雜度都是\(O(\frac{n^{\frac{3}{4}}}{log(\sqrt n)})\)。經實測,遞歸版會稍微快一些。
經過一系列的容斥后,可以計算。
比如說,要求\(1\sim n\)中不是\(a_i(1\le i\le m)\)的倍數,但要是\(b\)的倍數。
我們就把\(n/b\)之后進行計算。
再譬如,如果在此基礎上,還要求\(f(x)=1\),我們就要特殊考慮了,必須要考慮的是\(b\)的質因數情況,因為我們還是在\(n/b\)的基礎上進行計算,經過一番特殊處理后還是可以做出來,只要透徹理解質數和合數的計算即可。
#include <bits/stdc++.h>
#define F(i, a, b) for (LL i = a; i <= b; i ++)
#define mem(a, b) memset(a, b, sizeof a)
#define get getchar()
typedef long long LL;
const LL M = 3e5 + 10;
using namespace std;
LL n, m, b, sqr, MAX, top, L, r[61], vis[M], s[M], S[M];
LL a[M], g[M], w[M], bz[M], z[M], id1[M], id2[M], R[M];
LL S0, S1, S2, S3; bool flag;
void Re(LL &x) {
char c = get; x = 0; LL t = 1;
for (; !isdigit(c); c = get) t = (c == '-' ? - 1 : t);
for (; isdigit(c); x = (x << 3) + (x << 1) + c - '0', c = get); x *= t;
}
void Init() {
Re(n), Re(m), Re(b);
F(i, 1, m) Re(a[i]);
F(i, 1, 60) Re(r[i]);
r[0] = 1;
}
void GetPrime() {
mem(bz, 0), z[0] = 0, mem(vis, 0), top = 0;
F(i, 2, sqr) {
if (!bz[i]) {
z[++ z[0]] = i, s[z[0]] = s[z[0] - 1] + 1;
vis[z[0]] = 1;
while (top < m && a[top] < i) top ++;
if (a[top] == i)
s[z[0]] --, vis[z[0]] = 0;
}
F(j, 1, z[0]) {
if (z[j] * i > sqr) break;
bz[z[j] * i] = 1;
if (i % z[j] == 0) break;
}
}
}
LL dg(LL x, LL y, LL n, LL t) {
if (z[y] > x) return 0;
LL k = (x <= sqr) ? id1[x] : id2[n / x];
LL sum = (g[k] - s[y - 1]) * r[t];
F(k, y, z[0]) {
if (z[k] * z[k] > x) break;
if (vis[k])
for (LL s = z[k], cnt = 1; s * z[k] <= x; s *= z[k], cnt ++)
if (!t) sum += dg(x / s, k + 1, n, t) + 1; else {
if (r[cnt])
sum += dg(x / s, k + 1, n, t);
sum += r[cnt + 1];
}
}
return sum;
}
LL DG(LL x, LL y, LL n, LL res) {
if (z[y] > x) return 0;
LL k = (x <= sqr) ? id1[x] : id2[n / x], sum;
if (res > 1) sum = 0; else
if (res == 1) sum = (x >= MAX); else
sum = (g[k] - s[y - 1]);
F(k, y, z[0]) {
if (z[k] * z[k] > x) break;
if (vis[k]) {
for (LL s = z[k], cnt = 1; s * z[k] <= x; s *= z[k], cnt ++) {
if (r[cnt + R[k]])
sum += DG(x / s, k + 1, n, res - !r[R[k]]);
if ((res - !r[R[k]]) == 0)
sum += r[cnt + R[k] + 1];
}
if (!r[R[k]]) break;
}
}
return sum;
}
LL Solve(LL n, LL q) {
sqr = q < 2 ? LL(sqrt(n)) : LL(sqrt(max(n, b))), L = 0, GetPrime();
for (LL i = 1, j; i <= n; i = j + 1) {
j = n / (n / i); w[++ L] = n / i;
if (w[L] <= sqr) id1[w[L]] = L; else id2[n / w[L]] = L;
g[L] = w[L] - 1;
}
F(j, 1, z[0])
for (LL i = 1; i <= L && z[j] * z[j] <= w[i]; i ++) {
LL k = (w[i] / z[j]) <= sqr ? id1[w[i] / z[j]] : id2[n / (w[i] / z[j])];
g[i] -= (g[k] - (j - 1));
}
LL j = m;
F(i, 1, L) {
while (a[j] > w[i]) j --;
g[i] -= j;
}
if (q <= 1)
return dg(n, 1, n, q);
else {
LL x = b, res = 0;
F(i, 1, z[0]) {
while (x % z[i] == 0)
R[i] ++, x /= z[i];
s[i] = s[i - 1] + r[R[i] + 1] * vis[i];
S[i] = S[i - 1] + vis[i];
if (vis[i] * (!r[R[i]]))
res ++, MAX = z[i];
}
if (x > 1) {
z[++ z[0]] = x, vis[z[0]] = 1;
while (top < m && a[top] < x) top ++;
if (a[top] == x) vis[z[0]] = 0;
s[z[0]] = s[z[0] - 1] + r[2] * vis[z[0]];
S[z[0]] = S[z[0] - 1] + vis[z[0]];
if (vis[z[0]] * (!r[1]))
res ++, MAX = x;
}
LL j = z[0];
F(i, 1, L) {
while (j > 0 && z[j] > w[i]) j --;
g[i] = (g[i] - S[j]) * (r[1] != 0) + s[j];
}
return DG(n, 1, n, res) + (res == 0);
}
}
int main() {
freopen("qiandao.in", "r", stdin);
freopen("qiandao.out", "w", stdout);
Init();
S0 = Solve(n, 0);
flag = 1;
F(i, 1, m)
if (b % a[i] == 0) { flag = 0; break; }
S1 = flag == 0 ? 0 : Solve(n / b, 0) + (b > 1);
// printf("%lld\n", S0);
// printf("%lld\n", S1);
S2 = Solve(n, 1);
S3 = flag == 0 ? 0 : Solve(n / b, 2);
// printf("%lld\n", S2);
// printf("%lld\n", S3);
printf("%lld\n", n - (n - S0 + S1 + S2 - S3));
}
