簡體   English   中英

算法:具有奇數和偶數的數組

[英]Algorithm: array with odd and even numbers

給定一個具有n個元素的數組,我想計算數組的最大范圍,其中奇數和偶數一樣多。

輸入:

2 4 1 6 3 0 8 10 4 1 1 4 5 3 6

預期產量:

12

我嘗試了什么

我嘗試使用以下步驟:

  • 將所有奇數更改為1,將所有偶數更改為-1
  • 檢查所有可能的子數組,並為每個子數組計算1和-1值的總和。
  • 取這些總和為0的最大子數組

但這具有O(n ^ 2)的時間復雜度。

如何在O(n)中執行此操作?

給定 :數組a

任務 :找到具有偶數個奇偶數的最大子陣列

O(n)

  1. 在Java中,將奇數替換為-1並將偶數替換為1時,為每個累加和創建一個哈希表m,該哈希圖具有最高的累積總和:

     Map<Integer, Integer> m = new HashMap<>(); int sum = 0; for (int i = 0; i < a.length; i++) { sum += a[i] % 2 == 0 ? 1 : -1; m.put(sum, i); } 
  2. 在Java中使用m查找最大距離,從而找到最大為0的最大子數組:

     int bestStart = -1, bestEnd = -1; // indexes, so end inclusive sum = 0; for (int i = 0; i < a.length; i++) { Integer end = m.get(sum); sum += a[i] % 2 == 0 ? 1 : -1; if (end != null && end - i > bestEnd - bestStart) { bestStart = i; bestEnd = end; } } 

基於觀察,您可以使用cumSum [y]-cumSum [x-1]獲得x和y的和(在將元素轉換為1和-1之后)。 因此,如果我們希望它為0,則它​​們必須相同(請注意,如果x = 0,則cumSum = 0,未定義cumSum [-1])。

這是一個常見的動態編程問題。 我們可以通過遍歷列表並同時更新最佳解決方案來維護次優解決方案。 這類似於查找數組的最大元素。 將第一個元素設置為最大值,並在每次迭代中對其進行更新(如果需要)。 這個問題只需要一點。

我們需要5個指針(整數,實際上是5個)。 開始指針,結束指針和當前指針maxend,maxstart。 startcurrent指針設置為數組的開始。 當下一個元素遵循規則時(交替使用奇數和偶數),請增加current指針。 一旦他們不遵守規則,請將結束指針設置為當前指針。 比較end-start指針的差,如果它大於maxend-maxstart,則更改maxend和maxstart並繼續此操作。 最后,您可以打印maxstart和maxend之間的數組部分。

使用1表示奇數,使用-1表示偶數的方法確實是正確的方法。 讓我們將這些值稱為增量(因此包括減量)。

以示例輸入為例,您可以將這些增量可視化,如下所示:

  2   5   6   0   8   3   4   5   2   7   4   8   2   6   6   5   7 
 -1   1  -1  -1  -1   1  -1   1  -1   1  -1  -1  -1  -1  -1   1   1   

然后,您可以可視化其下的那些增量的累積總和:

  2   5   6   0   8   3   4   5   2   7   4   8   2   6   6   5   7 
 -1   1  -1  -1  -1   1  -1   1  -1   1  -1  -1  -1  -1  -1   1   1   
0  -1   0  -1  -2  -3  -2  -3  -2  -3  -2  -3  -4  -5  -6  -7  -6  -5

 \     / \
   \ /     \
             \
               \ 
                 \     / \     / \     / \   
                   \ /     \ /     \ /     \ 
                                             \
                                               \
                                                 \
                                                   \
                                                     \             /
                                                       \         /
                                                         \     /
                                                           \ /

請注意,具有相等數量的偶數和奇數的范圍對應於增量加總為0的范圍。您可以通過選擇開始/結束點來確定這樣的范圍,以使開始之前的累積和等於該范圍結束后的累積總和。 這就像在上面的“圖形”中繪制一條水平線並獲取最遠的交叉點。

