我不懂我也看了好久才用数字硬写明白的
其实这个知识点主要来源于看牛客网上面的一道题 :NC 13221 数码。二话不说上个链接先:
https://ac.nowcoder.com/acm/problem/13221
好吧最开始这道题我看漏了个条件,就是x其实不止一个是一个区间的(我就说一个三星题怎么可能这么暴力!!)
目前自己想到的就只要双for,很明显,这肯定就直接T掉。所以直接搜了题解,才发现需要用到数论里的整除分块。
那现在就开始我对整除分块的理解吧!
1.故名思意,就是将整除的数分个块而已。(好的你已经学会了,干题去吧)(不想看我痛苦的心路历程的建议直接空降到第五段)
2.那到底怎样个分块法呢。细看这道题,让我们求区间[l, r]内所有x的因数的首位数字出现的个数。枚举x的因数???不不不这太浪费时间了,求区间内所有数满足条件的值的数量,通常我们都是用区间相减的办法来做的,这道题也一样。
3.由于[l, r] = [1, r] - [1, l - 1]。那么在求解的时候我们只需求两个区间内满足条件的值的数量相减就可以了,这里就可以直接写成一个solve(MAX, i)函数就好了。在这里MAX是指区间右端点,那么i 呢?我先卖个关子,先往后看。
4.那么这道题是不是就变成了求[1, MAX]内所有数的因数首位数字出现的个数了。刚刚我们说了,不可能一个个枚举x再一个个枚举x的因数吧,这样的话时间复杂度就变成了
O
(
n
∗
s
q
r
t
(
n
)
即
O
(
n
3
/
2
)
O(n*sqrt(n)即O(n^{3 / 2})
O(n∗sqrt(n)即O(n3/2)然后你就和我一样超时了 嘿嘿嘿嘿嘿嘿。
5.换一个思路对于一个因子来说,它可能是很多个数都有的因子,那么我们只要枚举因子,再判断区间内有多少个数字有这个因子,再判断因子首位不就行了,这样时间复杂度就不会超时了。所以关键问题是怎么分这个区间!所以这里就要用到我们说的整除分块了。
6.对于一个区间块[a, b] (1 <= a <= b <= n)来说,不难发现,关于任意一个数k,设其为一个因子, 其在区间[1, n]的数量都是a / k个。比如[1, 30]含有因子8的就只有30 / 8 = 3 个(8,16,24);而这个区间对于含有因子9、10的数字的数量居然也是3,那么我们是不是可以尝试将这种情况的区间(含有某个因子且这种数字个数相等的区间)划分到一块呢,那我们所举例区间块就应该为[8,10],这就是整除分块的思想了。
7.整除分块就是计算 ∑ i = 1 n n / i \sum_{i=1}^n n/ i i=1∑nn/i的和。我们先来打个表,就拿n = 30,来讲
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
10/i | 30 | 15 | 10 | 7 | 6 | 5 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | 2 |
i | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
10/i | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
8.从这个表可以看出当我们令一个区间的左端点为L,那么其右端点R就是n / (n / L)这样的区间可以使得其内部任何值a,都可以使得[1, n]内含有相同个数( = n / L个)的含因子a的数(这是对第七段的一个解释),从而起到简化这个sum函数运算时间的作用。
整除分块模板示例
ll g(ll x){
ll ans = 0;
for(ll l = 1, r; l <= x; l = r + 1 ){
r = x / (x / l); //区间右端点
ans += (r - l + 1) * (n / l); // 区间长度 * 区间内因子个数
}
return ans;
}
9.到这里就轻松了,拿上表的一个例子来说,对于因子16,其[L, R] 为[16, 30] 那么区间内任意数首位数只有1, 2,3. 我们要想统计首位数出现的次数,就可以使其分为三个区间即:[16, 19], [20, 29], [30, 30]这样就能统计出再这个范围内所有x的因子的首位数出现的次数即: 1 的 次 数 = ( 19 − 16 + 1 ) ∗ ( 30 / 16 ) 1的次数 = (19 - 16 + 1) * (30 / 16) 1的次数=(19−16+1)∗(30/16) : (这个式子的含义即是:在[16, 19]这个区间内,任意一个数a, 其[1, a]区间内含有 30 / 16 个 含有因子为16的值, 而a的取值共有 19 - 16 + 1 种情况, 所以相乘即再这个区间取一个值为区间右端点, 其区间内含有因子为16的数的个数为4(这个式子的答案)), 剩余两个区间以此类推,分别是: 2 的次数 = (29 - 20 + 1) * (30 / 20) = 10, 3 的次数 = (30 - 30 + 1) * (30 / 30) = 1. 那么再用这种方法推出其他因子的情况就好了.
10.注意了,此题求的是因子最高位数码出现的个数,那我们可以直接先枚举因子最高位数码,然后再把写出区间(上述区间分块过程反过来), 例如找首位为1 的因子,就是再区间[1, 2), [10, 20), [100, 200)… (这类区间我们在代码里称为10的倍数区间) 分别这些区间里面将含有某个因子数量相等的值分块就好了, 那这道题就出来了.所以之前留下来的solve函数的第二个参数i, 其实就是首位的数码.
所以上代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll solve(int x, int v){
ll res = 0;
for(int i = 1; i <= x / v; i *= 10){ //枚举位数
int l = i * v, r = min(x, l + i - 1);// 1, 10, 100...的倍数的区间即[v * 10^k, (v+1) * 10^k - 1]
for(int L = l, R; L <= r; L = R + 1){//在10的倍数的区间内枚举含有相同数量的含有因子L的数.
R = min(x / (x / L), r);//枚举的右端点
res += 1LL * (R - L + 1) * (x / L);//区间内任意数a,[1, a]中含有相同数量的因子L的数的总数.加之前的
}
}
return res;
}
int main (){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int l, r; cin >> l >> r;
for(int i = 1; i <= 9; i ++){//按因子的首位枚举输出.
cout << solve(r, i) - solve(l - 1, i) << "\n";
}
}