题意
如果一个数在经过若干次或 0 0 0 次除以 d d d 并向下取整后与一个被称作“原数”的数相等,则说这个数能被这个原数消除。
给定 n n n 个数 a a a、 m m m 个原数 b b b、 d d d 和 q q q 组形如 l , r l,r l,r 的询问,求在用原数尽可能多的消除 a l , a l + 1 , … , a r a_l,a_{l+1},\dots,a_r al,al+1,…,ar 的情况下最少需要几个原数。原数可以重复使用且不会变化。
- 原题链接:P5629 【AFOI-19】区间与除法
解法
题目中
m
m
m 只有
60
60
60,这提示我们可以把原数的使用信息用一个 long long
存起来。对于第
i
i
i 个原数
b
i
b_i
bi,如果二进制下第
i
i
i 位为
1
1
1 则表示决定使用
b
i
b_i
bi 去消除,为
0
0
0 则表示不决定使用。如果我们知道
a
a
a 中每个数使用了哪个原数消除,就可以用一个 st
表快速预处理出消灭
[
i
,
i
+
2
j
)
[i,i+2^j)
[i,i+2j) 中的数用了哪些原数去消除尽可能多的元素,最后也就可以
O
(
1
)
O(1)
O(1) 回答了。
考虑如何快速判断 x x x 可以被什么原数消除。题目的数据范围中给到了一个 30 % 30\% 30% 的部分分:
对于 30 % 30\% 30% 的数据: … , d = 2 , … \dots,d=2,\dots …,d=2,…
所以我们先考虑 d = 2 d=2 d=2,尝试从它推出其余情况。
以十进制中的
11
11
11 为例,它在二进制中表示为
(
1011
)
2
(1011)_2
(1011)2,即
2
3
×
1
+
2
2
×
0
+
2
1
×
1
+
2
0
×
1
2^3\times1+2^2\times0+2^1\times1+2^0\times1
23×1+22×0+21×1+20×1。将其除以
2
2
2 并向下取整:
⌊
11
÷
2
⌋
=
⌊
(
2
3
×
1
+
2
2
×
0
+
2
1
×
1
+
2
0
×
1
)
÷
2
⌋
=
⌊
2
2
×
1
+
2
1
×
0
+
2
0
×
1
+
1
2
⌋
=
2
2
×
1
+
2
1
×
0
+
2
0
×
1
=
(
101
)
2
\begin{aligned} {} &\ \lfloor11÷2\rfloor &\\ = &\ \lfloor(2^3\times1+2^2\times0+2^1\times1+2^0\times1)÷2\rfloor &\\ = &\ \lfloor2^2\times1+2^1\times0+2^0\times1+\frac{1}{2}\rfloor&\\ = &\ 2^2\times1+2^1\times0+2^0\times1&\\ = &\ (101)_2 \end{aligned}
==== ⌊11÷2⌋ ⌊(23×1+22×0+21×1+20×1)÷2⌋ ⌊22×1+21×0+20×1+21⌋ 22×1+21×0+20×1 (101)2
通过归纳可以推出:一个数除以
2
2
2 并向下取整,相当于在其二进制中整体向右移一位,舍弃最低位。想必大家早就知道了
如果我们把 2 2 2 进制推广到 d d d 进制,这个规则同样适用:一个数除以 d d d 并向下取整,相当于在其 d d d 进制中整体向右移一位,舍弃最低位。此时我们发现,如果一个原数与 x x x 在 d d d 进制表示下的某一段前缀相同,则 x x x 就可以被这个原数消灭。
所以我们只需判断 x x x 在 d d d 进制表示下,是否有一段前缀是某个在 d d d 进制表示下的原数即可。
显然 d d d 进制下位数越少的原数,它能消除的数越多。所以我们总是贪心的选择位数少的原数。
实现
对于快速判断前缀是否相同,Trie
树是很理想的数据结构。我们把所有的原数按照
d
d
d 进制下高位在前、低位在后,插入一棵 Trie
里,并标记每个原数在 Trie
中的末端节点。
这里有一个小优化:将原数从小到大插入 Trie
,如果在插入过程中发现了一个原数的结尾,即有一个原数在
d
d
d 进制下为自己的前缀,则后面的求解中肯定会使用前者而不是自己,此时就可以直接退出了。
设
f
i
,
j
f_{i,j}
fi,j 表示在尽可能消灭
[
i
,
i
+
2
j
)
[i,i+2^j)
[i,i+2j) 的情况下原数的使用信息,
f
i
,
j
f_{i,j}
fi,j 在二进制下第
x
x
x 位为
0
0
0 表示不使用
b
x
b_x
bx,为
1
1
1 则表示使用。对于第
i
i
i 个数
a
i
a_i
ai,如果在 Trie
树中找到了
d
d
d 进制下能找到的长度最短的原数
b
k
b_k
bk,则将
f
i
,
0
f_{i,0}
fi,0 设为
2
k
−
1
2^{k-1}
2k−1(因为
2
x
2^x
2x 表示二进制的第
(
x
−
1
)
(x-1)
(x−1) 位为
1
1
1),表示消灭
a
i
a_i
ai 使用了
b
k
b_k
bk;如果找不到则默认
f
i
,
0
=
0
f_{i,0}=0
fi,0=0。
在求
f
f
f 全体值的时候,从小到大枚举
j
j
j。设当前枚举到
i
i
i,则
f
i
,
j
−
1
or
f
i
+
2
j
−
1
,
j
−
1
→
f
i
,
j
f_{i,j-1}\ \text{or}\ f_{i+2^{j-1},j-1}\to f_{i,j}
fi,j−1 or fi+2j−1,j−1→fi,j。其中
or
\text{or}
or 表示按位或,即 |
,表示用
[
i
,
i
+
2
j
−
1
)
,
[
i
+
2
j
−
1
,
i
+
2
j
)
[i,i+2^{j-1}),[i+2^{j-1},i+2^j)
[i,i+2j−1),[i+2j−1,i+2j) 的信息合并出
[
i
,
i
+
2
j
)
[i,i+2^j)
[i,i+2j) 的信息。
最后对于一组询问 ( l , r ) (l,r) (l,r),令 k = ⌊ log 2 ( r − l + 1 ) ⌋ k=\lfloor\log_2(r-l+1)\rfloor k=⌊log2(r−l+1)⌋,则最终的原数使用信息 a n s = f l , k or f r − 2 k , k ans=f_{l,k}\ \text{or}\ f_{r-2^k,k} ans=fl,k or fr−2k,k。输出 a n s ans ans 在二进制上有几个 1 1 1 即为答案。
个人感觉这部分倍增实现可以参考 RMQ
,两者因为有
x
or
x
=
x
x\ \text{or}\ x=x
x or x=x 或
max
(
x
,
x
)
=
min
(
x
,
x
)
=
x
\max(x,x)=\min(x,x)=x
max(x,x)=min(x,x)=x 的性质,才可以用两段有重复部分的区间信息合并出大区间的信息。也就是说,它们都可以用相交的两区间的并,推出大区间。而像加法、乘法等
x
+
x
≠
x
,
x
2
≠
x
x+x≠x,x^2≠x
x+x=x,x2=x 的运算就不能这么做。
时间复杂度:Trie
树部分时间复杂度约为
O
(
n
log
d
A
)
O(n\log_dA)
O(nlogdA),其中
A
A
A 是原数最大值;倍增部分预处理
O
(
n
log
n
)
O(n\log n)
O(nlogn),总查询复杂度
O
(
q
)
O(q)
O(q),总时间复杂度约为
O
(
n
log
n
)
O(n\log n)
O(nlogn)。
代码
#include<bits/stdc++.h>
#define ll long long
#define lowbit(x) ((x) & (-(x)))
using namespace std;
const int maxn = 5e5 + 5;
const int maxd = 10;
int Trie[maxn][maxd],cnt = 1,d;
int tail[maxn],b[maxn];
void insert(ll x,int i) {
int now = 1,tot = 0;
while (x > 0) b[++ tot] = x % d, x /= d;
while (tot) {
int nxt = b[tot --];
if (!Trie[now][nxt]) Trie[now][nxt] = ++ cnt;
now = Trie[now][nxt];
if (tail[now]) return ; // 已经发现有更优的原数能替代自己,直接退出。
}
tail[now] = i;
}
int query(ll x) {
int now = 1, tot = 0;
while (x > 0) b[++ tot] = x % d, x /= d;
while (tot) {
int nxt = b[tot --];
if (!Trie[now][nxt]) return -1;
now = Trie[now][nxt];
if (tail[now]) return tail[now];
}
return -1;
}
int bitcount(ll x) { // 数出二进制上有几个1,即使用了几个原数。
int res = 0;
while (x > 0)
res += x & 1, x >>= 1;
return res;
}
int n,m,Q;
ll a[maxn],f[maxn][30];
int main() {
scanf("%d%d%d%d",&n,&m,&d,&Q);
for (int i = 1;i <= n;i ++) scanf("%lld",&a[i]);
for (int i = 1;i <= m;i ++) {
ll yuan; scanf("%lld",&yuan);
insert(yuan,i);
}
for (int i = 1,pos;i <= n;i ++)
if ((pos = query(a[i])) != -1)
f[i][0] = 1ll << (pos - 1);
for (int i = 1;i <= 20;i ++)
for (int j = 1;j + (1 << i) - 1 <= n;j ++)
f[j][i] = f[j][i - 1] | f[j + (1 << (i - 1))][i - 1];
for (int i = 1,l,r,k;i <= Q;i ++) {
scanf("%d%d",&l,&r);
k = log2(r - l + 1);
printf("%d\n",bitcount(f[l][k] | f[r - (1 << k) + 1][k]));
}
return 0;
}