這類 \(O(\log V)\)
每次下降或上升都跨越一個“數量級”,因此變化次數有限,即使包在循環中整體也只會是 \(O(n \log V)\) 而非 \(O(n^2)\)。
下面舉幾個比較典型的例題
GCD問題
gcd為1的最小區間
https://leetcode.cn/problems/minimum-number-of-operations-to-make-all-array-elements-equal-to-1
主要思路:
利用一個 vector<pair<int,int>> p 來維護狀態,對於每個新加入的數值,遍歷當前 p 中的所有元素,計算它們與 nums[i]的最大公約數,得到所有以 i 結尾的子數組的 GCD
如果新得到的 GCD 與上一個相同,則説明這兩個區間可以合併,只需要更新右端點即可;如果不同(變小),則説明出現了一個新的 GCD 段,將其追加到 p 的末尾。
通過這種方式,p 中的每個元素 {g, r} 實際上表示:所有以當前位置結尾、GCD 值為 g 的子數組,其起點區間的右端為 r;當出現 g = 1 時,我們即可通過 r 快速確定包含 GCD 為 1 的最短子數組長度。
表面上看,每次遍歷都要處理整個 p,似乎複雜度是 \(O(n^2)\),但實際上 p 的長度非常有限。因為隨着遍歷的進行,子數組的 GCD 值只會不增。當它下降時,必然降到某個約數,因此每次下降至少減半。由此可知,對於任意位置,GCD 值最多下降 \(\log_2 V\) 次(其中 V 為數組中的最大元素),即 p 的長度在任意時刻都不會超過 \(O(\log V)\)。因此整個算法的時間複雜度是 \(O(n \log V)\),空間複雜度也是 \(O(\log V)\)。
給出下面代碼:
int len=n;
vector<pair<int,int>>q;
for(int i=0;i<n;i++){
q.push_back({nums[i],i});
int j=0;
for(auto& item:q){// 遍歷已有數據
item.first=gcd(item.first,nums[i]);
if(j>0 && q[j-1].first==item.first)q[j-1].second=item.second;// 相同 GCD 更新右端點
else q[j++]=item; // 不同 GCD 添加
}
if(q[0].first==1)len=min(len,i-q[0].second+1);
q.resize(j);
// for(auto& item:q)cerr<<item.first<<' '<<item.second<<nl;
// cerr<<len<<nl;
}
最大公因數等於 k 的子數組數目
https://leetcode.cn/problems/number-of-subarrays-with-gcd-equal-to-k
思路與上面一致,還是每次相同就記錄可選的最右端點,不過添加了一個左端點來記錄長度
答案統計可以這麼想,只要添加 nums[i] 符合條件,原來長度為 len,那麼新增的符合條件的子數組就是 len 個 ,即每個數與nums[i]組合一下。
int subarrayGCD(vector<int>& nums, int k) {
int n=nums.size();
vector<array<int,3>>q;
int res=0;
for(int i=0;i<n;i++){
int j=0;
q.push_back({nums[i],i,i});
for(auto&[w,l,r]:q){
w = gcd(w,nums[i]);
if(j>0 && w==q[j-1][0])q[j-1][2]=r;
else{
q[j++]={w,l,r};
}
}
q.resize(j);
// cerr<<"i="<<i<<":"<<nl<<" ";
for(auto&[w,l,r]:q){
// cerr<<w<<" "<<l<<" "<<r<<nl;
if(w==k)res+=r-l+1;
}
// cerr<<nl;
}
return res;
}
LCM問題
最小公倍數等於 k 的子數組數目
https://leetcode.cn/problems/number-of-subarrays-with-lcm-equal-to-k
主要思路:和上面GCD問題一模一樣,只是把 GCD 變為了 LCM
int subarrayLCM(vector<int>& nums, int k) {
int n=nums.size();
vector<array<int,3>>q;
int res=0;
for(int i=0;i<n;i++){
int j=0;
q.push_back({nums[i],i,i});
for(auto&[w,l,r]:q){
w = lcm(w,nums[i]);
if(w>k)continue;
if(j>0 && w==q[j-1][0])q[j-1][2]=r;
else{
q[j++]={w,l,r};
}
}
q.resize(j);
// cerr<<"i="<<i<<":"<<nl<<" ";
for(auto&[w,l,r]:q){
// cerr<<w<<" "<<l<<" "<<r<<nl;
if(w==k)res+=r-l+1;
}
// cerr<<nl;
}
return res;
}
AND問題
子數組and等於k的數量
https://leetcode.cn/problems/number-of-subarrays-with-and-value-of-k
long long countSubarrays(vector<int>& nums, int k) {
int n=nums.size();
vector<array<int,3>>q;
long long res=0;
for(int i=0;i<n;i++){
q.push_back({nums[i],i,i});
int j=0;
for(auto &[w,l,r]:q){
w&=nums[i];
if(j>0 && q[j-1][0]==w)q[j-1][2]=r;
else q[j++]={w,l,r};
}
q.resize(j);
for(auto &[w,l,r]:q){
if(w==k)res+=r-l+1;
}
}
return res;
}
OR問題
子區間or值與給定值k的最小絕對值
https://leetcode.cn/problems/find-subarray-with-bitwise-or-closest-to-k/description/
給定一個數組 nums 和 一個正整數 k
一個子數組 nums[l..r] 滿足 |k - (nums[l] OR nums[l + 1] ... OR nums[r])| 最小。
int minimumDifference(vector<int>& nums, int k) {
int n=nums.size();
vector<int>buk;
int res=2e9;
for(int i=0;i<n;i++){
buk.push_back(nums[i]);
int j=0;
for(auto x:buk){
int mid=x|nums[i];
if(!j || mid!=buk[j-1])buk[j++]=mid;
}
buk.resize(j);
for(auto x:buk){
res=min(res,abs(x-k));
}
}
return res;
}
所有子區間or值的種類數
https://leetcode.cn/problems/bitwise-ors-of-subarrays
int subarrayBitwiseORs(vector<int>& nums) {
int n=nums.size();
vector<int>q;
set<int>res;
for(int i=0;i<n;i++){
int j=0;
q.push_back(nums[i]);
for(auto& w:q){
w=w|nums[i];
if(!j || w!=q[j-1])q[j++]=w;
}
q.resize(j);
res.insert(q.begin(),q.end());
}
return res.size();
}
待續 其他相關的,待我先做完 ...