求數組兩個元素與(&)運算最大值,異或(^)運算最大值

摘要:本文首先解決的是數組中兩元素與運算最大值問題,之後拓展異或運算最大值問題。建議讀者順序閱讀,比較兩問的相同與不同。


問題

給定一個數組 A[n]A[ n ],求max(A[i] & A[j])max(A[i] \ \& \ A[j]),其中iji \ne j

分析

顯然這道題目實際上是二進制運算問題。以下圖(a)的數組爲例:
在這裏插入圖片描述
對於&運算來說,相同位置上的數字同時爲1,該位置運算結果才爲1。

我們不妨從結果反推一下,假如最大值 maxmax 在第 nn 位上bit爲1,則代表選中的兩個元素在該位上的 bit 也爲 1。如結果爲 0101,則被選中的兩個元素形式必定爲 x1x1(x表示任意取值。)

爲了確保結果值最大,我們希望選中的兩個數字應該儘可能在高位&運算得到1。 所以我們從最高位開始我們的算法。

如圖(b),我們觀察所有元素的最高位,顯然,元素A[1],A[2],A[3]A[1], A[2], A[3]在最高位進行 & 運算都能得到1。所以,假如存在最大值,最大值必然是這三個元素中的某兩個元素計算得,我們不必考慮A[0]A[0]和其他元素的組合,直接刪掉A[0]A[0]
在這裏插入圖片描述
接下來考慮次高位,我們發現,在剩下的三個元素中,任意兩個元素的 & 運算結果都爲0,代表着最終結果 maxmax 在這一位上應該也爲0。剩下元素的任何組合都不影響最終結果,可以任意取兩個。

繼續看後面的位。一直將算法應用到下圖( c )的位置,此時,我們發現,A[1],A[3]A[1], A[3]在該位的 & 運算結果爲1,而A[2]A[2]與任何元素的運算結果都爲0,所以,最大值必然是(A[1]&A[3])(A[1] \& A[3])
在這裏插入圖片描述

解決方案

方法一:剔除元素至只剩兩個

根據我們上面的觀察,從最高位開始,假如當前位上存在兩個元素可以 & 運算獲得 1,我們可以將所有取值爲 1 的元素保留下來,而取值爲 0 的元素無論怎麼運算都是 0,直接刪除;假如當前位任何元素 & 運算都不能得到 0,我們就忽略這位,繼續觀察下一位。直到剩下兩個元素,那麼這兩個元素 & 運算肯定是最大的。

算法流程:

  1. 初始化當前位爲最高位
  2. 統計數組當前位上爲 1 的元素的個數 count
  3. 假如 count 大於1,刪除數組中該位爲0的元素(大於 1 表示至少有兩個元素該位取值爲 1)
  4. 若數組剩下兩個元素,返回兩個元素的 & 運算結果。若當前位爲最後一位,返回剩下任意兩個元素的 & 運算結果。否則當前位取下一位,返回步驟2

方法二(改進版):直接計算結果

算法一隻要剩下兩個元素就能夠提前結束。但是每一輪循環結束都要檢查當前數組剩下元素的個數,不太方便,我們可以稍微改進一下。

依然是統計1 的個數和剔除元素的思路,但是我們發現,每一次循環結束其實已經可以確定結果中的一位的取值了。所有循環結束,就能確定所有位。對 int 類型來說,32次循環就能確定結果的32位取值。

算法流程:

  1. 初始化當前位爲最高位,初始化結果result爲 0
  2. 統計數組當前位上爲 1 的元素的個數 count
  3. 假如 count 大於1,刪除數組中該bit爲0的元素。同時,將result的當前位 bit 置 1
  4. 若當前位爲最後一位,返回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,並且這個分支爲空,所以我們只能走另外一個分支。
時間複雜度爲Θ(32n)\Theta(32 * n),也就是Θ(n)\Theta(n)

前綴樹解法的原文:數組中兩個元素異或求最大值

二進制異或運算

下面討論第二種,對於二進制異或運算,我們有:

若a ^ b = c ,則 b ^ c = a
證明:
a ^ b = c
兩邊同時 ^b 得,a ^ b ^ b = c ^ b
其中,b ^ b = 0,且 a ^ 0 = a
故 a = b ^ c

和上一題比較類似的是我們仍然可以從結果來反推選中的元素。假如最大值 maxmax 在第 nn 位上 bit 爲1,則說明選中的兩個元素在當前位的取值不同(0/1)。

我們仍然希望選中的兩個數字應該儘可能在高位異或運算得到1,所以處理一下數組元素,每次只考慮高位,低位的bit設爲0。

考慮二進制的前 1 位,其後的位設 0,如1101設爲1000。根據我們的公式,我們可以假設maxmax當前位的值爲 1,如 1000。取出處理過的數組中一個元素,如取出A[0]A[0]爲 1000。顯然,如果maxmax當前位的值爲 1,我們可以通過 maxA[0]max * A[0] 計算出另一個元素的二進制肯定是 0000。之後在數組中尋找0000 即可。若找到則說明我們的假設成立了,maxmax的當前位確實爲 1。
如果沒有找到 0000,我們可以取出下一個元素 ii,繼續通過maximax * i 計算出對應元素的二進制,查找。
直到所有的元素都取出一遍,若都沒有找到對應的一對元素,說明假設不成立,maxmax當前位爲 0。其本質是沒有一對元素可以通過異或運算得到我們假設的maxmax,故假設應該摒棄。

考慮二進制的前 2 位,其後的位設 0,如1101設爲1100。此時當前位挪到第 2 位,maxmax第 1 位的取值已經在上一步得到。同樣假設maxmax當前位爲1,如 1100,取出處理後數組的一個元素,計算可能存在的對於元素,查找,判斷假設是否成立。成立就 break,不成立就繼續取下一個元素,以此類推。可以得到 maxmax 前 2 位的取值。

考慮二進制的前 3 位

當所有位都考慮完,maxmax的結果也可以直接得出了。

實現思路:處理數組,只考慮前 n 位 \to 取元素,計算另一元素取值 \to 查找

下面是實現的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;
}

總結

異或最大值的兩種算法的時間複雜度都是Θ(n)\Theta(n),都是利用空間換時間。第一種方法構造前綴樹(字典樹)自不用說,第二種方法也是巧妙地利用了 hash 查找 Θ(1)\Theta(1) 時間開銷的特點。


本文結束,歡迎留言討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章