Given a non-empty array of numbers, a0, a1, a2, … , an-1, where 0 ≤ ai < 231.
Find the maximum result of ai XOR aj, where 0 ≤ i, j < n.
Could you do this in O(n) runtime?
Example:
Input: [3, 10, 5, 25, 2, 8] Output: 28 Explanation: The maximum result is 5 ^ 25 = 28.
這道題是一道典型的位操作 Bit Manipulation 的題目,我開始以為異或值最大的兩個數一定包括數組的最大值,但是 OJ 給了另一個例子 {10,23,20,18,28},這個數組的異或最大值是 10 和 20 異或,得到 30。那么只能另辟蹊徑,正確的做法是按位遍歷,題目中給定了數字的返回不會超過 2^31,那么最多只能有 32 位,我們用一個從左往右的 mask,用來提取數字的前綴,然后將其都存入 HashSet 中,我們用一個變量t,用來驗證當前位為1再或上之前結果 res,看結果和 HashSet 中的前綴異或之后在不在 HashSet 中,這里用到了一個性質,若 a^b=c,那么 a=b^c,因為t是我們要驗證的當前最大值,所以我們遍歷 HashSet 中的數時,和t異或后的結果仍在 HashSet 中,說明兩個前綴可以異或出t的值,所以我們更新 res 為t,繼續遍歷,如果上述講解不容易理解,那么建議自己帶個例子一步一步試試,並把每次循環中 HashSet 中所有的數字都打印出來,基本應該就能理解了,算了,還是博主帶着大家來看題目中給的例子吧:
3 10 5 25 2 8
11 1010 101 11001 10 1000
我們觀察這些數字最大的為 25,其二進制最高位在 i=4 時為1,那么我們的循環 [31, 5] 之間是取不到任何數字的,所以不會對結果 res 有任何影響。
當 i=4 時,我們此時 mask 為前 28 位為 ‘1’ 的二進制數,跟除 25 以外的任何數相‘與’,都會得到0。 然后跟 25 的二進制數 10101 相‘與’,得到二進制數 10000,存入 HashSet 中,那么此時 HashSet 中就有0和16兩個數字。此時我們的t為結果 res(此時為0)‘或’上二進制數 10000,得到二進制數 10000。然后我們遍歷 HashSet,由於 HashSet 是無序的,所以我們會取出0和 16 中的其中一個,如果 prefix 取出的是0,那么 t=16 ‘異或’上0,還等於 16,而 16 是在 HashSet 中存在的,所以此時結果 res 更新為 16,然后 break 掉遍歷 HashSet 的循環。實際上 prefix 先取 16 的話也一樣,那么 t=16 ‘異或’上 16,等於0,而0是在 HashSet 中存在的,所以此時結果 res 更新為 16,然后 break 掉遍歷 HashSet 的循環。
3 10 5 25 2 8
11 1010 101 11001 10 1000
當 i=3 時,我們此時 mask 為前 29 位為 ‘1’ 的二進制數,如上所示,跟數字 3,5,2 中任何一個相‘與’,都會得到0。然后跟 10 的二進制數 1010,或跟8的二進制數 1000 相‘與’,都會得到二進制數 1000,即8。跟 25 的二進制數 11001 相‘與’,會得到二進數 11000,即 24,存入 HashSet 中,那么此時 HashSet 中就有 0,8,和 24 三個數字。此時我們的t為結果 res(此時為 16)‘或’上二進制數 1000,得到二進制數 11000,即 24。此時遍歷 HashSet 中的數,當 prefix 取出0,那么 t=24 ‘異或’上0,還等於 24,而 24 是在 HashSet 中存在的,所以此時結果 res 更新為 24,然后 break 掉遍歷 HashSet 的循環。大家可以嘗試其他的數,當 prefix 取出 24,其實也可以更新結果 res 為 24 的。但是8就不行啦,因為 HashSet 中沒有 16。不過無所謂了,我們只要有一個能更新結果 res 就可以了。
3 10 5 25 2 8
11 1010 101 11001 10 1000
當 i=2 時,我們此時 mask 為前 30 位為 ‘1’ 的二進制數,如上所示,跟3的二進制數 11 相‘與’,會得到二進制數0,即0。然后跟 10 的二進制數 1010 相‘與’,會得到二進制數 1000,即8。然后跟5的二進制數 101 相‘與’,會得到二進制數 100,即4。然后跟 25 的二進制數 11001 相‘與’,會得到二進制數 11000,即 24。跟數字2和8相‘與’,分別會得到0和8,跟前面重復了。所以最終 HashSet 中就有0,4,8,和 24 這四個數字。此時我們的t為結果 res(此時為 24)‘或’上二進制數 100,得到二進制數 11100,即 28。那么就要驗證結果 res 能否取到28。我們遍歷 HashSet,當 prefix 取出0,那么 t=28 ‘異或’上0,還等於 28,但是 HashSet 中沒有 28,所以不行。當 prefix 取出4,那么 t=28 ‘異或’上二進制數 100,等於 24,在 HashSet 中存在,Bingo!結果res更新為 28。其他的數可以不用試了。
3 10 5 25 2 8
11 1010 101 11001 10 1000
當 i=1 時,我們此時 mask 為前 31 位為 ‘1’ 的二進制數,如上所示,每個數與 mask 相‘與’后,我們 HashSet 中會有 2,4,8,10,24 這五個數。此時我們的t為結果 res(此時為 28)‘或’上二進制數 10,得到二進制數 11110,即 30。那么就要驗證結果 res 能否取到 30。我們遍歷 HashSet,當 prefix 取出2,那么 t=30 ‘異或’上2,等於 28,但是 HashSet 中沒有 28,所以不行。當 prefix 取出4,那么 t=30 ‘異或’上4,等於 26,但是 HashSet 中沒有 26,所以不行。當 prefix 取出8,那么 t=30 ‘異或’上8,等於 22,但是 HashSet 中沒有 22,所以不行。當 prefix 取出 10,那么 t=30 ‘異或’上 10,等於 20,但是 HashSet 中沒有 20,所以不行。當 prefix 取出 24,那么 t=30 ‘異或’上 24,等於6,但是 HashSet 中沒有6,所以不行。遍歷完了 HashSet 所有的數,結果 res 沒有被更新,還是 28。
3 10 5 25 2 8
11 1010 101 11001 10 1000
當 i=0 時,我們此時 mask 為前 32 位為 ‘1’ 的二進制數,如上所示,每個數與 mask 相‘與’后,我們 HashSet 中會有 2,3,5,8,10,25 這六個數。此時我們的t為結果 res(此時為 28)‘或’上二進制數1,得到二進制數 11101,即 29。那么就要驗證結果 res 能否取到 29。取出 HashSet 中每一個數字來驗證,跟上面的驗證方法相同,這里博主偷懶就不寫了,最終可以發現,結果 res 無法被更新,還是 28,所以最終的結果就是 28。
綜上所述,我們來分析一下這道題的核心。我們希望用二進制來拼出結果的數,最終結果 28 的二進制數為 11100,里面有三個 ‘1’,我們來找一下都是誰貢獻了這三個 ‘1’?在 i=4 時,數字 25 貢獻了最高位的 ‘1’,在 i=3 時,數字 25 貢獻了次高位的 ‘1’,在 i=2 時,數字5貢獻了第三位的 ‘1’。而一旦某個數貢獻了 ‘1’,那么之后在需要貢獻 ‘1’ 的時候,此數就可以再繼續貢獻 ‘1’。而一旦有兩個數貢獻了 ‘1’ 后,那么之后的 ‘1’ 就基本上只跟這兩個數有關了,其他數字有 ‘1’ 也貢獻不出來。驗證方法里使用了前面提到的性質,a ^ b = t,如果t是所求結果話,我們可以先假定一個t,然后驗證,如果 a ^ t = b 成立,說明該t可以通過a和b‘異或’得到。參見代碼如下:
class Solution { public: int findMaximumXOR(vector<int>& nums) { int res = 0, mask = 0; for (int i = 31; i >= 0; --i) { mask |= (1 << i); unordered_set<int> s; for (int num : nums) { s.insert(num & mask); } int t = res | (1 << i); for (int prefix : s) { if (s.count(t ^ prefix)) { res = t; break; } } } return res; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/421
參考資料:
https://leetcode.com/problems/maximum-xor-of-two-numbers-in-an-array/
https://leetcode.com/problems/maximum-xor-of-two-numbers-in-an-array/discuss/130427/()-92
