P5629 【AFOI-19】区间与除法 题解

题意

如果一个数在经过若干次或 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 的情况下最少需要几个原数。原数可以重复使用且不会变化。

解法

题目中 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} 2k1(因为 2 x 2^x 2x 表示二进制的第 ( x − 1 ) (x-1) (x1) 位为 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,j1 or fi+2j1,j1fi,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+2j1),[i+2j1,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(rl+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 fr2k,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;
}
  • 43
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值