稀疏表 (Sparse Table)
概念
將原序列從arr第i個開始長度為2j的連續子序列的答案用陣列儲存起來。
那要怎麼存呢?這裡會用到一點DP的想法,令dpi,j為原本定義的答案
初始狀態dp0=arr
dpi,j=cmp(dpi−1,j,dpi−1,j+2i−1)
cmp為合併兩個答案的函式,如果是區間最大值的話就用max,如果是最小值就用min
用法
預處理
用剛剛推出來的轉移式去做就好了
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | vector<vector<int>>mat(30);void build(const vector<int>&v){
 int n = v.size();
 mat[0] = a;
 for(int i = 1;(1<<i)<=n;++i) {
 mat[i].resize(n-(1<<i)+1);
 for(int j = 0;j<=n-(1<<i);++j) {
 mat[i][j] = cmp(mat[i-1][j],mat[i-1][j+(1<<(i-1))]);
 }
 }
 }
 
 | 
時間複雜度為O(nlogn)
區間查詢
查詢[l,r]區間的答案
let k=log2(r−l+1)
ans=cmp(dpk,l,dpk,r−(2k)+1)
cmp一樣視題目去做調整,要記得,詢問的答案必須滿足可重複貢獻性質,因為我們的dp紀錄的答案區間會有重疊的情況發生,所以如果重複貢獻會影響答案的話(如區間和、區間xor等)就不能用這樣的方式去進行詢問,通常會拿來進行區間max、區間min、區間gcd等查詢。
| 12
 3
 4
 
 | int query(int ql, int qr) {int k = __lg(qr - ql + 1);
 return cmp(st[k][ql], st[k][qr - (1<<k) + 1]);
 }
 
 | 
時間複雜度為O(cmp),如果是max或是min的話就是O(1),如果是gcd的話就是O(logA),A為值域。
要記得,取log的方式盡量避免用內建的log2()函式,避免TLE,內建的__lg()是使用位元運算實作的,所以速度很快,或者你也可以使用建表的方式。
練習
Codeforces 475D
https://codeforces.com/contest/475/problem/D
題目敘述
給你一個長度為N的序列,然後有Q筆詢問,每次詢問你這個序列當中一共有多少連續子序列的最大公因數為x。
1≤N≤105
1≤Q≤3×105
1≤x≤109
題解
我們可以先預處理詢問的答案,用一個hash_map存,key對應到x,value對應到答案。
那計算答案的部分該怎麼做呢,我們可以用二分搜,先枚舉左界l,尋找一個區間[L,R],使得gcd(al,...,aL)到gcd(al,...,aR)都等於x,為甚麼可以使用二分搜了,因為固定左界的時候,我們就發現從左界開始的最小公因數有單調遞減的特性,一共有logA個公因數,A是值域,然後二分搜花了logNlogA的時間,所以總時間複雜度為O(NlogNlog2A)
AC Code
| 12
 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
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 
 | template<class T,T (*op)(T,T)>struct sparse_table{int n;
 vector<vector<T>>mat;
 sparse_table(): n(0){}
 sparse_table(const vector<T>&v){
 n = (int)(v.size());
 mat.resize(30);
 mat[0] = v;
 for(int i = 1;(1<<i)<=n;++i){
 mat[i].resize(n-(1<<i)+1);
 for(int j = 0;j<n-(1<<i)+1;++j){
 mat[i][j] = op(mat[i-1][j],mat[i-1][j+(1<<(i-1))]);
 }
 }
 }
 T query(int ql,int qr){
 int k = __lg(qr-ql+1);
 return op(mat[k][ql],mat[k][qr-(1<<k)+1]);
 }
 };
 struct custom_hash {
 static uint64_t splitmix64(uint64_t x) {
 x += 0x9e3779b97f4a7c15;
 x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
 x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
 return x ^ (x >> 31);
 }
 size_t operator()(uint64_t x) const {
 static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
 return splitmix64(x + FIXED_RANDOM);
 }
 };
 template<class T,class U>using hash_map = gp_hash_table<T,U,custom_hash>;
 void solve(){
 hash_map<int,int>mp;
 int n;
 cin>>n;
 VI v(n+5);
 for(int i = 1;i<=n;++i){
 cin>>v[i];
 }
 sparse_table<int,__gcd>st(v);
 for(int i = 1;i<=n;++i){
 function<void(int,int)>f = [&](int val,int s){
 if(s>n)return;
 int l = s,r = n,ans = st.query(i,s);
 while(l<=r){
 int m = (l+r)>>1;
 if(st.query(i,m)>=val){
 l = m+1;
 ans = m;
 }
 else{
 r = m-1;
 }
 }
 mp[val]+=(ans-s+1);
 f(st.query(i,ans+1),ans+1);
 };
 f(v[i],i);
 }
 int m;cin>>m;
 while(m--){
 int x;
 cin>>x;
 cout<<mp[x]<<endl;
 }
 }
 
 | 
