1. 程式人生 > 其它 >[演算法入門]線性基

[演算法入門]線性基

#0.0 前置知識

下文中所說的集合除特殊說明,均指“無符號整數集”。

#0.1 張成

\(T\subseteq S\),所有這樣的子集 \(T\) 的異或和組成的集合稱為 \(S\)張成,記作 \(\text{span}(S)\)。即在 \(S\) 中選出任意多個數,其異或和的所有可能的結果組成的集合。

#0.2 線性相關

對於一個集合 \(S\),如果存在一個元素 \(S_j\),使得 \(S\) 的在除去這個元素的集合 \(S'\) 的張成 \(\text{span}(S')\) 中包含 \(S_j\),那麼就說 \(S\) 線性相關

相反的,如果集合 \(S\) 中不存在這樣一個元素,那麼就稱 \(S\)

線性無關

說的簡單一些,就是如果 \(S\) 線性相關,那麼存在一個元素可以由其他元素異或得到。

由此得到一個性質:對於一個線性相關的集合 \(S\),去除掉可以由其他元素異或得到的元素後,集合的張成不變。

#1.0 線性基

#1.1 線性基的性質

  • 線性基的元素能相互異或得到原集合的元素的所有相互異或得到的值。
  • 線性基是滿足性質 1 的最小的集合。
  • 線性基沒有異或和為 0 的子集。
  • 線性基中每個元素的異或方案唯一,也就是說,線性基中不同的異或組合異或出的數都是不一樣的。
  • 線性基中每個元素的二進位制最高位互不相同。

#1.2 線性基的構造

線性基一般採用動態構造的方式,即從一個空的線性基開始,逐個插入某個數 \(t\)

\(S\) 中用二進位制表示下有 \(L\) 位,那麼我們用一個長為 \(L\) 的陣列 \(a\) 來儲存該集合的線性基。

對於每一個 \(i\)\(a_i\) 只有以下兩種可能:

  1. \(a_i=0\),且只有滿足 \(j>i\)\(a_j\) 的第 \(i\) 個二進位制位上可能\(1\)
  2. \(a_i\ne0\),且
    • 整個 \(a\) 陣列中只有 \(a_i\) 的第 \(i\) 個二進位制位上為 \(1\);
    • \(a_i\) 更高的二進位制位一定\(0\)
    • \(a_i\) 更低的二進位制位可能\(1\)

注意,“整個 \(a\) 陣列中只有 \(a_i\)

的第 \(i\) 個二進位制位上為 \(1\)”這個性質是這個構造方案所特有的,並不是線性基必須具有的性質。

那麼構造方案據顯而易見了:對於一個原陣列中的數 \(t\),從 \(t\) 最高位的 \(1\) 開始考慮,假設這是第 \(j\) 位,且 \(a_j\ne0\),那麼就將 \(t\) 異或上 \(a_j\),也就是將 \(t\) 的第 \(j\) 個二進位制位上的 \(1\) 消掉,直到找到一個二進位制位 \(i\),滿足當前 \(t\) 的最高位 \(1\) 為第 \(i\) 位,且 \(a_i=0\),那麼我們就可以將 \(t\) 插入到 \(a_i\) 這個位置,插入時需要滿足:

  • \(t\)\(i\) 更高的二進位制位一定\(0\);這一點不必考慮,在上面的過程中已經消掉了;
  • \(t\)\(i\) 更低的二進位制位 \(j\),若 \(a_j\ne0\),那麼 \(t\) 的第 \(j\) 位必須為 \(0\);對於這個要求,我們可以列舉 \(t\) 的更低的為一的二進位制位 \(k\),如果 \(a_k\ne0\),那麼就讓 \(t\) 異或上 \(a_k\)
  • 整個 \(a\) 陣列中只有 \(a_i\) 的第 \(i\) 個二進位制位上為 \(1\);對於 \(j<i\) 的情況無需考慮,我們來看 \(j>i\) 的情況,對於每一個這樣的情況,我們讓 \(a_j\) 異或上 \(t\) 即可;

那麼不難寫出上面構造的程式碼。

#1.3 程式碼實現

void insert(ll t) {
    for (int i = 51; i >= 0; i --) {
        if (!(t & (1ll << i))) continue;
        if (a[i]) t ^= a[i];
        else {
            /*注意,這裡兩個迴圈的順序不能交換*/
            for (int j = 0; j < i; j ++)
              if (t & (1ll << j)) t ^= a[j];
            for (int j = 51; j > i; j --)
              if (a[j] & (1ll << i)) a[j] ^= t;
            a[i] = t; break;
        }
    }
}

時間複雜度為 \(O(\log n)\) 的。

#1.4 線性基的應用

注意到上面的構造方式,對任意的 \(a_i\ne0\)\(a_i\) 都是與線性基中其他的數或原集合中的數異或得來,而異或這個操作顯然是可逆的,所以線性基中的任意一個數都是可以由原集合中的數異或得來,同樣可以用 \(a\) 中的數異或得到原集合中的數。

#1.4.1 最大異或值

按上面的構造方案,直接將所有的 \(a\) 異或得到的值便是整個陣列的最大異或值。

這一點並不難理解,因為在上面我們保證了對於任意的 \(a_i\ne0\),僅有 \(a_i\) 的第 \(i\) 個二進位制位不為 \(0\),所以可以保證將每異或一個不為零的 \(a_k\),二進位制下第 \(k\) 位會變為 \(1\),且永遠不會再變回 \(0\)

至於 \(a_k=0\) 的情況,意味著不存在一種方案使得第 \(k\) 位可以在不被 \(a_j(j>k)\) 控制的情況下單獨為 \(1\),如果強行讓第 \(j\) 位為 \(1\),得到的答案不會更優,因為可能會導致更高位的 \(1\) 消失。

#1.4.2 最小非零異或值

顯然,線性基中最小的、不為零的 \(a_k\) 即為答案。

#1.4.3 查詢異或可行性

對於一個數 \(x\),我們想要知道它能否由 \(S\) 中的數異或得到,我們可以先構建出 \(S\) 的線性基 \(a\),再去嘗試將 \(x\) 插入 \(a\),如果最終 \(x\) 沒有被插入,即被消為 \(0\),那麼意味著可以被異或得到。相反則不能。

#2.0 另一種構造

我們還有另一種構造方式,但是會失去“整個 \(a\) 陣列中只有 \(a_i\) 的第 \(i\) 個二進位制位上為 \(1\)”這個特殊性質,但相對來說構造要更簡潔一些。與上面的構造方案唯一的區別就是不需要維護該性質,這裡不多贅述。

void insert(ll t) {
    for (int i = 51; i >= 0; i --) {
        if (!(t & (1ll << i))) continue;
        if (a[i]) t ^= a[i];
        else {a[i] = t; break;}
    }
}

要注意,對於這種構造方式,若我們要求最大異或值,在做異或時需要判斷異或後得到的值是否更大。

#3.0 更多操作

#3.1 線性基合併

兩個集合的線性基合併後得到的線性基為兩個集合的並的線性基。我們只需要將其中一個線性基中的數全部插入另一個線性基即可。

參考資料

[1] 線性基學習筆記 - Menci

[2] 線性基小記 - command_block

[3] 線性基 - OI Wiki