算法笔记:线性基详解

线性基是一个听起来很高级但实际上还蛮简单的算法,本质就类似于线性代数中的基向量,是一个整数集合的一组线性无关的数字。说是算法,我觉得其更偏向是一种思想。

例如一组向量:

v1={1, 0, 0}

v2={0, 1, 0}

v2={0, 0, 1}

三者中任意一个都无法用另外两者表示出来,因此它们线性无关。

举个线性基的例子(以下为二进制表示):

对一个整数集合{101, 100, 010, 011, 001}求线性基得到

{101, 010, 001}(这也是一组数不是向量啊

线性基三者中任意一个都无法通过另外两者异或得到,且原集合中的任意一个数都可以通过线性基中的某些数异或得到。

求线性基的过程如下:

ll lb[61];//线性基,大小取决于数字值域
int inse(ll x)//将x插入线性基,插入成功返回1,否则返回0
{
    //每次找到x的最高位的1
    //若线性基这个位置还没有数字,则把x插入这里
    //否则用线性基中这个位置的数字异或x,继续找x最高位的1
    for (int i=60; i>=0; i--)//i的初值取决于数字的值域
    {
        if (!(x&(1ll<<i))) continue;
        if (!lb[i]) {lb[i]=x; return 1;}
        x^=lb[i];
    }
    return 0;
    //可以看出,线性基中位置在i的数字的二进制第i位(从右往左数)一定是1
    //更大的二进制位置一定是零,这样就保证了线性基中的数字都是线性无关的
}

ll num[101];//整数集合
for (int i=1; i<=100; i++)
    inse(num[i]);//将整数集合中的数字逐一插入线性基

线性基的一般是用来判断是否可以从一个整数集合中选取一个子集,子集中的整数异或得到某一特定数字。(用向量来比喻就是给你一些向量,是否能够用这些向量表示出某一特定向量)。

一般来说,线性基有三大性质:

1、原集合里面的任意一个数都可以由线性基里面的一些数异或得到,可以通过原集合中某些数异或出来的值,也可以通过线性基中某些数异或得到

2、线性基中任意一些数字异或起来不为0(补充:若一个数字K没有成功插入线性基,则说明线性基中的某些数字可以异或得到K

3、线性基的任意两个不同的子集,子集中所有数字异或起来的结果不同

4、线性基是满足以上性质的最小集合

关于线性基的一些基本操作,还有如何支持删除(之前插入的数字),这个大佬的博客里写的非常清楚:

线性基详解———Hypoc_


放一些例题和题解(难度大概是递增的

P3857 [TJOI2008]彩灯

线性基第一、三条性质的运用,对这个集合求线性基,结果为2(线性基中数字数量)

#include <bits/stdc++.h>
#define maxn 501000
using namespace std;
typedef long long ll;

ll m, n, lb[61], a[maxn], res=1;
string s;

int inse(ll x)
{
    for (int i=60; i>=0; i--)
    {
        if (!(x&(1ll<<i))) continue;
        if (!lb[i]) {lb[i]=x; return 1;}
        x^=lb[i];
    }
    return 0;
}

int main()
{
    cin>>m>>n;
    for (ll i=1; i<=n; i++)
    {
        cin>>s;
        for (int j=m; j>=1; j--)
            if (s[j-1]=='O') a[i]|=1ll<<(m-j);
    }
    for (ll i=1; i<=n; i++) inse(a[i]);
    for (int i=60; i>=0; i--)
        if (lb[i]) res=res*2%2008;
    cout<<res;
    return 0;
}

P3265 [JLOI2015]装备购买

实数情况的线性基,用类似高斯消元的方法操作一下,然后过程中贪心的选价格最小的。

ps:注意eps的设置要大一点,否则由于计算精度不太够,判0太严格会wa掉

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
const long double eps=1e-5;

int n, m, vis[510], cost[510], ans_, ans;
long double v[510][510];

int main()
{
    cin>>n>>m;
    for (int i=1; i<=n; i++)
        for (int j=1; j<=m; j++)
            cin>>v[i][j];
    for (int i=1; i<=n; i++) cin>>cost[i];
    for (int i=1; i<=m; i++)
    {
        int mic=inf, mic_=0;
        for (int j=1; j<=n; j++)
            if (!vis[j] && fabs(v[j][i])>eps && cost[j]<mic)
                mic=cost[j], mic_=j;
        if (!mic_) continue;
        vis[mic_]=1, ans+=mic, ans_++;
        for (int j=1; j<=n; j++)
        {
            if (vis[j]) continue;
            long double k=v[j][i]/v[mic_][i];
            for (int l=i; l<=m; l++)
                v[j][l]-=k*v[mic_][l];
        }
    }
    cout<<ans_<<" "<<ans;
    return 0;
}

P4570 [BJWC2011]元素

第二条性质的运用。先按魔力值降序排序,然后按顺序尝试将其序号插入线性基,插入成功则计入答案,插入失败则说明加入这个矿石后会和之前选取的某些矿石异或得零,就必须得丢弃。

#include <bits/stdc++.h>
#define maxn 50100
using namespace std;
typedef long long ll;

struct item
{
    ll id, v;
}a[maxn];
ll n, lb[100], ans;

ll check(ll x)
{
    ll buf=0;
    for (ll i=61; i>=0; i--)
        if ((buf>>i)!=(x>>i)) buf^=lb[i];
    return !(x==buf);
}
void ins(ll x)
{
    for (ll i=61; i>=0; i--)
    {
        if (!(x&(1ll<<i))) continue;
        if (!lb[i]) {lb[i]=x; break;}
        x^=lb[i];
    }
}

int main()
{
    cin>>n;
    for (ll i=1; i<=n; i++)
        cin>>a[i].id>>a[i].v;
    sort(a+1, a+1+n, [](item x, item y){return x.v>y.v;});
    for (ll i=1; i<=n; i++)
        if (check(a[i].id)) ans+=a[i].v, ins(a[i].id);
    cout<<ans;
    return 0;
}

P4301 [CQOI2013]新Nim游戏

本质跟上面那题基本一样。

根据博弈论SG函数的知识,我们知道要让先手保证获胜,就要保证两个人都执行完特殊操作后,剩下的火柴堆异或起来不为0。也就是要保证先手进行完特殊操作后,剩下的火柴堆没有异或为零的组合(否则后手者只要把组合之外的火柴都拿掉就好了)

所以要让先手必胜,我们只要把会导致 (某些火柴堆异或为0 )的那些火柴堆拿掉就好了。为了让拿走的火柴数最小,先按火柴数降序排列,然后逐个尝试插入线性基,把插入失败的火柴堆取走就行了。

#include <bits/stdc++.h>
#define maxn 50100
using namespace std;
typedef long long ll;

ll n, lb[100], num[maxn], ans;

int ins(ll x)
{
    for (ll i=61; i>=0; i--)
    {
        if (!(x&(1ll<<i))) continue;
        if (!lb[i]) {lb[i]=x; return 1;}
        x^=lb[i];
    }
    return 0;
}

int main()
{
    cin>>n;
    for (ll i=1; i<=n; i++)
        cin>>num[i];
    sort(num+1, num+1+n, [](ll a, ll b){return a>b;});
    for (int i=1; i<=n; i++)
        if (!ins(num[i])) ans+=num[i];
    cout<<ans;
    return 0;
}

P4151 [WC2011]最大XOR和路径

有点巧妙的题目。

每条边和点可以走多次,稍加观察可以发现,假设我们本来是沿着某一路径从1走到n,在中途其实可以走其它边,然后走到某一个环,转完那个环再原路返回,此时答案只异或上了那个环上的所有值,前往那个环的路上所有值都被异或了两次所以等于没异或。

因此做法就是,找到一条1到n的路径,答案就是这条路径上的值异或起来加入线性基,再把所有环的异或加入线性基,即可构造出最大的答案。

1到n的路径可能有多个,其实随便用哪一个都一样的。因为如果存在多个1到n的路径,它们本身也是成环的,在进行线性基的过程中会自然被替换成最优的选择。

还有一个问题:要找出所有环的话复杂度似乎至少得n*m?反正肯定会超时。其实只要随便从一个点用dfs找环就行了,虽然找的不全,但神奇的是它确实不影响答案的正确性。(个人认为是因为没找到的环其实都是可以通过找到的环异或得到

#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define maxn 50100
using namespace std;
typedef long long ll;
const ll mod=998244353;

vector<pair<ll, ll> > edge[maxn];
ll n, m, lb[100], res, vis[maxn], buf[maxn];

void add(ll x)
{
    for (ll i=61; i>=0; i--)
    {
        if (!(x&(1ll<<i))) continue;
        if (!lb[i]) {lb[i]=x; break;}
        x^=lb[i];
    }
}

void dfs(ll now, ll fa, ll xr)
{
    vis[now]=1, buf[now]=xr;
    if (now==n) res=xr;
    for (auto to: edge[now])
    {
        if (to.first==fa) continue;
        if (vis[to.first]) add(xr^to.second^buf[to.first]);
        else dfs(to.first, now, xr^to.second);
    }
}

int main()
{
    cin>>n>>m;
    for (ll i=1, u, v, w; i<=m; i++)
    {
        cin>>u>>v>>w;
        edge[u].push_back(make_pair(v, w));
        edge[v].push_back(make_pair(u, w));
    }
    dfs(1, 0, 0);
    for (ll i=61; i>=0; i--)
        if (!(res&(1ll<<i)) && lb[i])
            res^=lb[i];
    cout<<res;
}

P3292 [SCOI2016]幸运数字

这几题里面唯一一道代码长一点的题。据说可以用点分治搞成O(nlog2n),但是点分治不怎么熟就还没研究那个做法。

每次要对一条路径上的所有值求线性基。可以把这条路径分成两半,u->lca(u, v)和v->lca(u,v),可以对两段分别求线性基再合并起来。求一个点到其某个祖先的线性基可以利用倍增的思想,对不超过logn个线性基进行合并得到。

整体来说非常像倍增求lca(我就是按照那个板子改的),复杂度单次合并是O(log2n)的,每次询问合并logn次,所以复杂度应该是O(mlog3n)

#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define maxn 20100
using namespace std;
typedef long long ll;

const ll mod=998244353;
const long double eps=1e-5;

int n, q, lg[maxn], fa[maxn][20], deep[maxn];
ll lb[maxn][20][61], buf1[61], buf2[61], num[maxn];
vector<int> edge[maxn];

void inse(ll x, ll *res)
{
    for (ll i=60; i>=0; i--)
    {
        if (!(x&(1ll<<i))) continue;
        if (!res[i]) {res[i]=x; break;}
        x^=res[i];
    }
}

void merg(ll *a1, ll *a2, ll *res)
{
    for (ll i=0; i<=60; i++)
        res[i]=a1[i];
    for (ll i=60; i>=0; i--)
    {
        ll temp=a2[i];
        for (ll j=60; j>=0; j--)
        {
            if (!(temp&(1ll<<j))) continue;
            if (!res[j]) {res[j]=temp; break;}
            temp^=res[j];
        }
    }
}

void dfs(ll now, ll f)
{
    deep[now]=deep[f]+1;
    fa[now][0]=f;
    inse(num[now], lb[now][0]);
    inse(num[f], lb[now][0]);
    for (ll i=1; (1ll<<i)<=deep[now]; i++)
    {
        fa[now][i]=fa[fa[now][i-1]][i-1];
        merg(lb[now][i-1], lb[fa[now][i-1]][i-1], lb[now][i]);
    }
    for (auto i: edge[now])
        if (i!=f) dfs(i, now);
}

ll lca(ll x, ll y)
{
    memset(buf1, 0, sizeof(buf1));
    memset(buf2, 0, sizeof(buf2));
    if (deep[x]<deep[y]) swap(x, y);
    while (deep[x]!=deep[y])
    {
        merg(buf1, lb[x][lg[deep[x]-deep[y]]-1], buf1);

        x=fa[x][lg[deep[x]-deep[y]]-1];
    }
    if (x==y) return x;
    for (ll i=lg[deep[x]]; i>=0; i--)
        if (fa[x][i]!=fa[y][i])
        {
            merg(buf1, lb[x][i], buf1);
            merg(buf2, lb[y][i], buf2);
            x=fa[x][i], y=fa[y][i];
        }
    merg(buf1, buf2, buf1);
    inse(num[fa[x][0]], buf1);
    inse(num[x], buf1);
    inse(num[y], buf1);
    return fa[x][0];
}

int main()
{

    ios::sync_with_stdio(0); cin.tie(0);
    cin>>n>>q;
    for (ll i=1; i<=n; i++) cin>>num[i];
    for (ll i=1; i<=n; i++)
        lg[i]=lg[i-1]+(i==(1ll<<lg[i-1]));
    for (ll i=1, u, v; i<n; i++)
    {
        cin>>u>>v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    dfs(1, 0);
    for (ll i=1, u, v; i<=q; i++)
    {
        cin>>u>>v;
        if (u==v) {cout<<num[u]<<"\n"; continue;}
        lca(u, v);
        ll temp=0;
        for (ll j=60; j>=0; j--)
            if (!(temp&(1ll<<j))) temp^=buf1[j];
        cout<<temp<<"\n";
    }
}
posted @ 2020-04-04 00:25  opppppppp  阅读(460)  评论(0)    收藏  举报