高斯消元与线性基小结

以前就学了高斯消元,但是学得零零散散很混乱,有必要系统地总结一下了。

高斯消元

学习博客:https://www.cnblogs.com/candy99/p/6653743.html

模板题:洛谷P3389

#include<bits/stdc++.h>
using namespace std;
const int N=100+10;
const double eps=1e-8;
int n;
double c[N][N],b[N];

bool Gauss() {
    int r=0;
    for (int i=1;i<=n;i++) {
        for (int j=i;j<=n;j++)
            if (fabs(c[j][i])>eps) {
                for (int k=1;k<=n;k++) swap(c[i][k],c[j][k]);
                swap(b[i],b[j]);
            }
        if (fabs(c[i][i])<eps) continue;  //消元结束    
        r++;  //矩阵的秩 
        for (int j=1;j<=n;j++) {
            if (i==j) continue;
            double rate=c[j][i]/c[i][i];
            for (int k=i;k<=n;k++) c[j][k]-=c[i][k]*rate;
            b[j]-=b[i]*rate;
        }
    }
    if (r<n) return 0;  //有多解 
    return 1;  //唯一解 
}

int main()
{
    cin>>n;
    for (int i=1;i<=n;i++) {
        for (int j=1;j<=n;j++) scanf("%lf",&c[i][j]);
        scanf("%lf",&b[i]);
    }    
    if (!Gauss()) puts("No Solution");
    else for (int i=1;i<=n;i++) printf("%.2lf
",b[i]/c[i][i]);    
    return 0;
} 
View Code

POJ-2947

高斯消元解同余方程组模板题,写了可以当高斯消元全情况判断的模板。注意判断多解,无解,唯一解的情况。另外注意输入的时候要模7。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int N=300+10;
int n,m;
char s[N];
int c[N][N],b[N];

int power(int x,int p) {
    int ret=1;
    for (;p;p>>=1) {
        if (p&1) ret=(ret*x)%7;
        x=(x*x)%7;
    }
    return ret;
}

int inv(int x) { return power(x,7-2); }

void Gauss() {
    int r=0;
    for (int i=1;i<=n;i++) {
        int j=r+1;
        while (j<=m && c[j][i]==0) j++;  //从下面方程找一个第i位不为0的 
        if (j==m+1) continue;  //不存在第i位不为0的方程 
        r++;  //矩阵的秩
        for (int k=1;k<=n;k++) swap(c[r][k],c[j][k]);  //存在第i位不为0的方程,交换上去 
        swap(b[r],b[j]);
        
        for (int j=1;j<=m;j++) {  //以r方程回代m个方程 
            if (r==j) continue;
            int rate=c[j][i]%7*inv(c[r][i])%7;
            for (int k=i;k<=n;k++) c[j][k]-=c[r][k]*rate,c[j][k]%=7,c[j][k]=(c[j][k]+7)%7;
            b[j]-=b[r]*rate,b[j]%=7,b[j]=(b[j]+7)%7;
        }    
    }
    
    for (int i=1;i<=m;i++) if (b[i]) {  //判断无解情况 
        bool ok=0;
        for (int j=1;j<=n;j++) if (c[i][j]) { ok=1; break; }
        if (!ok) { puts("Inconsistent data."); return; }
    }
    
    if (r<n) { puts("Multiple solutions."); return; }  //有解但多解
    
    for (int i=1;i<=n;i++) b[i]=b[i]%7*inv(c[i][i])%7;  //唯一解求解 
    for (int i=1;i<=n;i++) printf("%d ",b[i]<3 ? b[i]+7 : b[i]); puts(""); 
}

int num(char *s) {
    if (s[0]=='M') return 1;
    if (s[0]=='W') return 3;
    if (s[0]=='F') return 5;
    if (s[0]=='T') if (s[1]=='U') return 2; else return 4;
    if (s[0]=='S') if (s[1]=='A') return 6; else return 7;
}

//POJ-2947 高斯消元解同余方程组
int main()
{
    while (scanf("%d%d",&n,&m)==2 && n) {
        for (int i=1;i<=m;i++) {
            memset(c[i],0,sizeof(c[i])); b[i]=0;  //清空方程数组 
            int t,x,y; char s1[10],s2[10];
            scanf("%d%s%s",&t,s1,s2);
            x=num(s1); y=num(s2); 
            for (int j=1;j<=t;j++) {  //建立同余方程 
                int tp; scanf("%d",&tp);
                c[i][tp]++;
            }
            b[i]=((y-x+1)%7+7)%7;
            for (int j=1;j<=n;j++) c[i][j]%=7;
        }
        Gauss();
    }
    return 0;
}
View Code

POJ-2065

跟POJ2947差不多甚至还要更简单些,注意除法变成逆元随时取模即可。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int N=100+10;
int n,P;
char s[N];
int c[N][N],b[N];

int power(int x,int p) {
    int ret=1;
    for (;p;p>>=1) {
        if (p&1) ret=(ret*x)%P;
        x=(x*x)%P;
    }
    return ret;
}

int inv(int x) { return power(x,P-2); }

void Gauss() {
    for (int i=1;i<=n;i++) {
        for (int j=i;j<=n;j++)
            if (abs(c[j][i])>0) {
                for (int k=1;k<=n;k++) swap(c[i][k],c[j][k]);
                swap(b[i],b[j]);
            }
        for (int j=1;j<=n;j++) {
            if (i==j) continue;
            double rate=c[j][i]%P*inv(c[i][i])%P;
            for (int k=i;k<=n;k++) c[j][k]-=c[i][k]*rate,c[j][k]%=P,c[j][k]=(c[j][k]+P)%P;
            b[j]-=b[i]*rate,b[j]%=P,b[j]=(b[j]+P)%P;
        }
    }
}

void equation() {
    for (int i=1;i<=n;i++) {
        for (int j=1;j<=n;j++) c[i][j]=power(i,j-1);
        b[i]=(s[i]=='*') ? 0 : (s[i]-'a'+1);
    }
}

int main()
{
    int T; cin>>T;
    while (T--) {
        scanf("%d",&P);
        scanf("%s",s+1);
        n=strlen(s+1);
        equation();
        Gauss();
        for (int i=1;i<=n;i++) printf("%d ",b[i]%P*inv(c[i][i])%P);
        printf("
");
    }
    return 0;
} 
View Code

BZOJ-1770

线性基

 推荐博客:https://blog.sengxian.com/algorithms/linear-basis

https://blog.csdn.net/litble/article/details/78820648

上面两位大佬讲得炒鸡好,线性基的内容也大抵如此了。

模板题:HDU-3949

求集合子集异或值第K小(去掉重复数)。思路和代码都是抄上面大佬的,当作模板了。

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
typedef long long LL;
int n,m;
bool have0; 

LL cnt,a[N],b[70],v[70];
void prepare() {  //求a[1-n]得线性基放到v数组 
    int r=0; memset(b,0,sizeof(b));
    for (int i=1;i<=n;i++) {
        for (int j=60;j>=0;j--) {
            if (!(a[i]&(1LL<<j))) continue;  //该位置为0直接跳过 
            if (b[j]) a[i]^=b[j];  //该位置已经有数 
            else {
                ++r; b[j]=a[i];  //插入成功 
                for (int k=j-1;k>=0;k--) if (b[k]&&((b[j]>>k)&1)) b[j]^=b[k]; 
                for (int k=j+1;k<=60;k++) if ((b[k]>>j)&1) b[k]^=b[j]; 
                break;  //同时为了维护一个对角矩阵,要先用下面的行消自己,再用自己消上面的行。 
            }
        }
    }
    have0=(r!=n);
    cnt=0; for (int i=0;i<=60;i++) if (b[i]) v[++cnt]=b[i];  //得到线性基 
}

LL query(LL k) {
    if (have0) k--;  //因为这种方法算的是非0第k小,所以存在0的话就得减一 
    if (k>=(1LL<<cnt)) return -1;  //这里要注意: 到这里已经排除0的情况了,那么线性基的数不能一个不取 
    LL ret=0;
    for (int i=1;i<=cnt;i++) if (k&(1LL<<(i-1))) ret^=v[i];  //按k的二进制表示取数 
    return ret;
}

int main()
{
    int T,kase=0; cin>>T;
    while (T--) {
        scanf("%d",&n);
        for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
        prepare();
        scanf("%d",&m);
        printf("Case #%d:
",++kase);
        for (int i=1;i<=m;i++) {
            int x; scanf("%d",&x);
            printf("%lld
",query(x));
        }
    }
    return 0;
}
View Code

BZOJ-2844

求集合某个子集异或值得名次(不去重复数)。我们先不考虑重不重复这个事,先想去掉重复得情况下,某个数x得名次是多少?网上大佬都是一笔带过,蒟蒻愣是一时半会没想明白。其实求名次这个操作就是求第K小值这个操作逆过来,但是我们要注意到因为线性基的性质,线性基异或成某个数的方案是唯一的,也就是说线性基异或出来的某个数x和异或得到x的组合是一一对应的,这就为我们做这个逆操作提供了方向。我们只要看看x到底由哪些线性基组合而成,然后按照位置计算权值累加起来即可。例如线性基是{1,4,8} 然后我们的x是9,我们发现9=1+8,x里面有第1位和第3位,那它的rank就是1<<0+1<<2=5。然后再考虑重复的个数即可,重复个数已经有很多证明了其实就是每个数会重复恰好2^n-cnt次(包括0),然后加1即可。

这里有一个思维的小细节:其实按照我上面说的计算rank的方法是从1开始算的,但是我们要求的其实是rank-1,但是因为我们还没算0,所以+1又恰好等于rank。例如上面的9的rank在所有不重复组合{0,1,4,5,8,9} 中排第6,(rank-1)*(2^n-cnt)+1就是答案了。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int MOD=10086;
typedef long long LL;
int n,x,ans;
bool have0; 

LL cnt,a[N],b[70],v[70];
void prepare() {  //求a[1-n]得线性基放到v数组 
    int r=0; memset(b,0,sizeof(b));
    for (int i=1;i<=n;i++) {
        for (int j=30;j>=0;j--) {
            if (!(a[i]&(1LL<<j))) continue;  //该位置为0直接跳过 
            if (b[j]) a[i]^=b[j];  //该位置已经有数 
            else {
                ++r; b[j]=a[i];  //插入成功 
                for (int k=j-1;k>=0;k--) if (b[k]&&((b[j]>>k)&1)) b[j]^=b[k]; 
                for (int k=j+1;k<=30;k++) if ((b[k]>>j)&1) b[k]^=b[j]; 
                break;  //同时为了维护一个对角矩阵,要先用下面的行消自己,再用自己消上面的行。 
            }
        }
    }
    have0=(r!=n);
    cnt=0; for (int i=0;i<=30;i++) if (b[i]) v[++cnt]=i;  //得到线性基 
}

int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
    prepare();

    scanf("%d",&x);
    for (int i=1;i<=cnt;i++) if (x&(1LL<<v[i])) ans=(ans+(1LL<<(i-1)))%MOD;
    for (int i=1;i<=n-cnt;i++) ans=(ans+ans)%MOD;
    ans=(ans+1)%MOD;
    cout<<ans<<endl;
    return 0;
}
View Code

BZOJ-2115

参考大佬题解:从1到n的任意一条路径上的异或和,一定可以表示为随意一条路径异或上若干环的异或和的值。所以把所有的环找出来,求出环的异或线性基,然后以随意一条路径从线性基高位开始贪心。

其实蒟蒻并没有完全理解。但是个人感觉其实并不是把所有的环都找出来了,只是用dfs把能找的都找出来了,但是基于线性基的特性(线性基的组合能表示所有的集合),其实所有环的异或都能表示出来,且根据上面路径=路径+环的结论,贪心就能得到最大值。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e4+10,M=1e5+10;
LL n,m,num,dis[N];

LL cnt=0,head[N],nxt[M<<1],to[M<<1],len[M<<1]; 
void add_edge(LL x,LL y,LL z) {
    nxt[++cnt]=head[x]; to[cnt]=y; len[cnt]=z; head[x]=cnt;
}

LL k,a[M<<2],b[70],v[70];
void prepare() {  //求a[1-n]得线性基放到v数组 
    memset(b,0,sizeof(b));
    for (int i=1;i<=num;i++) {
        for (int j=60;j>=0;j--) {
            if (!(a[i]&(1LL<<j))) continue;  //该位置为0直接跳过 
            if (b[j]) a[i]^=b[j];  //该位置已经有数 
            else {
                b[j]=a[i];  //插入成功 
                for (int k=j-1;k>=0;k--) if (b[k]&&((b[j]>>k)&1)) b[j]^=b[k]; 
                for (int k=j+1;k<=60;k++) if ((b[k]>>j)&1) b[k]^=b[j]; 
                break;  //同时为了维护一个对角矩阵,要先用下面的行消自己,再用自己消上面的行。 
            }
        }
    }
    k=0; for (int i=0;i<=60;i++) if (b[i]) v[++k]=b[i];  //得到线性基 
}

bool vis[N];
void dfs(int x) {
    vis[x]=1;
    for (int i=head[x];i;i=nxt[i]) {
        int y=to[i];
        if (!vis[y]) dis[y]=dis[x]^len[i],dfs(y);
        else a[++num]=dis[x]^len[i]^dis[y];  //返祖边:形成环 
    }
}

int main()
{
    cin>>n>>m;
    for (int i=1;i<=m;i++) {
        LL x,y,z; scanf("%lld%lld%lld",&x,&y,&z);
        add_edge(x,y,z); add_edge(y,x,z);
    }    
    dfs(1); prepare();
    LL ans=dis[n];
    for (int i=k;i;i--) if ((ans^v[i])>ans) ans=ans^v[i];
    cout<<ans<<endl;
    return 0;
} 
View Code

BZOJ-3811

原文地址:https://www.cnblogs.com/clno1/p/10905183.html