1. 程式人生 > >線性基(處理集合異或的強力工具)

線性基(處理集合異或的強力工具)

看了好多篇關於線性基的部落格,只是說明了怎麼求線性基,但是大都沒有說明為什麼這樣求線性基。

定義:

有一個集合 S  = {a1,a2...,an},T的滿足下面條件的一個最小子集A = {a1,a2,....,ak}

A的所有子集的異或和的值域與T的所有子集的異或和的值域相同,那麼A就是T的線性基。

預備知識:

1、張成:S的所有子集,其異或和的所有可能的結果組成的集合,為S的張成,記作span(S)。

2、線性相關:對於一個集合S,如果存在一個元素Sj,去除這個元素後得到的集合S'的張成span(S')中包含Sj,即SJspan(S'),則稱集合S是線性相關的。如果不存在這樣的Sj,那麼集合S就是線性無關

的。

3、線性基:有了上面兩個名詞,我們還可以這樣定義線性基。

    (1)A ⊆ span(S)

    (2)A是線性無關的

則集合A是集合S的線性基。

性質:

1、A是一個集合的線性基,那麼它的任何真子集都不可能是線性基;

2、S中所有的向量都可以按唯一的方式表達為 A 中元素的線性組合(也就是異或和)。

構造:

我們令集合中的數為a1,a2,....,an,b[ ]陣列用來儲存線性基裡面的數。(下面的二進位制的位數下標從0開始)

第一種情況:找到ai的最高位,假如是第j位,如果b[j]還沒有數,即b[j] = 0,那麼現在b陣列中的數的張成肯定不包含ai,那麼我們就可以b[j] = a[i],然後利用已經線上性基裡的最高位小於j的把b[j]二進位制中的1給消掉,具體就是 b[j] ^= b[k] ( k < j && b[j]的第k位為1 && b[k] != 0) ,然後用同樣的方法把大於j的也消掉。

第二種情況:找到ai的最高位,假如是第j位,如果b[j]已經有數,就判斷現線上性基的張成span(a1,a2...,ai-1)包不包含ai,如果包含,那麼ai就沒有必要加進線性基。怎麼判斷呢,我們如果ai的第j位為1且b[j] != 0,那麼我們就把ai的值異或上b[j],依次往後判斷,直到ai當前的最高位對應的b[j] == 0,就可以把ai加入線性基,執行上面第一種情況的操作,或者ai為0,就丟掉ai。

程式碼如下:

void create()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=60;j>=0;j--)
        {
            if((1LL<<j)&a[i])
            {
                if(b[j] != 0)
                    a[i] ^= b[j];
                else
                {
                    b[j] = a[i];
                    for(int k=j-1;k>=0;k--)
                        if(((1LL<<k)&b[j]) && b[k])
                            b[j] ^= b[k];
                    for(int k=j+1;k<=60;k++)
                        if(((1LL<<j)&b[k]))
                            b[k] ^= b[j];
                    break;
                }
            }
        }
    }
}

這段程式碼是維護一個對角矩陣,加入一行之後,先用下面的行消自己,然後再用自己去消上面的行。

我們來演示一下這個過程:

加入n = 5,a = {7,1,3,4,5}

初始矩陣: 

                            0 0 0 

                            0 0 0

                            0 0 0

插入7之後:

                            1 1 1

                            0 0 0 

                            0 0 0 

插入3之後,為了維護對角矩陣,把7的低位消掉:

                            1 0 0

                            0 1 1 

                            0 0 0

插入1之後,把1上面的行的低位都消掉:

                            1 0 0 

                            0 1 0

                            0 0 1

然後就發現後面那幾個數都已經包含在b陣列的張成裡了,加不進去了。

上述過程是把線性基維護成一個對角矩陣,其實我們還有一種程式碼量比較少的線性基的構造方法,就是隻把矩陣消成上三角矩陣,這樣的話同樣可以知道哪一位存在於線性基內。

線性基的操作:

我們把線性基封裝成一個結構體,這樣使用起來方便一點:

struct LineBasis
{
    LL b[66];
    LL p[66];
    int cnt;
    int max_b = 62;
    LineBasis()
    {
        memset(b,0,sizeof(b));
        memset(p,0,sizeof(p));
        cnt = 0;
    }
}

這裡的b陣列就是用於存線性基裡的數,這個cnt是記錄線性基裡面有多少個數。

那麼這個cnt有什麼作用呢,2的cnt次冪就是這個線性基所有子集異或和能構成的不同元素的個數(這裡包括零)。

max_b是最大那個數二進位制的長度

下面介紹他的各個函式:

1、插入

上面雖然展示了一種線性基的構造方法,那個方法可以提現出線性基的性質,但是下面我們用它比較方便的寫法:

    bool Insert(LL val)
    {
        for(int i=max_b;i>=0;i--)
        {
            if((1LL<<i)&val)
            {
                if(b[i] == 0)
                {
                    b[i] = val;
                    break;
                }
                val ^= b[i];
            }
        }
        if(val > 0)
            cnt++;
        return val > 0;
    }

