632. 最小區間(很有收穫) - 8月1日
題目
632. 最小區間
我的思路
這道題思路其實很簡單,但要學會合理選擇資料結構加快降低時間複雜度。起初我的的解決方案在最後幾個比較龐大的測試用例下發生了超時的問題。
兩種思路(官方題解):
思路一:
給定 k 個列表,需要找到最小區間,使得每個列表都至少有一個數在該區間中。該問題可以轉化為,從 k個列表中各取一個數,使得這 k 個數中的最大值與最小值的差最小。
由於k個列表都是升序排列的,因此對每個列表維護一個指標。每次選擇這些指標中對應元素val最小的向右滑動,更新該指標,並重新計算當前的區間大小。如果區間大小比記錄的最小的情況還小,那麼記錄這個最新的。直到某個指標滑出所在列表(表示該列表最大元素作為區間下界的情況已經考慮完畢,不會有下界更大且滿足條件的區間)。
考慮複雜度,最壞情況下會有k(列表數)*n(列表平均長度)次滑動,也就是會有k*n次刪除,插入,排序這樣的操作。也就是那麼如何維護這k個指標呢會有較大的影響。因為涉及頻繁的刪除,插入,並且經常操作最大最小的元素,那麼顯然堆(注意堆和平衡二叉樹,二叉排序樹的區別優劣)最合適。而堆在C++中可以用優先佇列實現優先順序佇列priority_queue(過載運算子,優先順序比較)。
那麼時間複雜度就是k*n*logk,空間複雜度是k
class Solution { public: vector<int> smallestRange(vector<vector<int>>& nums) { int rangeLeft = 0, rangeRight = INT_MAX; int size = nums.size(); vector<int> next(size); auto cmp = [&](const int& u, const int& v) { return nums[u][next[u]] > nums[v][next[v]]; }; priority_queue<int, vector<int>, decltype(cmp)> pq(cmp); int minValue = 0, maxValue = INT_MIN; for (int i = 0; i < size; ++i) { pq.emplace(i); maxValue = max(maxValue, nums[i][0]); } while (true) { int row = pq.top(); pq.pop(); minValue = nums[row][next[row]]; if (maxValue - minValue < rangeRight - rangeLeft) { rangeLeft = minValue; rangeRight = maxValue; } if (next[row] == nums[row].size() - 1) { break; } ++next[row]; maxValue = max(maxValue, nums[row][next[row]]); pq.emplace(row); } return {rangeLeft, rangeRight}; } }; 作者:LeetCode-Solution 連結:https://leetcode-cn.com/problems/smallest-range-covering-elements-from-k-lists/solution/zui-xiao-qu-jian-by-leetcode-solution/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
思路二:滑動視窗+雜湊表
和我的思路有點類似不過資料結構不太一樣。
總體思路都是:把k個列表中出現過的元素從小到大全部排列,同時要記住各個元素在原來的k個列表中哪幾個列表中出現過。然後滑動視窗,保證視窗中的元素在所有k個列表中都存在。方法是:維護各個列表中在當前視窗中元素的個數。
那麼在我最初的實現中:時間複雜度是k*n*k+2*k*n也就是k^2*n,並不理想。我當時排列所有元素時,每一輪都是是遍歷k個列表中的最小元素,得最小的插入一個佇列vector。我的改進是用set(也就是二叉平衡樹、紅黑樹)來存這個包含所有元素的佇列,那麼每次插入的複雜度就是logk。同時set(二叉平衡樹實現)容器可以使用迭代器(priority_queue不行,我猜因為底層結構是堆,不方便排序,只是方便最大最小元素的增刪查)。
所以改進後時間複雜度是k*n*logk,空間複雜度是k*n。程式碼見(我的實現)
我覺得官方的思路更好一點,使用雜湊表。
把k個列表中所有出現過的元素作為雜湊表的鍵,對映到一個列表,儲存該元素所在的列表號。這樣滑動視窗的指標是指向雜湊表的鍵,(可以通過count()判斷是否存在該鍵)。同樣在滑動的同時,維護各個列表在當前視窗中元素的個數。
這樣一來複雜度:
class Solution { public: vector<int> smallestRange(vector<vector<int>>& nums) { int n = nums.size(); unordered_map<int, vector<int>> indices; int xMin = INT_MAX, xMax = INT_MIN; for (int i = 0; i < n; ++i) { for (const int& x: nums[i]) { indices[x].push_back(i); xMin = min(xMin, x); xMax = max(xMax, x); } } vector<int> freq(n); int inside = 0; int left = xMin, right = xMin - 1; int bestLeft = xMin, bestRight = xMax; while (right < xMax) { ++right; if (indices.count(right)) { for (const int& x: indices[right]) { ++freq[x]; if (freq[x] == 1) { ++inside; } } while (inside == n) { if (right - left < bestRight - bestLeft) { bestLeft = left; bestRight = right; } if (indices.count(left)) { for (const int& x: indices[left]) { --freq[x]; if (freq[x] == 0) { --inside; } } } ++left; } } } return {bestLeft, bestRight}; } }; 作者:LeetCode-Solution 連結:https://leetcode-cn.com/problems/smallest-range-covering-elements-from-k-lists/solution/zui-xiao-qu-jian-by-leetcode-solution/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
我的實現
class Solution { public: vector<int> smallestRange(vector<vector<int>>& nums) { struct pairComp { bool operator() (const pair<int,int>& lhs, const pair<int,int>& rhs) const{ if( lhs.first < rhs.first)return true; else if(lhs.first==rhs.first &&lhs.second<rhs.second)return true; else return false; //return lhs.first <= rhs.first && lhs.second <rhs.second; } }; //從小到大遍歷一遍這個k個升序的整數陣列,把它們的元素按大小排在一個向量vector<pair<int(元素值),int(元素所在的陣列號))>> allelements中。 vector<int> result(2); set<pair<int,int>,pairComp> allelements; bool empty_check; int min_val; int no_nums; //cout<<"check!"<<endl; empty_check = true; min_val = INT_MAX; for(auto it_nums = nums.begin(); it_nums!=nums.end();it_nums++){ int dis = distance(nums.begin(),it_nums); for(auto it_val:*it_nums){ allelements.insert(make_pair(it_val,dis)); } } /*while(1){ for(auto it_nums = nums.begin(); it_nums!=nums.end();it_nums++){ if(!(*it_nums).empty()){//該陣列還剩餘有元素 if(*(*it_nums).begin()<=min_val){ min_val = *(*it_nums).begin(); no_nums = distance(nums.begin(),it_nums); empty_check = false; } } } if(empty_check==false){ allelements.insert(make_pair(min_val,no_nums)); nums[no_nums].erase(nums[no_nums].begin()); //cout<<min_val<<"\t"<<no_nums<<endl; } else break; }*/ //for(auto it:allelements) //cout << it.first<<"\t"<<it.second<<endl; //用兩個指標指向allelements的元素,並讓這兩個指標指向區間的邊界: auto it_lower = allelements.begin(); auto it_upper = allelements.begin(); int nums_size = nums.size(); int check_allin = 0; int min_gap = -1; vector<int> check_eachin(nums_size,0); //把後邊界指標向後滑動,滑動的過程中,用k個變數來標記當前區間各個陣列的元素在區間中出線的次數 while(check_allin<nums_size){ //判斷是否越界,其實不必要??? //cout<<(*it_upper).second<<endl; if(check_eachin[(*it_upper).second]==0){ ++check_allin; } check_eachin[(*it_upper).second]++; it_upper++; } it_upper--; //把首址針向後滑動,直到某個標記等於0之前 while(1){ while(check_eachin[(*it_lower).second]>1){ check_eachin[(*it_lower).second]--; it_lower++; } if(min_gap==-1||min_gap>(*it_upper).first-(*it_lower).first){ min_gap=(*it_upper).first-(*it_lower).first; result[0]=(*it_lower).first; result[1]=(*it_upper).first; } if((++it_upper)!=allelements.end()){ //it_upper++; check_eachin[(*it_upper).second]++; }else{ break; } } return result; } }; /* 一個比較樸素的思路: 從小到大遍歷一遍這個k個升序的整數陣列,把它們的元素按大小排在一個向量vector<pair<int(元素值),int(元素所在的陣列號))>> allelements中。 用兩個指標指向allelements的元素,並讓這兩個指標指向區間的邊界: 首先都指向首元素跳過 把後邊界指標向後滑動,滑動的過程中,用k個變數來標記當前區間各個陣列的元素在區間中出線的次數 直到滑動到所有k個標記都大於0 把首址針向後滑動,直到某個標記等於0之前 (注意滑動時若遇到連續多個想同val的元素,那麼直接滑到該連續序列的最後一個) 提審錯了,需要找到最短的,所以方法是交替滑動上下邊界的指標,保留距離最短的一組 */
拓展學習
在初始化set等容器時,過載小於運算子,實現更復雜物件的排序。