之前學過 莫隊 算法,其運用了分塊思想;但是我居然是第一次寫純種的分塊題目。
題意
給你一個長度為 n n n 的序列 a a a(一開始 ? a i ∈ [ 1 , 1000 ] \forall a_i\in[1,1000] ?ai?∈[1,1000])。要求執行 q q q 次操作,操作有兩種,每次形如 o p , l , r , w / c op,l,r,w/c op,l,r,w/c:
- o p op op 為 M \rm M M,將 a l , a l + 1 , ? , a r a_l,a_{l+1},\cdots,a_r al?,al+1?,?,ar? 分別加上 w w w;
- o p op op 為 A \rm A A,查詢 a l , a l + 1 , ? , a r a_l,a_{l+1},\cdots,a_r al?,al+1?,?,ar? 中,有多少個數大于 c c c。
n ≤ 1 0 6 , q ≤ 3000 , w ≤ 1000 , c ≤ 1 0 9 n\le10^6,q\le3000,w\le1000,c\le10^9 n≤106,q≤3000,w≤1000,c≤109
思路
主席樹?是我早就不會打的。
如果我們把它變成一道分塊練習題呢?
考慮對序列 a a a 分塊,對于每一塊內,使用輔助數組 b b b 以保證塊內數有序。不妨設 b l t , b r t bl_t,br_t blt?,brt? 表示塊 t t t 的左右端點, b e l i bel_i beli? 表示下標 i i i 所在的塊的編號。
對于修改操作 l , r , w l,r,w l,r,w,如果 ? t , b l t ≤ l < r ≤ b r t \exist t,bl_t\le l<r\le br_t ?t,blt?≤l<r≤brt?,即同一塊, t = b e l l = b e l r t=bel_l=bel_r t=bell?=belr?,如果其不在左右端點上,那么塊內的排序性質就會被破壞;反之如果它們不在同一塊,說明它們中間跨過了若干塊整塊的區間,我們發現被跨過的區間仍然保持有序。
那么我們得到一個初步的修改方法:
- 設左右端點所在的塊分別在 l b = b e l l , r b = b e l r lb=bel_l,rb=bel_r lb=bell?,rb=belr?,如果 l b = r b lb=rb lb=rb,就塊內暴力加 w w w 并快排更新;
- 否則即 l b < r b lb<rb lb<rb,我們發現塊 l b + 1 ~ r b ? 1 lb+1\sim rb-1 lb+1~rb?1 內仍然有序,那么不妨想線段樹引入懶惰標記一樣,我們搞一個加法標記,把整一塊 t t t 內所有元素同時加的數,用 t a g t tag_t tagt? 記錄下來,等到查詢時再處理;只強制更新更新 l ~ b r l b l\sim br_{lb} l~brlb? 和 b l r b ~ r bl_{rb}\sim r blrb?~r 的數據和塊 l b , r b lb,rb lb,rb。
這樣子大大減少了排序的次數,每次修改操作頂多 2 × log ? 2 n 2\times \log_2n 2×log2?n,瓶頸在于快排。
void modify(ll t)//更新t塊內數據
{for(int i=bl[t];i<=br[t];i++)b[i]=a[i];sort(b+bl[t],b+br[t]+1);
}
void add(ll ql,ll qr,ll x)//l,r,w
{ll lb=bel[ql],rb=bel[qr];//左右端所在塊if(lb==rb)//同一塊{for(int i=ql;i<=qr;i++)a[i]+=x;modify(lb);return;}//不同塊for(int i=ql;i<=br[lb];i++)a[i]+=x;for(int i=bl[rb];i<=qr;i++)a[i]+=x;modify(lb);modify(rb);for(int i=lb+1;i<rb;i++)//lb+1~rb-1塊打標記tag[i]+=x;
}
接下來就是查詢,其實就和修改所運用的思想差不多了。同樣討論 l , r l,r l,r 在或不在同一塊的兩種情況。如果同一塊就直接 q l ~ q r ql\sim qr ql~qr 掃過去(倒著枚舉提前 break 凹時間也行、甚至乎二分,反正塊上數組 b b b 就是有序的),不同塊就搜左右兩邊 l ~ b r r b l\sim br_{rb} l~brrb? 和 b l r b ~ r bl_{rb}\sim r blrb?~r(這里不能二分,因為原數組并非有序),至于中間的整塊整塊的,按塊二分就好。
ll find(ll ql,ll qr,ll x)
{ll l=ql,r=qr;while(l<=r){ll mid=(l+r)>>1;if(b[mid]<x)l=mid+1;else r=mid-1;}return qr-l+1;
}
ll query(ll ql,ll qr,ll x)
{ll ret=0,lb=bel[ql],rb=bel[qr];if(lb==rb){ret+=find(ql,qr,x-tag[lb]);return ret;}for(int i=ql;i<=br[lb];i++)if(a[i]+tag[lb]>=x)ret++;for(int i=bl[rb];i<=qr;i++)if(a[i]+tag[rb]>=x)ret++;for(int i=lb+1;i<rb;i++)ret+=find(bl[i],br[i],x-tag[i]);return ret;
}
塊長 n \sqrt{n} n?,修改一次是塊長+塊內排序,查詢是 塊內二分 或 塊長掃左右端+塊數*塊內二分,時間復雜度 Θ ( m n + m n log ? 2 n ) = Θ ( m n log ? 2 n ) \Theta(m\sqrt{n}+m\sqrt{n}\log_2\sqrt{n})=\Theta(m\sqrt{n}\log_2\sqrt{n}) Θ(mn?+mn?log2?n?)=Θ(mn?log2?n?)。
代碼
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e6+9;
ll n,q,a[N];
ll tag[N];//加法標記
ll bSize,cnt_b,bel[N],bl[N],br[N],b[N];
void init()
{bSize=sqrt(n);cnt_b=n/bSize;if(n%bSize)cnt_b++;for(int i=1;i<=n;i++){bel[i]=(i-1)/bSize+1;b[i]=a[i];}for(int i=1;i<=cnt_b;i++){bl[i]=(i-1)*bSize+1;br[i]=i*bSize;}br[cnt_b]=n;for(int i=1;i<=cnt_b;i++)sort(b+bl[i],b+br[i]+1);
}
void modify(ll t)//更新t塊內數據
{for(int i=bl[t];i<=br[t];i++)b[i]=a[i];sort(b+bl[t],b+br[t]+1);
}
void add(ll ql,ll qr,ll x)
{ll lb=bel[ql],rb=bel[qr];//左右端所在塊if(lb==rb)//同一塊{for(int i=ql;i<=qr;i++)a[i]+=x;modify(lb);return;}//不同塊for(int i=ql;i<=br[lb];i++)a[i]+=x;for(int i=bl[rb];i<=qr;i++)a[i]+=x;modify(lb);modify(rb);for(int i=lb+1;i<rb;i++)//lb+1~rb-1塊打標記tag[i]+=x;
}
ll find(ll ql,ll qr,ll x)
{ll l=ql,r=qr;while(l<=r){ll mid=(l+r)>>1;if(b[mid]<x)l=mid+1;else r=mid-1;}return qr-l+1;
}
ll query(ll ql,ll qr,ll x)
{ll ret=0,lb=bel[ql],rb=bel[qr];if(lb==rb){ret+=find(ql,qr,x-tag[lb]);return ret;}for(int i=ql;i<=br[lb];i++)if(a[i]+tag[lb]>=x)ret++;for(int i=bl[rb];i<=qr;i++)if(a[i]+tag[rb]>=x)ret++;for(int i=lb+1;i<rb;i++)ret+=find(bl[i],br[i],x-tag[i]);return ret;
}
int main()
{scanf("%lld%lld",&n,&q);for(int i=1;i<=n;i++)scanf("%lld",&a[i]);init();while(q--){char op;ll l,r,x;cin>>op;scanf("%lld%lld%lld",&l,&r,&x);if(op=='M')add(l,r,x);else printf("%lld\n",query(l,r,x));}return 0;
}