2、合併:

把一個線性基裡的元素一個一個的Insert到另一個裡面,就完成了合併。

    LineBasis Merge(LineBasis n1,LineBasis n2)
    {
        LineBasis ret = n1;
        for(int i=0;i<=max_b;i++)
            if(n2.b[i])
                ret.Insert(n2.b[i]);
        return ret;
    }

線性基的用途:

1、求一組數所有組合能構成的不同異或和的個數:

求出這組數的線性基,線性基裡數的個數為cnt,答案就為2的cnt次冪

2、存在性:

查詢x是否存在於異或集合中,跟上面構造方法的思想相似,從高位到低位掃描x位1的二進位制位,掃到第i位時,x ^= b[i],如果最後x變為0,則存在,否則不存在。

3、最大值:

求異或集合中的最大值

如果消成對角矩陣的話,直接把線性基中的所有元素異或起來即可。但是對於上三角矩陣,異或之前判斷一下是否能變大。

還可以求一個數x與集合中某些數異或的最大值,只用把初值設為x就行了,單純求最大值時最初始值設為0.

    LL QueryMax(LL x)
    {
        LL ans = x;
        for(int i=max_b;i>=0;i--)
            if((ans^b[i]) > ans)
                ans ^= b[i];
        return ans;
    }

4、最小值:

最小值就是最低位上的線性基。

    LL QueryMin()
    {
        for(int i=0;i<=max_b;i++)
            if(b[i])
                return b[i];
        return 0;
    }

5、k小值

這時候用構造出的上三角矩陣就不能解決這個問題了,我們要把上三角矩陣變換成對角矩陣,然後再把不為零的都按順序拿出來。這時候矩陣已經變成對角矩陣(至少是行最簡形矩陣),我們異或上某一行的值,答案就會變大一點。我們可以想象,從一個數組 a = {8,4,2,1}中選出幾個,求能組成第k小的值是多少,利用二進位制的性質,如果k的二進位制第i位為1,我們就加上數組裡第i大的數。這裡的異或上一個值也會變大一點,所以可以用同樣的思想。具體看下面程式碼

    void rebuild()
    {
        for(int i=max_b;i>=0;i--)
            for(int j=i-1;j>=0;j--)
                if(b[i]&(1LL<<j))
                    b[i] ^= b[j];
        cnt = 0;
        for(int i=0;i<=max_b;i++)
            if(b[i])
                p[cnt++] = b[i];
    }
    LL kthquery(LL k)
    {
        LL ans = 0;
        if(k>=(1LL<<cnt))
            return -1;
        for(int i=max_b;i>=0;i--)
            if(k&(1LL<<i))
                ans ^= p[i];
        return ans;
    }

總的模板:

#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define LL long long


using namespace std;



struct LineBasis
{
    LL b[66];
    LL p[66];
    int cnt;
    int max_b = 62;
    LineBasis()
    {
        memset(b,0,sizeof(b));
        memset(p,0,sizeof(p));
        cnt = 0;
    }
    bool Insert(LL val)
    {
        for(int i=max_b;i>=0;i--)
        {
            if((1LL<<i)&val)
            {
                if(b[i] == 0)
                {
                    b[i] = val;
                    break;
                }
                val ^= b[i];
            }
        }
        if(val > 0)
            cnt++;
        return val > 0;
    }
    LineBasis Merge(LineBasis n1,LineBasis n2)
    {
        LineBasis ret = n1;
        for(int i=0;i<=max_b;i++)
            if(n2.b[i])
                ret.Insert(n2.b[i]);
        return ret;
    }
    LL QueryMax(LL x)
    {
        LL ans = x;
        for(int i=max_b;i>=0;i--)
            if((ans^b[i]) > ans)
                ans ^= b[i];
        return ans;
    }
    LL QueryMin()
    {
        for(int i=0;i<=max_b;i++)
            if(b[i])
                return b[i];
        return 0;
    }
    void rebuild()
    {
        for(int i=max_b;i>=0;i--)
            for(int j=i-1;j>=0;j--)
                if(b[i]&(1LL<<j))
                    b[i] ^= b[j];
        cnt = 0;
        for(int i=0;i<=max_b;i++)
            if(b[i])
                p[cnt++] = b[i];
    }
    LL kthquery(LL k)
    {
        LL ans = 0;
        if(k>=(1LL<<cnt))
            return -1;
        for(int i=max_b;i>=0;i--)
            if(k&(1LL<<i))
                ans ^= p[i];
        return ans;
    }
};


int a[11000];
int main(void)
{
    int n,i,j;
    LineBasis s;
    scanf("%d",&n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        s.Insert(a[i]);
    }


    return 0;
}


/*
5
7 1 4 3 5
*/

如有錯誤,歡迎指出。