1.前言:
这个题目还是挺不错的。我只想出了一个 O ( n l o g n ) O(nlogn) O(nlogn)的方法。对于 O ( n ) O(n) O(n)的欧拉筛+积性函数的方法完全忘干净了。这个题目出题人从一种非常优秀的角度看待这个问题。让这个问题变得非常明了简洁.赞!
2.素数筛法的本质:
素数筛法的本质就是构造一颗筛法DAG。两点之间有边当且仅当它们满足筛法的条件.
例如:
1.普通埃筛:构造DAG的特点:任意两个成倍数关系的数之间都连接有一条边。
复杂度:无非就是分析边的个数.那就分析每个点的出度呗。 ∑ i = 1 n n i = n l o g n \sum_{i=1}^{n}\frac{n}{i}=nlogn ∑i=1nin=nlogn.
2.素数优化埃筛:构造的DAG的特点:任意一个质数与它的倍数之间形成一条链
复杂度:根据素数倒数和定理有: O ( n l o g l o g n ) O(nloglogn) O(nloglogn).
3.线性筛:构造的DAG的特点:任意一个大的数只会连向 G p 1 \frac{G}{p_1} p1G.也就是除掉一个最小质因子后的数.我们发现这个DAG有 n n n个点,除了 1 1 1以外,每个数都会往恰好一个小的数连接一条边。所以是 n − 1 n-1 n−1条边。所以是颗树
复杂度:这个时候就显然了,复杂度严格在 O ( n ) O(n) O(n).
那么有DAG,自然就会有一些dp问题。例如:
当然还有本题.
3.题目大意:
令 f ( i ) f(i) f(i)为将其质因分解后对质数排序组成的十进制数.求 ∑ i = 2 n f ( i ) \sum_{i=2}^nf(i) ∑i=2nf(i).
4.题目思路:
脑子里有了那颗树,一切都显然了。我们可以单纯的对每个数走到根节点,求出 f ( i ) f(i) f(i).但是也可以用 d p dp dp.这里我们之前称之为积性函数。这里从动态规划的角度来讲就是一个符合乘法的转移式。
由于我们的边一定是最小质因子。所以转移的时候 f [ i ∗ p [ j ] ] f[i*p[j]] f[i∗p[j]]一定是在 f ( i ) f(i) f(i)的开头拼接上 p [ j ] p[j] p[j]。这个时候就还需要记录一个长度。转移就完事了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define vi vector<int>
#define vll vector<ll>
#define fi first
#define se second
const int maxn = 4e6 + 5;
const int mod = 1e9 + 7;
int bk[maxn] , cnt;
ll p[maxn / 4] , f[maxn] , g[maxn] , ans;
ll solve (int n)
{
for (int i = 2 ; i <= n ; i++){
if (!bk[i]){
p[++cnt] = i;
f[i] = i;
int x = i;
g[i] = 1;
while (x){
x /= 10;
g[i] *= 10;
}
}
for (int j = 1 ; j <= cnt && p[j] * i <= n ; j++){
bk[i * p[j]] = 1;
f[i * p[j]] = (f[i] + g[i] * f[p[j]]%mod)%mod;
g[i * p[j]] = g[i] * g[p[j]]%mod;
if (i % p[j] == 0) break;
}
}
for (int i = 2 ; i <= n ; i++){
ans = (ans + f[i]) % mod;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
int n; cin >> n;
cout << solve(n) << endl;
return 0;
}