因此,例如,在總和中首次出現兩個-1值表示具有[5, 6]的子數組是有效范圍(其中有相等數量的奇數和偶數)。 尋找其他這樣的范圍,我們可以發現采用最左邊的-3會產生更大的結果: [3 4 5 2 7 4] 我們也可以將-2作為邊界值: [8 3 4 5 2 7]

我們還可以看到,最長范圍必須與介於0和結束總和之間的總和相對應(在示例中為-5)。 以不在此范圍內的示例為例:在示例中為-6。 因為0和-5都在-6的同一側,所以我們確信使用-5可以得到更好的結果(將圖形中的水平線向上移動)。 對於所有介於0到最終和之間的中間和值,都是如此。

可以得出的另一個結論是,總是有可能找到最佳解決方案,其中左端點與方向變化相對應。 在此示例中,第一個點的總和為-3的情況。

算法

您可以創建一個遞歸算法,只要找到符合上述規則的點,該算法便會遞歸:

  • 將起點值相加之前的總和在0和最終總和之間
  • 起始位置的增量與之前的增量(或之前沒有增量)不同,並且從開始到結束都與總體方向相反。 這對應於上圖中的一個谷,但是當最終總和為正時將是一個山頂。

求和等於最終和時,遞歸停止。 這立即導致尺寸。 此大小返回給調用者(遞歸樹中的上一層),並且數組的末尾被縮短,直到最終總和等於該遞歸級別上正在查看的總和。 這再次導致尺寸。 這兩個大小中最好的一個將返回給調用者,等等。

除遞歸生成的調用棧外,該算法中未創建任何數組。 但是,如果數組是完全隨機的,則遞歸調用的次數應平均較少,因為按統計術語,總和的預期值為0。

時間復雜度為O(n),因為總和的計算顯然是O(n) ,而其他兩個循環沿一個方向移動數組的開始索引或結束索引,從此不再訪問同一元素。

JavaScript實現

此代碼使用最簡單的JavaScript語法,因此算法很明確:

 function value(x) { // Return 1 when the given value is odd, else -1 return (x % 2) || -1; } function largestRange(a) { var sign, sumEnd, end; // Calculate final sum sumEnd = 0; for (end = 0; end < a.length; end++) { sumEnd = sumEnd + value(a[end]); } // ... and its sign (1 or -1 or 0) sign = Math.sign(sumEnd); function recurse(start, sumStart) { var sum, i, size, val; // End of recursion: if (sumStart === sumEnd) return end - start; sum = sumStart for (i = start; sum !== sumEnd; i++) { val = value(a[i]); // Got closer to sumEnd, and now moving away from it if (val !== sign && Math.sign(sum - sumStart) == sign) break; sum = sum + val; } // Get longest range size for this particular sum size = recurse(i, sum); // Get range size for sumStart while (sumEnd !== sumStart) { end--; sumEnd = sumEnd - value(a[end]); } // Retain the best of both: if (end - start > size) size = end - start; return size; } // Initiate the recursion and return result return recurse(0, 0); } // Sample input var a = [2, 5, 6, 0, 8, 3, 4, 5, 2, 7, 4, 8, 6, 6, 5, 7]; // Calculate var size = largestRange(a); // Output size of longest range console.log(size); 

性能

由於對於隨機數組,預期總和預計接近於0,因此大多數中間總和值可能會超出0和該最終總和的范圍,這意味着它們不會占用太多時間,也沒有空間。

我進行了一些性能測試,並將其與為每個遇到的總和創建帶有密鑰的哈希的解決方案進行了比較。 這些算法對於較短的輸入數組更快,但是對於較大的數組(例如1000個條目),以上算法表現更好。 當然,可以修改基於散列的解決方案,以考慮到我上面確定的規則,然后對於較大的數組也將表現更好。 遞歸帶來了哈希映射所沒有的一些開銷。

但是,由於您對注釋感興趣,希望看到沒有哈希圖的解決方案,因此我選擇了該解決方案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM