「算法笔记」线性基

推荐博客:Sengxian的博客
推荐博客:YveH的博客

基:在线性代数中,基(也称为基底)是描述、刻画向量空间的基本工具。向量空间的基是它的一个特殊的子集,基的元素称为基向量。向量空间中任意一个元素,都可以唯一地表示成基向量的线性组合。如果基中元素个数有限,就称向量空间为有限维向量空间,将元素的个数称作向量空间的维数。
同样的,线性基是一种特殊的基,它通常会在异或运算中出现,它的意义是:通过原集合S的某一个最小子集S1使得S1内元素相互异或得到的值域与原集合S相互异或得到的值域相同。
——百度百科

例题一:【JLOI 2015】装备购买

线性基模版题。按装备的价格排序,然后一边高斯消元,一边贪心的选装备即可。具体高斯消元过程见代码。

// This Template is Very Important
#include <cmath>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long double ldb;
const int maxn = 505;
const ldb eps = 1e-8;
int n, m, base[maxn];
struct weapon {
    ldb a[maxn];
    int v;
    bool operator<(const weapon &o) const{
        return v < o.v;
    }
} w[maxn];
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int k, j = 1; j <= m; j++) {
            scanf("%d", &k);
            w[i].a[j] = k;
        }
    }
    for (int i = 1; i <= n; i++) {
        scanf("%d", &w[i].v);
    }
    sort(w + 1, w + n + 1);
    int res = 0, ans = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (fabs(w[i].a[j]) > eps) {
                if (!base[j]) {
                    base[j] = i;
                    res++, ans += w[i].v;
                    break;
                } else {
                    ldb d = w[i].a[j] / w[base[j]].a[j];
                    for (int k = j; k <= m; k++) {
                        w[i].a[k] -= d * w[base[j]].a[k];
                    }
                }
            }
        }
    }
    printf("%d %d\n", res, ans);
    return 0;
}

例题二:【HDU 3949】XOR

异或空间线性基。具体求法见代码。

思路:先求出线性基,从基底中选出 m m 个数共有2m种选法,与 2m 2 m 种异或值一一对应。
于是,我们将询问的 k k 二进制分解,就对应了基的一种选法。不难用二进制思想证明。

细节:注意是否可以得到0的情况。如果能得到 0 0 ,就将k1二进制分解。

// This Template is Also Very Important
#include <cstdio>
#include <iostream>
using namespace std;
typedef unsigned long long ull;
const int maxn = 10005;
ull a[maxn];
int tasks, n, m, zero, t;
int main() {
    scanf("%d", &tasks);
    for (int task = 1; task <= tasks; task++) {
        printf("Case #%d:\n", task);
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%llu", a + i);
        }
        zero = 0, t = n;
        for (int i = 1; i <= n; i++) {
            for (int j = i + 1; j <= n; j++) {
                if (a[j] > a[i]) {
                    swap(a[i], a[j]);
                }
            }
            if (!a[i]) {
                zero = 1, t = i - 1;
                break;
            }
            for (int k = 63; ~k; k--) {
                if (a[i] >> k & 1) {
                    for (int j = 1; j <= n; j++) {
                        if (i != j && a[j] >> k & 1) {
                            a[j] ^= a[i];
                        }
                    }
                    break;
                }
            }
        }
        for (scanf("%d", &m); m--; ) {
            ull k, ans = 0;
            scanf("%llu", &k);
            k -= zero;
            if (k >= 1ull << t) {
                puts("-1");
                continue;
            }
            for (int i = t - 1; ~i; i--) {
                if (k >> i & 1) {
                    ans ^= a[t - i];
                }
            }
            printf("%llu\n", ans);
        }
    }
    return 0;
}

例题三:【BZOJ 2844】albus就是要第一个出场

用类似例题二的做法可以求出每个数的排名,但是我们还不知道每个异或值在数列中出现了几次。

定理:每个异或值出现了 2nm 2 n − m 次,其中 m m 为线性基的大小。

这样,我们就可以用快速幂算出答案了。

#include <cstdio>
#include <iostream>
using namespace std;
const int mod = 10086;
const int maxn = 100005;
int n, m, q, a[maxn], b[maxn];
int mpow(int x, int y) {
    int z = 1;
    for (; y; y >>= 1) {
        if (y & 1) {
            z = z * x % mod;
        }
        x = x * x % mod;
    }
    return z;
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", a + i);
    }
    scanf("%d", &q);
    m = n;
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            if (a[i] < a[j]) {
                swap(a[i], a[j]);
            }
        }
        if (a[i] == 0) {
            m = i - 1;
            break;
        }
        for (int k = 29; k >= 0; k--) {
            if (a[i] >> k & 1) {
                b[i] = k;
                for (int j = 1; j <= n; j++) {
                    if (i != j && a[j] >> k & 1) {
                        a[j] ^= a[i];
                    }
                }
                break;
            }
        }
    }
    int ans = 0;
    for (int i = m - 1; i >= 0; i--) {
//      printf("%d %d\n", b[m - i], 1 << i);
        if (q >> b[m - i] & 1) {
            ans += 1 << i;
        }
    }
    ans = (ans % mod * mpow(2, n - m) + 1) % mod;
    printf("%d\n", ans);
    return 0;
}

例题四:【WC 2011】Xor

这题将集合中的问题搬到了图上。我们发现一个性质:1 n n 的一条路径的异或值必定等于1 n n 的一条简单路径的异或值异或图中一些简单环的边权异或值。

接下来就好办了。找到图中的每一个环的异或值,再随便找一条1 n n 的简单路径,问题就变成了找到一个环的子集,使得它们的异或值异或1 n n 的那条简单路径的值最大。显然可以通过贪心来解决。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 50005;
const int maxm = 100005;
bool vis[maxn];
int n, m, t, cnt;
int tot, nxt[maxm], ter[maxm], lnk[maxn];
ll a[maxn + maxm * 2], sum[maxn], val[maxm];
void addedge(int u, int v, ll w) {
    ter[tot] = v;
    val[tot] = w;
    nxt[tot] = lnk[u];
    lnk[u] = tot++;
}
void dfs(int u, int p) {
    vis[u] = 1;
    ll w;
    for (int v, i = lnk[u]; ~i; i = nxt[i]) {
        v = ter[i], w = val[i];
        if (v == p) {
            continue;
        }
        if (vis[v]) {
            a[++cnt] = w ^ sum[u] ^ sum[v];
        } else {
            sum[v] = sum[u] ^ w;
            dfs(v, u);
        }
    }
}
int main() {
    memset(lnk, -1, sizeof(lnk));
    scanf("%d %d", &n, &m);
    ll w;
    for (int u, v, i = 1; i <= m; i++) {
        scanf("%d %d %lld", &u, &v, &w);
        addedge(u, v, w);
    }
    dfs(1, 0);
    t = cnt;
    for (int i = 1; i <= cnt; i++) {
        for (int j = i + 1; j <= cnt; j++) {
            if (a[i] < a[j]) {
                swap(a[i], a[j]);
            }
        }
        if (a[i] == 0) {
            t = i - 1;
            break;
        }
        for (int k = 59; k >= 0; k--) {
            if (a[i] >> k & 1) {
                for (int j = 1; j <= cnt; j++) {
                    if (i != j && a[j] >> k & 1) {
                        a[j] ^= a[i];
                    }
                }
                break;
            }
        }
    }
    ll ans = sum[n];
    for (int i = 1; i <= t; i++) {
        if ((ans ^ a[i]) > ans) {
            ans ^= a[i];
        }
    }
    printf("%lld\n", ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值