Codility上的練習(12),codility上練習12
(1) MinMaxDivision
給定一個非負整數數組,每個整數都是[0..M]之間的,你要把它分成K段,(切K - 1刀),段可以為空白,每個元素必須屬於一段,每段必須包含0個或者多個連續的元素,要求分好和最大段的和盡量小,返回這個儘可能小的最大和。
資料範圍 :N, K [1..10^5], M [0..10^4]
要求複雜度 時間 O(N * log(N + M)) 空間 O(1)。
分析:典型的二分我們可以。二分一個最大段的和,然後我們一段一段地加,超過要加的值,就開始一段新的。這個方法得力雩都是非負整數……
簡單說一下複雜度,二分問題的架構就是 二分 + 判斷。 判斷部分顯然是O(N)的。 二分的複雜度取決於二分的區間大小。我們的二分區間左端點可以認為是min(A[i]),也可以認為是0,反正區間大點也關係,右端點最大是N * M,那麼二分的複雜度是O(log(N * M)) = O(logN + logM) = O(2 * log(max(M, N)) = O(log(max(M, N)) = O(log (M + N)) 所以算上檢測的複雜度就達到要求的那個了。
// you can use includes, for example:// #include <algorithm>// you can write to stdout for debugging purposes, e.g.// cout << "this is a debug message" << endl;bool can(vector<int> &a,int x, int k) {int sum = 0; --k; for (int i = 0; i < a.size();) { if ((sum += a[i]) > x) { if (--k < 0) { return false; } sum = 0; } else { ++i; } } return true;}int solution(int K, int M, vector<int> &A) { // write your code in C++98 int left = 0,right = -1; for (int i = 0; i < A.size(); ++i) { right += A[i]; left = max(left, A[i]); } while (left <= right) { int mid = (left + right) >> 1; if (can(A, mid, K)) { right = mid - 1; } else { left = mid + 1; } } return right + 1; }
(2) NailingPlanks
N塊木板,可以看作N線段,給定兩個長度為N的正整數數組A[],B[],[A[k],B[k]]表示木板(線段)的起點和終點,A[k] <= B[k]。有M個釘子,它們分別在長度為M的正整數數組裡。釘子I可以固定住木板K,當且今當A[K]<=C[I]<=B[K]。問按順序使用釘子,至少使用前多少個釘子可以固定住所有木板?無解返回-1。
資料範圍: 木板數N和M的範圍[1..30000], A B C數組元素範圍為[1..2 * M]
要求複雜度: 時間O((N+M)*log(M)) , 空間O(M)
分析: 一個顯然的並且符合要求的演算法是二分答案,問題是如何判斷,顯然我們不能迴圈木板和釘子。但是我們可以計算從開頭到當前位置一共有多少個釘子,這是首碼和的思想。計算首碼和需要O(M),判斷需要O(N),二分是O(logM),所以整好是要求的時間複雜度。空間上需要存首碼和O(M)。
代碼:
// you can also use includes, for example:// #include <algorithm>int solution(vector<int> &A, vector<int> &B, vector<int> &C) { // write your code in C++98 int m = C.size(); int M = (m << 1) | 1; int left = 0, right = m, result = -1; while (left <= right) { int mid = (left + right) >> 1; vector<int> v; v.resize(M, 0); for (int i = 0; i < mid; ++i) { ++v[C[i]]; } for (int i = 1; i < M; ++i) { v[i] += v[i - 1]; } bool can = true; for (int i = 0; i < A.size(); ++i) { if (v[B[i]] - v[A[i] - 1] == 0) { can = false; break; } } if (can) { result = mid; right = mid - 1; } else { left = mid + 1; } } return result;}
更快的演算法,如果我們建立一個長度為2 * M的數組,每個位置表示該位置上釘子的最小編號(可能同一個位置有多個釘子,取編號最小的),沒有釘子的位置值為無窮大。那麼固定第i塊木板的最小編號釘子,相當於[A[i],B[i]]區間的最小值。但是我們這個題實際上是求這些最小值的max,首先如果一個木板A的覆蓋區間完全包含另外一個木板B,則實際上我們只考慮木板B即可。因為固定木板B同時能固定木板A,並且我們一定要固定木板B,即使A覆蓋區間有更小的值,也無法改變最終取最大值的結果。
於是,我們可以建立一個數組plank[x]表示右端點為x的木板的最大左邊界,沒有木板的話,認為邊界是0。我們從左至右遍曆木板右邊界,假設這之前(更左)的木板已經被固定了,已經固定的區間範圍是[left,right) (右開區間),然後對當前這個木板,如果顯然它的右邊界更大(我們遍曆右邊界是按當增的順序),如果該木板start <= left,則它已經被前面固定住了,不影響結果。否則,要求[start,end]之間的最大值。這個問題有點像滑動視窗最大值的問題。本質在於:我們不斷查詢最大值,每次查詢的時候視窗的左邊界和右有邊界是單調遞增的,於是我們可以動態更新視窗維護最大值。這個經典問題可以用單調隊列實現,這也是把單調隊列發揮到了極致。
結論: 查詢時段最大值的時候,如果視窗向右滑動的過程中,查詢時左邊界和右邊界都是單增的,則可以使用單調隊列解決。
時間複雜度 O(N + M)達到了線性。
代碼:
// you can also use includes, for example:// #include <algorithm>#include <deque>const int inf = 2000000000;int solution(vector<int> &A, vector<int> &B, vector<int> &C) { // write your code in C++98 int m = C.size(), M = (m << 1) | 1; vector<int> nail(M, inf); for (int i = m - 1; i >= 0; --i) { nail[C[i]] = i; } vector<int> plank(M, 0); for (int i = 0; i < A.size(); ++i) { plank[B[i]] = max(plank[B[i]], A[i]); } int left = 0, right = 0, r = 0; deque<int> q; for (int i = 1; i < M; ++i) { if (plank[i] > left) { left = plank[i]; while ((!q.empty()) && (q.front() < left)) { q.pop_front(); } for (right = max(right, left); right <= i; ++right) { while ((!q.empty()) && (nail[q.back()] >= nail[right])) { q.pop_back(); } q.push_back(right); } r = max(r, nail[q.front()]); if (r >= inf) { return -1; } } } return r + 1;}