【数学】线性基

TODO:可能需要一个更快速复制、合并的版本,下面这个常数非常大。
注意这里的z表示是否可以选取若干个非空的线性基组合出0。对于某些问题来说如果插入的数字中带有0的话,需要特殊处理。

下面版本的线性基的插入过程实际上是两步:检查新出现的元素和线性基中所有向量xor之后是不是为0,虽然检查到一半就已经知道某一位不存在,但是还是得继续xor剩下的元素才行,要保证每一个纵列最多只有1个1!然后把所有的1消掉了之后再回头去把前面的行中,新加入的那一位消除掉。

但实际上没有必要保证每一个纵列最多只有1个1(行最简形)。下面这个只是普通的上阶梯型。关于这两种形状哪一种常数更大,貌似不太好说。根据CF1902的结果貌似是行最简形比上阶梯型速度要快。

struct Basis {

    static const int LEN = 20;

    ll row[LEN] = {};
    bool contain_zero_vector = false;

    bool Insert (ll x) {
        if (x == 0) {
            contain_zero_vector = true;
            return false;
        }
        for (int i = LEN - 1; i >= 0; --i) {
            if (x >> i & 1) {
                if (row[i]) {
                    x ^= row[i];
                    continue;
                }
                row[i] = x;
                return true;
            }
        }
        contain_zero_vector = true;
        return false;
    }

    bool Query (ll x) {
        if (x == 0) {
            return contain_zero_vector;
        }
        for (int i = LEN - 1; i >= 0; --i) {
            if (x >> i & 1) {
                if (row[i]) {
                    x ^= row[i];
                    continue;
                }
                return false;
            }
        }
        return x == 0;
    }

};

关于线性基能组合出的最大值和最小值。都需要考虑能不能组合出0,还有空集合的特殊情况,当然这种一般是***钻问题才会问的。下面的文章和模板有对应的解释。行最简形的线性基拥有很多很方便的性质。

类似下面这样,横线代表的是完全空行,注意到最高位的1都在对角线上,同时如果对角线上有1的话,该纵列没有其他的1。

1 0 0 0 0 1 0 1
0 1 0 0 0 0 0 1
- - - - - - - -
0 0 0 1 0 0 0 0
0 0 0 0 1 0 0 1
- - - - - - - -
0 0 0 0 0 0 1 0
- - - - - - - -

取出最小值的证明:
如果能表示0,那就是0。否则从最后一行开始看,最后一行只可能有最低位1。如果有,则是1。否则,至少为2。移动到上一行。由于是行最简形,所以每一行非对角线的1其实是没办法移除的,除非同时引入一个高位的1。

取出最大值的证明:
有数字的第一行最大,然后继续,如果某一行还有数字,由于是行最简形,所以其他行这一列没有数字。目前也不会有,放心贪心。

线性基能表示出的第k小值:
同样,要小心0能不能被表示。由于每个线性基选或者不选对应一个新的数字,对应的值为对角线上有没有这个1,至于右边的1其实是附赠的。那么显然某个基底不选就是小的那一侧,选了就是大的那一侧。可以从最高位入手开始二分,也可以类似下文这样,从最低位反过来找,每去除一个基点,能表示的数字都是少了一半,如果去掉的是最小的基底,那么相当于把把从高到低的树每一对叶子剪掉其中一半,如果k的最低位是1,则是选右半叶子,否则是选左半叶子,然后把问题的规模缩减到其父亲。感觉不如二分的思路容易想,二分的思路是,当前线性基能表示2^n个数字,如果k超过这个表示范围的一半,则要选当前的基底(走入右半分支),否则选左半分支。

线性基能表示多少个数:
要注意能不能表示0,同时0算不算合法的数。就是2^n,n是基底的数量,每个线性基选或者不选对应一个新的数字,全部不选也是一个数字。(要注意能不能全部不选)

这个32位版本的稍微好一点。

struct LB {

    static const int LEN = 20;

    int n, z;
    int d[LEN];

    LB() {
        Init();
    }

    void Init() {
        n = 0;
        z = 0;
        for (int i = 0; i < LEN; ++i) {
            d[i] = 0;
        }
    }

    void Init (const LB& B) {
        n = B.n;
        z = B.z;
        for (int i = 0; i < LEN; ++i) {
            d[i] = B.d[i];
        }
    }

    bool Insert (ll x) {
        if (x == 0) {
            z = 1;
            return false;
        }
        for (int i = LEN - 1; i >= 0; --i) {
            if (x >> i & 1) {
                if (d[i]) {
                    x ^= d[i];
                    continue;
                }
                ++n;
                d[i] = x;
                for (int j = 0; j < i; ++j) {
                    if (d[i] >> j & 1)
                        d[i] ^= d[j];
                }
                for (int j = i + 1; j < LEN; ++j) {
                    if (d[j] >> i & 1)
                        d[j] ^= d[i];
                }
                return true;
            }
        }
        z = 1;
        return false;
    }

    bool Query (ll x) {
        if (x == 0)
            return z == 1;
        for (int i = LEN - 1; i >= 0; --i) {
            if (x >> i & 1) {
                if (d[i]) {
                    x ^= d[i];
                    continue;
                }
                return false;
            }
        }
        return x == 0;
    }

    // Maximal
    ll Max() {
        if (!n)
            return z ? 0 : -1;
        ll res = 0;
        for (int i = LEN - 1; i >= 0; --i)
            res ^= d[i];
        return res;
    }

    // Minimal
    ll Min() {
        if (!n)
            return z ? 0 : -1;
        for (int i = 0; i < LEN; ++i) {
            if (d[i])
                return d[i];
        }
    }

    ll KthMin (ll k) {
        if (z)
            --k;
        ll res = 0;
        for (int i = 0; i < LEN; ++i) {
            if (d[i]) {
                if (k & 1)
                    res ^= d[i];
                k >>= 1;
            }
        }
        return k ? -1 : res;
    }

    ll Size() {
        return (1LL << n) - 1 + z;
    }

    void Show (int sl = LEN) {
        printf ("n=%d z=%d\n", n, z);
        for (int i = LEN - 1; i >= 0; --i) {
            if (d[i]) {
                printf ("d[%02d]=", i);
                for (int j = sl - 1; j >= 0; --j)
                    printf ("%d", d[i] >> j & 1);
                printf ("\n");
            }
        }
        printf ("\n");
    }

    void MergeFrom (const LB& B) {
        for (int i = 0; i < LEN; ++i) {
            Insert (B.d[i]);
        }
    }

    void MergeFrom (const LB& A, const LB& B) {
        if (A.n >= B.n) {
            Init (A);
            MergeFrom (B);
        } else {
            Init (B);
            MergeFrom (A);
        }
    }

};

这是一个看起来很像Gauss-Jordan消元法里面的形式(行最简形矩阵)的线性基,和其他人的Gauss消元法里面的形式(上三角形矩阵)看起来并不一样

d[i] 表示掌管二进制第i位(1LL<<i)的这个线性基。

struct LB {

    static const int LEN = 60;

    int n, z;
    ll d[LEN];

    void Init() {
        n = 0;
        z = 0;
        ms(d);
    }

    bool Insert(ll x) {
        for(int i = LEN - 1; i >= 0; --i) {
            if(x >> i & 1) {
                if(d[i]) {
                    x ^= d[i];
                    continue;
                }
                ++n;
                d[i] = x;
                for(int j = 0; j < i; ++j) {
                    if(d[i] >> j & 1)
                        d[i] ^= d[j];
                }
                for(int j = i + 1; j < LEN; ++j) {
                    if(d[j] >> i & 1)
                        d[j] ^= d[i];
                }
                return true;
            }
        }
        z = 1;
        return false;
    }

    bool Query(ll x) {
        if(x == 0)
            return z == 1;
        for(int i = LEN - 1; i >= 0; --i) {
            if(x >> i & 1) {
                if(d[i]) {
                    x ^= d[i];
                    continue;
                }
                return false;
            }
        }
        return x == 0;
    }

    // Maximal
    ll Max() {
        if(!n)
            return z ? 0 : -1;
        ll res = 0;
        for(int i = LEN - 1; i >= 0; --i)
            res ^= d[i];
        return res;
    }

    // Minimal
    ll Min() {
        if(!n)
            return z ? 0 : -1;
        for(int i = 0; i < LEN; ++i) {
            if(d[i])
                return d[i];
        }
    }

    ll KthMin(ll k) {
        if(z)
            --k;
        ll res = 0;
        for(int i = 0; i < LEN; ++i) {
            if(d[i]) {
                if(k & 1)
                    res ^= d[i];
                k >>= 1;
            }
        }
        return k ? -1 : res;
    }

    ll Size() {
        return (1LL << n) - 1 + z;
    }

    void Show(int sl = LEN) {
        printf("n=%d z=%d\n", n, z);
        for(int i = LEN - 1; i >= 0; --i) {
            if(d[i]) {
                printf("d[%02d]=", i);
                for(int j = sl - 1; j >= 0; --j)
                    printf("%d", d[i] >> j & 1);
                printf("\n");
            }
        }
        printf("\n");
    }

} lb;

n:线性基中非零值有多少个
z:是否可以组成0

设值域为 \(A\)

尝试往线性基中插入一个x:

问题就是x是否能被当前的线性基中的数字线性表示。
所以对于x的每个非0的位,都去找对应的d[i]和他异或,注意这样可能会引入新的位,所以规定从高到低异或就可以每次消去一位。当其可以被线性基中的数字表示时,那么再加上这个数本身就可以表示0了,就置z标记为1。否则要把这个剩余的数字插入到对应的d[i]中,然后用比它低位的d[j]消去d[i]中带有d[j]的位,再用d[i]消去比它高位的d[j]中的带有d[i]的位。(注意顺序,要先确保比i低的被掌管的位已经被消为0)。

最大值:

当不存在非零值时,判断是否可以组成0,若0都没有,那么就是空集。否则,由于是行最简形,所以就所有的d[i]都异或起来。

最小值:

当不存在非零值时,判断是否可以组成0,若0都没有,那么就是空集。判断是否能够组成0,有0就是0,否则输出最小的那个基底。

第k小值:

判断是否能够组成0,有0的话0就是最小值,--k。然后在非零的里面找第k小的。
然后从最小的基底开始往高位扫,每次k的最低位是奇数,就选中最小的基底,否则就不选。若最后k没有被消除完,则说明非0的值不足k个。
http://acm.hdu.edu.cn/showproblem.php?pid=3949

合并:
(改变d[i]的定义后)把规模小的那个线性基插入到大的那个,或者都直接暴力扫一遍。

Insert x=41
n=1 z=0
d[05]=000000101001

Insert x=35
n=2 z=0
d[05]=000000100011
d[03]=000000001010

Insert x=190
n=3 z=0
d[07]=000010010111
d[05]=000000100011
d[03]=000000001010

Insert x=1924
n=4 z=0
d[10]=011100010011
d[07]=000010010111
d[05]=000000100011
d[03]=000000001010

Insert x=737
n=5 z=0
d[10]=010101000110
d[09]=001001010101
d[07]=000010010111
d[05]=000000100011
d[03]=000000001010

Insert x=1388
n=6 z=0
d[10]=010101000101
d[09]=001001010101
d[07]=000010010100
d[05]=000000100000
d[03]=000000001001
d[01]=000000000011

Insert x=1238
n=7 z=0
d[10]=010001000001
d[09]=001001010101
d[08]=000100000100
d[07]=000010010100
d[05]=000000100000
d[03]=000000001001
d[01]=000000000011

Insert x=686
n=8 z=0
d[10]=010000000100
d[09]=001000010000
d[08]=000100000100
d[07]=000010010100
d[06]=000001000101
d[05]=000000100000
d[03]=000000001001
d[01]=000000000011

好好用哦。

提供一个用vector的实现:

struct LB {

    static const int LEN = 60;

    int z;
    vector<ll> d;

    void Init() {
        z = 0;
        d.clear();
    }

    bool Insert(ll x) {
        for(ll &i : d)
            cmin(x, x ^ i);
        if(x) {
            for(ll &i : d)
                cmin(i, i ^ x);
            d.eb(x);
            sort(all(d));
            return true;
        }
        z = 1;
        return false;
    }

    bool Query(ll x) {
        if(x == 0)
            return z == 1;
        for(ll &i : d)
            cmin(x, x ^ i);
        return x == 0;
    }

    // Maximal
    ll Max() {
        if(!d.size())
            return z ? 0 : -1;
        ll res = 0;
        for(ll &i : d)
            res ^= i;
        return res;
    }

    // Minimal
    ll Min() {
        if(!d.size())
            return z ? 0 : -1;
        return d[0];
    }

    ll KthMin(ll k) {
        if(z)
            --k;
        ll res = 0;
        for(ll &i : d) {
            if(k & 1)
                res ^= i;
            k >>= 1;
        }
        return k ? -1 : res;
    }

    ll Size() {
        return (1LL << d.size()) - 1 + z;
    }

    void Show(int sl = LEN) {
        printf("n=%u z=%d\n", d.size(), z);
        for(ll &i : d) {
            printf("d=", i);
            for(int j = sl - 1; j >= 0; --j)
                printf("%d", i >> j & 1);
            printf("\n");
        }
        printf("\n");
    }

} lb;

合并线性基的时候记得顺便把z标记也合并。

当可以离线查询时,可以维护[i,R]的所有线性基,当++R时从右往左扫描所有“当前后缀线性基”并尝试插入,因为每个线性基最多被插入成功log次,当插入失败时说明这个位置的值可以替代当前R的值,更前面的也会插入失败。

前缀线性基:给一个序列,每次往末尾加一个数或者查询LR的最大值,强制在线。
维护若干个[1,i]的线性基,线性基里面每个基底最后被插入的pos(当不存在这个基底,直接插入并设pos为当前pos,当存在这个基底时,假如当前pos>基底的pos,说明现在这一位需要换成最新的(因为是最大值),则交换插入的x和当前基底(然后把交换后的基底继续往后看看能不能插回去)),当查询[L,R]时,找[1,R]的线性基,然后找pos>=L的基底组合。

struct LB {

    static const int LEN = 60;

    int n;
    ll d[LEN];
    int pos[LEN];

    void Init() {
        n = 0;
        ms(d);
        ms(pos);
    }

    void Insert(ll x, int xpos) {
        for (int i = LEN - 1; i >= 0; --i) {
            if (x >> i & 1) {
                if (!d[i]) {
                    d[i] = x;
                    pos[i] = xpos;
                    break;
                } else {
                    if (xpos > pos[i]) {
                        swap(x, d[i]);
                        swap(xpos, pos[i]);
                    }
                    x ^= d[i];
                }
            }
        }
    }

    // Maximal
    ll Max() {
        ll res = 0;
        for (int i = LEN - 1; i >= 0; --i)
            res ^= d[i];
        return res;
    }

} lb;
posted @ 2021-02-11 09:46  purinliang  阅读(173)  评论(0编辑  收藏  举报