摘要:本文首先解決的是數組中兩元素與運算最大值問題,之後拓展異或運算最大值問題。建議讀者順序閱讀,比較兩問的相同與不同。
問題
給定一個數組 ,求,其中。
分析
顯然這道題目實際上是二進制運算問題。以下圖(a)的數組爲例:
對於&運算來說,相同位置上的數字同時爲1,該位置運算結果才爲1。
我們不妨從結果反推一下,假如最大值 在第 位上bit爲1,則代表選中的兩個元素在該位上的 bit 也爲 1。如結果爲 0101,則被選中的兩個元素形式必定爲 x1x1(x表示任意取值。)
爲了確保結果值最大,我們希望選中的兩個數字應該儘可能在高位&運算得到1。 所以我們從最高位開始我們的算法。
如圖(b),我們觀察所有元素的最高位,顯然,元素在最高位進行 & 運算都能得到1。所以,假如存在最大值,最大值必然是這三個元素中的某兩個元素計算得,我們不必考慮和其他元素的組合,直接刪掉。
接下來考慮次高位,我們發現,在剩下的三個元素中,任意兩個元素的 & 運算結果都爲0,代表着最終結果 在這一位上應該也爲0。剩下元素的任何組合都不影響最終結果,可以任意取兩個。
繼續看後面的位。一直將算法應用到下圖( c )的位置,此時,我們發現,在該位的 & 運算結果爲1,而與任何元素的運算結果都爲0,所以,最大值必然是。
解決方案
方法一:剔除元素至只剩兩個
根據我們上面的觀察,從最高位開始,假如當前位上存在兩個元素可以 & 運算獲得 1,我們可以將所有取值爲 1 的元素保留下來,而取值爲 0 的元素無論怎麼運算都是 0,直接刪除;假如當前位任何元素 & 運算都不能得到 0,我們就忽略這位,繼續觀察下一位。直到剩下兩個元素,那麼這兩個元素 & 運算肯定是最大的。
算法流程:
- 初始化當前位爲最高位
- 統計數組當前位上爲 1 的元素的個數 count
- 假如 count 大於1,刪除數組中該位爲0的元素(大於 1 表示至少有兩個元素該位取值爲 1)
- 若數組剩下兩個元素,返回兩個元素的 & 運算結果。若當前位爲最後一位,返回剩下任意兩個元素的 & 運算結果。否則當前位取下一位,返回步驟2
方法二(改進版):直接計算結果
算法一隻要剩下兩個元素就能夠提前結束。但是每一輪循環結束都要檢查當前數組剩下元素的個數,不太方便,我們可以稍微改進一下。
依然是統計1 的個數和剔除元素的思路,但是我們發現,每一次循環結束其實已經可以確定結果中的一位的取值了。所有循環結束,就能確定所有位。對 int 類型來說,32次循環就能確定結果的32位取值。
算法流程:
- 初始化當前位爲最高位,初始化結果result爲 0
- 統計數組當前位上爲 1 的元素的個數 count
- 假如 count 大於1,刪除數組中該bit爲0的元素。同時,將result的當前位 bit 置 1
- 若當前位爲最後一位,返回result。否則當前位取下一位,返回步驟2
兩種方法的思路基本一致,其中方法二利用了結果與選中元素的關係,可直接計算結果,而不理會剩餘元素的個數問題,更巧妙。
實現
怎麼統計位上 1 的個數?
使用一個flag與數組元素進行 & 運算,如0x01可以判斷最低位是不是1,使用位操作(<<)就能確定不同位上的取值是1還是0。
怎樣剔除元素?
可以使用一個標記數組,標記被剔除/留下的元素。
下面是方法二的C/C++實現
unsigned Max(unsigned array[], int len){
int maxBit = sizeof(unsigned) * 8; // 最高位數
unsigned currBit = 1; // 當前位
int mark[len]; // 標記被剔除元素的數組,下標對應相應的元素
int result = 0;
for (int i = 0; i < len; i++) mark[i] = 1; // 第i個元素爲1,表示保留
currBit = currBit << (maxBit-1);
for (int i = 0; i < maxBit; i++){
// 統計
int count = 0;
for(int j = 0; j < len; j++){
if(mark[j] == 1 && (array[j] & currBit) != 0)
count++;
}
if (count > 1){
// 剔除
for(int j = 0; j < len; j++){
if ((array[j] & currBit) == 0)
mark[j]=0; // 標記數組置爲0表示剔除
}
// 更新結果,如 0100 + 0010 = 0110
result += currBit;
}
// 下一位
currBit = currBit >> 1;
}
return result;
}
拓展:求數組兩個元素異或(^)運算最大值
題目改寫爲求兩個元素的異或值。
我發現雖然兩個題目背景很相似,可是上題的統計 1 個數的思路已經不適用了。
經查閱,網上大多數解法有兩種,其一爲構建前綴樹(Trie),其二爲利用異或運算的公式。
前綴樹(Trie)
其思路是利用數組中的每個元素二進制表示形式建一棵樹,我看到網上大多數解法都開了太大的數組空間,不知道爲什麼,但是我覺得沒有必要。
只要用現有的數組元素二進制值建一棵深度爲33的樹即可,從根到葉子結點的路徑就代表了一個元素.然後再對數組中每一個元素取反之後到Trie中去搜索最大的異或值。
爲什麼要取反呢?
因爲取反之後在查找Trie的時候如果當前匹配的話,那麼就說明當前這一位異或之後是爲1的,我們就可以繼續沿着這個分支走下去.如果不匹配說明異或之後當前這位是爲0,並且這個分支爲空,所以我們只能走另外一個分支。
時間複雜度爲,也就是。
前綴樹解法的原文:數組中兩個元素異或求最大值
二進制異或運算
下面討論第二種,對於二進制異或運算,我們有:
兩邊同時 ^b 得,a ^ b ^ b = c ^ b
其中,b ^ b = 0,且 a ^ 0 = a
故 a = b ^ c
和上一題比較類似的是我們仍然可以從結果來反推選中的元素。假如最大值 在第 位上 bit 爲1,則說明選中的兩個元素在當前位的取值不同(0/1)。
我們仍然希望選中的兩個數字應該儘可能在高位異或運算得到1,所以處理一下數組元素,每次只考慮高位,低位的bit設爲0。
考慮二進制的前 1 位,其後的位設 0,如1101設爲1000。根據我們的公式,我們可以假設當前位的值爲 1,如 1000。取出處理過的數組中一個元素,如取出爲 1000。顯然,如果當前位的值爲 1,我們可以通過 計算出另一個元素的二進制肯定是 0000。之後在數組中尋找0000 即可。若找到則說明我們的假設成立了,的當前位確實爲 1。
如果沒有找到 0000,我們可以取出下一個元素 ,繼續通過 計算出對應元素的二進制,查找。
直到所有的元素都取出一遍,若都沒有找到對應的一對元素,說明假設不成立,當前位爲 0。其本質是沒有一對元素可以通過異或運算得到我們假設的,故假設應該摒棄。
考慮二進制的前 2 位,其後的位設 0,如1101設爲1100。此時當前位挪到第 2 位,第 1 位的取值已經在上一步得到。同樣假設當前位爲1,如 1100,取出處理後數組的一個元素,計算可能存在的對於元素,查找,判斷假設是否成立。成立就 break,不成立就繼續取下一個元素,以此類推。可以得到 前 2 位的取值。
考慮二進制的前 3 位 …
當所有位都考慮完,的結果也可以直接得出了。
實現思路:處理數組,只考慮前 n 位 取元素,計算另一元素取值 查找
下面是實現的Java代碼:
int findMaximumXOR(int[] nums) {
int len = nums.length;
if(len < 2) return 0;
int max = 0;
int flag = 0;
for (int i = 31; i >= 0; i--){
HashSet<Integer> hash = new HashSet<>();
// 獲取處理後的數組,存在hash集合中
flag = flag | (1 << i);
for(int num: nums)
hash.add(num & flag); // &運算將低位bit置0
// 查找集合中是否存在計算出來的元素
int temp = max | (1 << i); // 假設值爲temp
for (int num: hash) {
if (hash.contains(num ^ temp)) {
max = temp;
break;
}
}
}
return max;
}
總結
異或最大值的兩種算法的時間複雜度都是,都是利用空間換時間。第一種方法構造前綴樹(字典樹)自不用說,第二種方法也是巧妙地利用了 hash 查找 時間開銷的特點。
本文結束,歡迎留言討論。