P7960 [NOIP2021] 报数 题解

仔细看一眼题目,发现每次询问都只给出一个数,因此可以想到需要预处理。那么怎么进行预处理呢?

一开始可能会想到这样一种算法:把 $[1,\max{x}]$ 中每一个含有 $7$ 的整数存入队列中,在数的前面、后面补上任意数位,具体实现可以使用搜索。但仔细一想,发现这样重复的状态多、效率极低,同时写起来也比较麻烦,不可行。

既然枚举出所有不能报出的整数不行,那我们能不能从所有的整数中找到不能报出的整数呢?

要想找到这样的整数,我们可以在一个足够大的整数范围之内进行判断。具体地,对于每个待判断的整数,先看是否含有数位 $7$。若含有,则对该整数在区间范围内的所有整数倍进行覆盖。因此可以写出这样的初步代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool check(int x)
{
while(x)
{
if(x%10==7)return true;
x/=10;
}
return false;
}
void init(int N) // N 表示枚举整数的上限
{
for(int i=2;i<=N;++i)
for(int j=1;i*j<=N;++j) // 覆盖该整数的所有正整数倍
flag[i*j]=true; // flag[i] 为真表示该数不能被报出
}

这样我们便可以在 $\mathcal O(N^2 \lg N)$ 的复杂度内找出所有不能被报出的数。

对于询问,我们可以 $\mathcal O(N)$ 预处理比每个能被报出的整数大的第一个不能被报出的整数,再单次 $\mathcal O(1)$ 询问。当然,也可以将预处理之后的数据直接进行存储,然后在询问时临时进行二分,单次询问复杂度为 $\mathcal O(\log_2{\text{len}})$($\text{len}$ 表示区间范围内能报出的整数的数量)。

但是,询问整数最大值在 $10^7$ 数量级下,肯定会超时。不难发现,每次覆盖的复杂度为 $\mathcal O(N)$。但如果当前枚举到的整数已经被覆盖到,那么它的倍数也一定已经被覆盖过了。因此对于已经覆盖到的整数,不再进行覆盖:

1
2
3
4
5
6
7
8
void init(int N)
{
for(int i=2;i<=N;++i)
{
if(flag[i]||!check(i))continue;
for(int j=1;i*j<=N;++j)flag[i*j]=true;
}
}

接着我们需要考虑 $N$ 的取值大小。由于 $10^7$ 本身可以被报出的,因此需要找到大于该数且不能被报出的最小整数。经过试验可知为 $10^7+1$。因此取 $N=10^7+1$ 即可。

该部分的代码乍一看像埃氏筛,但实际上与埃氏筛的复杂度并不相同,而且比较难证。不过,我们可以通过一个更为直观的方法证明该算法不会超时:

当 $N$ 取 $10^7+1$ 时,经过试验可知,筛选过程中不需要继续向后覆盖的整数个数为 $923655$。

也就是说对于其中 $923655$ 个整数,只需进行一次覆盖操作;而剩余的无需覆盖。

单次覆盖操作的复杂度取决于 $N$ 和当前整数的大小。设当前整数为 $i$,则单次覆盖的期望复杂度为 $\mathcal O(\dfrac{N}{i})$。

那么总覆盖的复杂度的常数在最坏情况下等于 $\sum_{i=1}^{923655} \dfrac{10^7+1}{i} \approx 143133115 \approx 1.4 \times 10^8$,可以保证不超时。实际上,需要被覆盖的不可能只集中在前面。由于最坏情况的效率都可以保证,因此这样的做法显然是可以通过的。

因此我们可以得到采用这种方法的两种代码:

单次 $\mathcal O(1)$ 询问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<bits/stdc++.h>
using namespace std;
int T,a[10000101];
bool flag[10000101];
bool check(int x)
{
while(x)
{
if(x%10==7)return true;
x/=10;
}
return false;
}
void init(int N)
{
for(int i=2;i<=N;++i)
{
if(flag[i]||!check(i))continue;
for(int j=1;i*j<=N;++j)flag[i*j]=true;
}
for(int i=1,t=1;i<=N;++i)
{
if(!flag[i])
{
a[t]=i;
t=i;
}
}
}
int main()
{
init(10000001);
read(T);
for(int i=1,x;i<=T;++i)
{
read(x);
if(flag[x])
{
pc('-');
pc('1');
}
else write(a[x]);
pc('\n');
}
fwrite(obuf,p3-obuf,1,stdout);
return 0;
}

每次在 $\mathcal O(\log_2{\text{len}})$ 的复杂度内通过二分进行询问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<bits/stdc++.h>
using namespace std;
int T,len,a[800001];
bool flag[10001001];
bool check(int x)
{
while(x)
{
if(x%10==7)return true;
x/=10;
}
return false;
}
void init(int N)
{
for(int i=2;i<=N;++i)
{
if(flag[i]||!check(i))continue;
for(int j=1;i*j<=N;++j)flag[i*j]=true;
}
for(int i=1;i<=N;++i)
{
if(flag[i])continue;
a[++len]=i;
}
}
int binary(int l,int r,int x)
{
while(l<=r)
{
int mid=(l+r)>>1;
if(a[mid]>x)r=mid-1;
else l=mid+1;
}
return a[l];
}
int main()
{
init(10000001);
read(T);
for(int i=1,x;i<=T;++i)
{
read(x);
if(flag[x])
{
pc('-');
pc('1');
}
else write(binary(1,len,x));
pc('\n');
}
fwrite(obuf,p3-obuf,1,stdout);
return 0;
}

P7960 [NOIP2021] 报数 题解

https://hensier.github.io/OI/P7960/

作者

hensier

发布于

2021-11-30

更新于

2023-01-02

许可协议

评论