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

文章讲述了如何使用Trie树数据结构解决一个关于区间和除法的问题,通过分析数在d进制下的特性,确定最小数量的原数消除给定数列中的元素。算法利用了原数的使用信息和二进制位操作,达到线性查询复杂度。

题意

如果一个数在经过若干次或 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;
}
# P5629AFOI-19区间除法 ## 题目背景 SY 好不容易才解出QM给她的数学题,在恰午饭的时候,QM 向她的脑洞里塞了个幻想的泡泡……SY 戳开一看,又是长长的一串数字! SY 实在是不想思考了,她决定用小学的除法消灭她脑洞里的数字. ## 题目描述 定义 $op$ 操作意义为将当前数除以 $d$ 并向下取整. SY 现在有 $m$ 个“原数”,若一个数经过若干次 $op$ 操作(包括 $0$ 次)后能变为这个“原数”,那么这个数是可以被这个“原数”所消灭的。注意,“原数”是不会被消耗的. 现在 SY 想问你,对于一个区间 $[l,r]$,在消灭最多个数的前提下最少需要多少个“原数”? ## 输入格式 第一行 $4$ 个数,分别是 $n,m,d,q$,分别表示数列 $\{a\}$ 元素个数,SY 拥有的 “原数” 个数,$op$ 操作参数,询问个数。 第二行为 $\{a\}$ 数列,即需要被消灭的数列。 第三行为 $m$ 个“原数”。 接下来 $q$ 行,每行两个数 $l$ 和 $r$,表示询问区间为 $[l,r]$。 ## 输出格式 按照询问顺序,每一行输出一个整数表示答案. ## 输入输出样例 #1 ### 输入 #1 ``` 2 3 3 3 0 20 6 6 6 1 1 2 2 1 2 ``` ### 输出 #1 ``` 0 1 1 ``` ## 输入输出样例 #2 ### 输入 #2 ``` 6 3 3 3 6 5 10 15 19 7 2 5 10 1 6 1 4 4 6 ``` ### 输出 #2 ``` 3 3 2 ``` ## 说明/提示 #### 样例解释: **#样例1** : $20$ 经过一次 $op$ 操作(除以 $3$ 向下取整)可以变成 $6$,而 $0$ 不能经过若干次 $op$ 操作变成 $6$ 。 所以区间 $[1,1]$ 最多消灭 $0$ 个数,消灭最多数前提下最少需要 $0$ 个 "原数",区间 $[1,2],[2,2]$ 最多消灭 $1$ 个数,消灭最多数前提下最少需要 $1$ 个 "原数" 。 **#样例2** : $2$ 能消灭 $\{6,19,7\}$ , $5$ 能消灭 $\{5,15\}$ , $10$ 能消灭 $\{10\}$ , 所以区间 $[1,6],[1,4]$ 最少能用所有 "原数" 全部消灭,区间 $[4,6]$ 能用 $2,5$ 全部消灭。 #### 数据范围: 对于 $30\%$ 的数据:$n\le100,m\leq10, d=2, q\le 10$ 对于 $100\%$ 的数据:$n\le5\times 10^{5},m\leq60,2\leq d\leq10,q\le10^{6},0\le a_i,b_i\le 2^{63}$ ![](https://cdn.luogu.com.cn/upload/image_hosting/t7pn0p1n.png) 特殊性质:数据经过构造。 #include <bits/stdc++.h> #define ll long long using namespace std; const ll N=1e6; struct node{ ll l,r,sum; }tr[4*N+5]; struct xd{ ll l,r,f; }b[N+5]; ll n,m,d,q; ll a[N+5],ans[N+5]; ll pre[N+5],lst[N+5]; ll ys[N+5],dy[N+5]; map<ll,bool> mp; void updata(ll p){ tr[p].l=tr[2*p].l; tr[p].r=tr[2*p+1].r; tr[p].sum=tr[2*p].sum+tr[2*p+1].sum; return; } void build(ll p,ll l,ll r){ if(l==r){ tr[p].l=l; tr[p].r=r; return; } ll mid=(l+r)>>1; build(2*p,l,mid); build(2*p+1,mid+1,r); updata(p); return; } void change(ll p,ll x,ll y){ if(tr[p].l==x && tr[p].r==x){ tr[p].sum+=y; return; } if(x<=tr[2*p].r) change(2*p,x,y); else change(2*p+1,x,y); updata(p); return; } ll searchh(ll p,ll l,ll r){ if(r<l) return 0; ll s=0; if(tr[p].l==l && tr[p].r==r) return tr[p].sum; if(tr[2*p].r>=r) s+=searchh(2*p,l,r); else if(tr[2*p+1].l<=l) s+=searchh(2*p+1,l,r); else{ s+=searchh(2*p,l,tr[2*p].r); s+=searchh(2*p+1,tr[2*p+1].l,r); } return s; } bool cmp(xd l1,xd l2){ return l1.r<l2.r; } int main(){ memset(dy,-1,sizeof(dy)); scanf("%lld%lld%lld%lld",&n,&m,&d,&q); for(ll i=1;i<=n;i++) scanf("%lld",&a[i]); for(ll i=1;i<=m;i++) scanf("%lld",&ys[i]),mp[ys[i]]=1; for(ll i=1;i<=n;i++){ ll num=a[i],w=-1; if(mp[num]) w=num; while(num){ num/=d; if(mp[num]) w=num; } dy[i]=w; } // for(ll i=1;i<=n;i++) cout<<dy[i]<<" "; // return 0; for(ll i=1;i<=q;i++){ scanf("%lld%lld",&b[i].l,&b[i].r); b[i].f=i; // cout<<i<<endl; } for(ll i=1;i<=n;i++){ if(dy[i]==-1) continue; pre[i]=lst[dy[i]]; lst[dy[i]]=i; } build(1,1,n); sort(b+1,b+1+q,cmp); ll nxt=1; for(ll i=1;i<=q;i++){ while(nxt<=b[i].r){ if(dy[nxt]==-1){ nxt++; continue; } if(pre[nxt]) change(1,pre[nxt],-1); change(1,nxt,1); nxt++; } ans[b[i].f]=searchh(1,1,b[i].r)-searchh(1,1,b[i].l-1); } for(ll i=1;i<=q;i++) printf("%lld\n",ans[i]); return 0; } 时间限制3s,评测结果RE+TLE,找错误,并在不改变代码原本写法的情况下修正
08-01
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值