稀疏表 (Sparse Table)
概念
將原序列從arr第i個開始長度為2j的連續子序列的答案用陣列儲存起來。
那要怎麼存呢?這裡會用到一點DP的想法,令dpi,j為原本定義的答案
初始狀態dp0=arr
dpi,j=cmp(dpi−1,j,dpi−1,j+2i−1)
cmp為合併兩個答案的函式,如果是區間最大值的話就用max,如果是最小值就用min
用法
預處理
用剛剛推出來的轉移式去做就好了
1 2 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等查詢。
1 2 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
1 2 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
1 2 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
1 2 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; }
|