:::
Codeforces 307093G
https://codeforces.com/edu/course/2/lesson/9/2/practice/contest/307093/problem/G
題目敘述
給你一個長度為N的序列,求出連續子序列gcd為1的最小長度
1≤N≤105
題解
跟上面那題類似,固定左界二分搜找區間gcd為1的右界
時間複雜度為O(NlogNlogA)
AC Code
| 12
 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
 
 | template<class T,T (*op)(T,T)>struct sparse_table{int n;
 vector<vector<T>>mat;
 sparse_table(): n(0){}
 sparse_table(const vector<T>&v){
 n = (int)(v.size());
 mat.resize(30);
 mat[0] = v;
 for(int i = 1;(1<<i)<=n;++i){
 mat[i].resize(n-(1<<i)+1);
 for(int j = 0;j<n-(1<<i)+1;++j){
 mat[i][j] = op(mat[i-1][j],mat[i-1][j+(1<<(i-1))]);
 }
 }
 }
 T query(int ql,int qr){
 int k = __lg(qr-ql+1);
 return op(mat[k][ql],mat[k][qr-(1<<k)+1]);
 }
 };
 void solve(){
 int n,mn = inf;
 cin>>n;
 VI v(n+5);
 for(int i = 1;i<=n;++i){
 cin>>v[i];
 }
 sparse_table<int,__gcd>st(v);
 for(int i = 1;i<=n;++i){
 int l = i,r = n,ans = i;
 while(l<=r){
 int m = (l+r)>>1;
 if(st.query(i,m)>1){
 l = m+1;
 }
 else{
 ans = m;
 r = m-1;
 }
 }
 if(st.query(i,ans)==1)mn = min(mn,ans-i+1);
 }
 cout<<(mn>1000000?-1:mn)<<endl;
 }
 
 | 
Codeforces 514D
https://codeforces.com/problemset/problem/514/D
題目敘述
有M個長度為N的序列,可以對這N個序列選一個連續區間進行K次操作,每次操作就是把其中一個序列的連續區間減1,請你求出連續區間最大時對於每個序列分別要進行幾次操作。
1≤N≤105
1≤M≤5
1≤K≤109
題解
枚舉連續區間的左界,檢查每個序列的連續區間最大值總和≤K的最大右界是多少,右界的部分可以用二分搜的方式去找,時間複雜度為O(NMlogN)
AC Code
| 12
 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
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 
 | template<class T,T (*op)(T,T)>struct sparse_table{int n;
 vector<vector<T>>mat;
 sparse_table(): n(0){}
 sparse_table(const vector<T>&v){
 n = (int)(v.size());
 mat.resize(30);
 mat[0] = v;
 for(int i = 1;(1<<i)<=n;++i){
 mat[i].resize(n-(1<<i)+1);
 for(int j = 0;j<n-(1<<i)+1;++j){
 mat[i][j] = op(mat[i-1][j],mat[i-1][j+(1<<(i-1))]);
 }
 }
 }
 T query(int ql,int qr){
 int k = __lg(qr-ql+1);
 return op(mat[k][ql],mat[k][qr-(1<<k)+1]);
 }
 };
 int Max(int a,int b){return max(a,b);}
 void solve(){
 int n,m,k;
 cin>>n>>m>>k;
 vector<vector<int>>v(m,vector<int>(n));
 vector<int>tmp(m);
 sparse_table<int,Max>st[m];
 for(int i = 0;i<n;++i){
 for(int j = 0;j<m;++j){
 cin>>v[j][i];
 }
 }
 for(int i = 0;i<m;++i){
 st[i] = sparse_table<int,Max>(v[i]);
 }
 int mx = 0;
 for(int i = 0;i<n;++i){
 int l = i,r = n-1,ans = -1;
 while(l<=r){
 int mid = (l+r)>>1;
 int sum = 0;
 for(int j = 0;j<m;++j){
 sum+=st[j].query(i,mid);
 }
 if(sum<=k){
 ans = mid;
 l = mid+1;
 }
 else{
 r = mid-1;
 }
 }
 if((ans-i+1)>mx and ans!=-1){
 mx = (ans-i+1);
 for(int j = 0;j<m;++j){
 tmp[j] = st[j].query(i,ans);
 }
 }
 }
 for(int i = 0;i<m;++i){
 cout<<tmp[i]<<' ';
 }
 cout<<endl;
 }
 
 |