本文已参与「新人创作礼」活动,一起开启掘金创作之路。
整除分块
给出一道例题,已知n, 求
这就是整除分块的基本例题
画一个表格找一下怎么计算,以n = 15为例
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 15 | 7 | 5 | 3 | 3 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
证明:
假设分块的左端点为l,要求分块的右端点r.
设分块的值为k,对于区间的每个数满足, 即,需要找到最大的i使其成立
可得
计算的相关代码如下:
每次计算出相同值的左右端点 ,那么相同值的个数就为
int res = 0;
for(int l = 1, r; l <= n; l = r + 1)
{
r = n / (n / l);
res += n / l * (r - l + 1);
}
题目
简单版
链接:
状态表示:
: i变为1的种类数
状态转移:
前一部分是考虑减法的方程,后一部分是考虑除法的方程
-
减法:可以使用前缀和进行优化
-
除法: 考虑使用整除分块
复杂度为
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
void solve()
{
int n, m;
cin >> n >> m;
vector<ll> f(n + 1, 0), s(n + 1, 0);
f[1] = 1;
s[1] = 1;
for(int i = 2; i <= n; i++)
{
f[i] = (f[i] + s[i - 1]) % m;
for(int l = 2, r; l <= i; l = r + 1)
{
r = i / (i / l);
int cnt = r - l + 1;
ll x = f[i / l] * cnt % m;
f[i] = (f[i] + x) % m;
}
s[i] = s[i - 1] + f[i];
s[i] %= m;
}
cout << f[n] % m << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
// cin >> t;
t = 1;
while(t--)
solve();
return 0;
}
正常版
这道题n的限制变大,整除分块无法过去
反过来考虑,从小到大进行考虑
状态表示:
i变到n的方案数
状态转移:
状态转移公式可能看着很难懂,下面进行解释:
-
前一部分:通过加法变到
j的方案和,i可以变到的任意一个- 计算方法:可以通过后缀和进行计算
-
后一部分:通过乘法变到
k的方案数,因为题目是除法,我们反过来就变成了乘法,除法进行考虑,我们找一下原始的一个区间,区间中的每一个值可以通过除法变到同一个值-
若除
2,区间中的数可以变到i, 计算次数为 -
若除
3,区间中的数可以变到i,计算次数为 -
若除
4,区间中的数可以变到i,计算次数为 -
...
-
若除
j,区间中的数可以变到i,计算次数为 -
统计方法:后缀和:
-
总的计算次数就是 后面的是调和级数,复杂度为,故总的复杂度为
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
void solve()
{
int n, m;
cin >> n >> m;
vector<ll> f(n + 2, 0), s(n + 2, 0);
f[n] = 1;
s[n] = 1;
for(int i = n - 1; i >= 1; i--)
{
f[i] = (f[i] + s[i + 1]) % m;
for(int j = 2; j * i <= n; j++)
{
// [i * j, i * j + j - 1]
f[i] = (f[i] + s[i * j] - s[min(i * j + j, n + 1)]) % m;
f[i] = (f[i] + m) % m;
}
s[i] = (s[i + 1] + f[i]) % m;
}
cout << f[1] << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
// cin >> t;
t = 1;
while(t--)
solve();
return 0;
}