2024-9-10 高斯消元

说在前面

  • 一直听说高斯消元这个算法,但是由于自己对数学的抵触情绪,一直没有敢于来学习。直到看见了这道题和完这道题后的罪恶感让我下定了学习高斯消元的决心,学完感觉并没有想象中那样难,而且这种人机思维的转换十分有趣,值得一学

高斯消元

这是什么

  • 高斯消元,一种在约 O ( n 3 ) O(n^3) O(n3)的时间内求解n元一次方程组的算法,通常作为程序的一部分,可以与期望概率、位运算、dp等配合使用,可以用于求解受后继状态影响的dp方程

引入

  • 考虑我们解方程的步骤:(以二元一次方程为例)

    • 我们知道,普通人解决这个问题通常运用两种方法:加减消元和代入消元

    • 对于一个二元一次方程组:

    { x + 2 y = 3    ( 1 ) 4 x + 4 y = 5    ( 2 ) \begin{cases}x+2y=3 \ \ (1) \\4x+4y=5\ \ (2) \end{cases} {x+2y=3  (1)4x+4y=5  (2)

    • ( 2 ) − 2 × ( 1 ) (2)-2\times(1) (2)2×(1) x = − 1 2 x=-\frac{1}{2} x=21,将x带入y中,解得 y = 7 4 y=\frac{7}{4} y=47
  • 像这样,一个多元一次方程组就可以在进行多次加减消元和带入消元后被求解,高斯消元利用的就是这种思路

实现(以P3389 【模板】高斯消元法 为例)

  • 考虑把这种加减和代入的思路转换为计算机可以处理的方式

前置知识:矩阵的初等行变换

  • 用非零的数a乘以某一整行

  • 把一行的a倍加到另一行

  • 互换任意两行的位置

  • 利用这种变换可以实现加减消元
  1. 将方程组转化为矩阵
  • 我们发现,在解方程过程中真正关注的其实是方程系数,所以把方程的系数单独拿出来,形成一个矩阵,即系数矩阵A,满足 A i , j = 第 i 个方程中的第 j 个未知数的系数 A_{i,j}=第i个方程中的第j个未知数的系数 Ai,j=i个方程中的第j个未知数的系数(在每个方程中未知数顺序一样)

  • 再把右侧的常数拿出来,形成一个矩阵,称为常数矩阵B

  • 则整个方程组可表示为增广矩阵A|B,即矩阵A和B拼起来的矩阵:

[ 1 2 3 4 4 5 ] \left[\begin{array}{cc|c} 1&2&3\\ 4&4&5 \end{array}\right] [142435]
2. 对增广矩阵进行求解

  • 为了方便,我们让第i个方程保留第i个未知数,这样消元完成后的矩阵就是一个只有右上三角有数(普通高斯消元)或只有对角线有数的矩阵(高斯-约旦消元)

  • 普通高斯消元:

  • 枚举未知数i(1->n)进行消元(将矩阵通过矩阵初等变换变换为三角矩阵)

    主元:主元(pivot element),一种变元。指在消去过程中起主导作用的元素。
    主元就是在矩阵消去过程中,每列的要保留的非零元素,用它可以把该列其他消去
    在阶梯型矩阵中,主元就是每个非零行第一个非零元素就是主元

    • 首先,在整个矩阵中寻找第i个未知量的系数不为零的方程,然后将它们进行互换(如果找不到不为0的方程,则第i个位置数的值可以取任意值,该方程组有多解)

    • 在这一步中,为了减少误差,通常要找系数最大的一个作为主元

    • 接着枚举i之后的方程,把它们中的第i个未知数消掉

  • 消元完成后,对矩阵进行回带操作

    • 倒序枚举行,通过带入消元将后面仍然未被消掉的未知数消掉
inline void gauss(){
    for(int i=1;i<=n;i++){
        int maxx=i;
        for(int j=i+1;j<=n;j++){//寻找主元
            if(fabs(a[j][i])>fabs(a[maxx][i])) maxx=j;//记得加绝对值
        }
        if(fabs(a[maxx][i])<=eps){//记得加绝对值
            cout<<"No Solution";
            exit(0);
        }
        swap(a[maxx],a[i]);
        for(int j=i+1;j<=n;j++){//加减消元
            double tmp=a[j][i]/a[i][i];
            for(int k=1;k<=n+1;k++){
                a[j][k]-=tmp*a[i][k];
            }
        }
    }
    for(int i=n;i>=1;i--){//回带
        for(int j=i+1;j<=n;j++){
            a[i][n+1]-=a[i][j]*res[j];
        }
        res[i]=a[i][n+1]/a[i][i];
    }
    return;
}
  • 高斯-约旦消元: 在加减消元时对整个方程组都进行一次消元,可以避免回带操作, a i , n + 1 a i , i \frac{a_{i,n+1}}{a_{i,i}} ai,iai,n+1即为答案

    inline void gauss(){
        for(int i=1;i<=n;i++){
            int maxx=i;
            for(int j=i+1;j<=n;j++){//寻找主元
                if(fabs(a[j][i])>fabs(a[maxx][i])) maxx=j;//记得加绝对值
            }
            if(fabs(a[maxx][i])<=eps){//记得加绝对值
                cout<<"No Solution";
                exit(0);
            }
            swap(a[maxx],a[i]);
            for(int j=1;j<=n;j++){//加减消元
                if(i==j) continue;
                double tmp=a[j][i]/a[i][i];
                for(int k=1;k<=n+1;k++){
                    a[j][k]-=tmp*a[i][k];
                }
            }
        }
        return ;
    }
    
  1. 关于解的数量
  • 显而易见的是,当消元结束后,如果主元为0,则存在无解和有无数解的情况

  • 当常数项为0时,因为 0 × x = 0 0\times x=0 0×x=0恒成立,所以有无数解

  • 当常数项不为0时,无论 x x x取何值,等式都不会成立,所以方程无解

  • 在本题中,因为不进行区分,所以只需在过程中判断系数是否为0即可

  1. 注意事项
  • 小数运算存在精度问题,判零时不能判是否为0,而是要判断其是否小于一个 e p s eps eps,一般情况下, e p s eps eps 1 0 − 7 ∼ 9 10^{-7\sim9} 1079

  • 判断大小是否为零和选择主元时的数字比较要记得打绝对值,并且double的绝对值时fabs()

  1. 完整代码
  • 普通高斯消元
#include<bits/stdc++.h>
using namespace std;

const int N = 105;
const double eps=1e-9;
int n;
double a[N][N],res[N];
inline void gauss(){
    for(int i=1;i<=n;i++){
        int maxx=i;
        for(int j=i+1;j<=n;j++){//寻找主元
            if(fabs(a[j][i])>fabs(a[maxx][i])) maxx=j;//记得加绝对值
        }
        if(fabs(a[maxx][i])<=eps){//记得加绝对值
            cout<<"No Solution";
            exit(0);
        }
        swap(a[maxx],a[i]);
        for(int j=i+1;j<=n;j++){//加减消元
            double tmp=a[j][i]/a[i][i];
            for(int k=1;k<=n+1;k++){
                a[j][k]-=tmp*a[i][k];
            }
        }
    }
    for(int i=n;i>=1;i--){//回带
        for(int j=i+1;j<=n;j++){
            a[i][n+1]-=a[i][j]*res[j];
        }
        res[i]=a[i][n+1]/a[i][i];
    }
    return;
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n+1;j++){
            scanf("%lf",&a[i][j]);
        }
    }
    gauss();
    for(int i=1;i<=n;i++){
        printf("%.2lf\n",res[i]?res[i]:0);
    }
    return 0;
}
  • 高斯-约旦消元
#include<bits/stdc++.h>
using namespace std;

const int N = 105;
const double eps=1e-9;
int n;
double a[N][N];
inline void gauss(){
    for(int i=1;i<=n;i++){
        int maxx=i;
        for(int j=i+1;j<=n;j++){//寻找主元
            if(fabs(a[j][i])>fabs(a[maxx][i])) maxx=j;//记得加绝对值
        }
        if(fabs(a[maxx][i])<=eps){//记得加绝对值
            cout<<"No Solution";
            exit(0);
        }
        swap(a[maxx],a[i]);
        for(int j=1;j<=n;j++){//加减消元
            if(i==j) continue;
            double tmp=a[j][i]/a[i][i];
            for(int k=1;k<=n+1;k++){
                a[j][k]-=tmp*a[i][k];
            }
        }
    }
    return ;
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n+1;j++){
            scanf("%lf",&a[i][j]);
        }
    }
    gauss();
    for(int i=1;i<=n;i++){
        double tmp=a[i][n+1]/a[i][i];
        printf("%.2lf\n",tmp?tmp:0);
    }
    return 0;
}
  1. 两种算法的优劣:好像没有什么太多的区别,但是高斯-约旦消元稍微短小好写一些

例题

例题1:P2455 [SDOI2006] 线性方程组

  • 本质也是板子题,不过有一些细节需要注意

  • 由于主播写的有些累了,这里引用一下题解

    普通的高斯-约旦消元存在漏洞:以 hehe_zhou 大佬的hack数据为例:

    2
    0 2 3
    0 0 0
    

    答案显然是0,但是我们的程序会说这是-1。

    我们发现,我们可以根据第一个式子来求出第二元,但是程序会用它来算第一元,并且以后算第二元的时候不会再考虑该式,因为我们认为它已经对应第一元了!

    也就是说,我们的答案受到消元顺序影响

    题解里部分选手当场召唤玄学势力,把消元顺序随机乱改,大概率能出正解。

    这里提供一种新的做法。

    注意到,我们的症结在于把还有用的式子放到了用不上它的地方,并且以后也不来看它是否可用。

    那么,我们就来看它是否可用。

    原本的高斯消元中,我们认为只有 i 之后的式子是可用的,因为我们不用管无解还是无数解,系数为0直接判掉。

    但这里,我们有可能会在系数为0之后继续做下去。这就是受到消元顺序影响的原因。

    那么,可用的就不仅仅是之后的式子,还有之前系数为0的式子。

    于是解决了。

  • 手动膜拜大佬Rui_R orz orz,大佬不要告我侵权拜谢拜谢

#include<bits/stdc++.h>
using namespace std;

const int N = 55;
const double eps=1e-7;
int n;
double a[N][N];

inline double Abs(double a){
    return a<0?-a:a;
} 

inline void gauss(){
    for(int i=1;i<=n;i++){//未知数 
        int maxx=i;
        for(int j=1;j<=n;j++){//枚举所有行,寻找最大的系数当做主元 
            if(Abs(a[j][j])>eps&&j<i) continue;//排除掉在i之前且系数不为0的(已确定) 
            if(Abs(a[j][i])>Abs(a[maxx][i])) maxx=j;
        }
        for(int j=1;j<=n+1;j++){//将选出主元所在的行换到第i行 
            swap(a[i][j],a[maxx][j]);
        } 
        if(Abs(a[i][i])<=eps) continue;//系数为0,先跳过
        for(int j=1;j<=n;j++){//对剩下的每一行进行加减消元 
            if(i==j) continue;
            double tmp=a[j][i]/a[i][i];
            for(int k=i;k<=n+1;k++){
                a[j][k]-=tmp*a[i][k];
            }
        } 
    }
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n+1;j++){
            scanf("%lf",&a[i][j]);
        }
    }
    gauss();
    int flag=1;
    for(int i=1;i<=n;i++){//枚举每一个未知数的系数,判断无解和无数解 
        if(Abs(a[i][i])<=eps){
            if(Abs(a[i][n+1])>eps) flag=-1;//系数为0,常数不为零,无解,且必须所有都符合才无解 
            else if(flag!=-1) flag=0;
        }
    }
    if(flag!=1) printf("%d\n",flag);
    else for(int i=1;i<=n;i++){
        //printf("%.10lf %d\n",Abs(a[i][n+1]/a[i][i]),Abs(a[i][n+1]/a[i][i])<=eps);
        printf("x%d=%.2lf\n",i,Abs(a[i][n+1]/a[i][i])<=eps?0:a[i][n+1]/a[i][i]);//用常数除以第i个未知数的最终系数 
    }

    return 0;
}

例题2:P4035 [JSOI2008] 球形空间产生器

  1. 思路:
  • 注意到这里要求球心坐标,很容易想到圆的轨迹方程 (whk乱入),又发现已经给出了 n + 1 n+1 n+1个点,所以通过半径相等,我们可以列出n个方程,求解n个未知数,高斯消元即可

  • 注意到,高斯消元只能求解标准形式的n元一次方程,所以我们需要帮忙进行一个化简:

    令已知的 n + 1 n+1 n+1个点分别为 P i ( x i , y i , z i , …   ) , i ∈ { i ∈ R ∣ 1 ≤ i ≤ n + 1 } P_i(x_i,y_i,z_i,\dots),i\in\{i\in R|1\le i\le n+1\} Pi(xi,yi,zi,),i{iR∣1in+1}

    设圆心 O ( x , y , z , …   ) O(x,y,z,\dots) O(x,y,z,)

    则有 ( x i − x ) 2 + ( y i − y ) 2 + ( z i − z ) 2 + ⋯ = ( x i − 1 − x ) 2 + ( y i − 1 − y ) 2 + ( z i − 1 − z ) 2 + … (x_i-x)^2+(y_i-y)^2+(z_i-z)^2+\dots=(x_{i-1}-x)^2+(y_{i-1}-y)^2+(z_{i-1}-z)^2+\dots (xix)2+(yiy)2+(ziz)2+=(xi1x)2+(yi1y)2+(zi1z)2+

    化简后,得到

    x i 2 + x 2 − 2 x i x + ⋯ = x i − 1 2 + x 2 − 2 x i − 1 x + … x_i^2+x^2-2x_ix+\dots=x_{i-1}^2+x^2-2x_{i-1}x+\dots xi2+x22xix+=xi12+x22xi1x+

    移项后,得到

    x i 2 − x i − 1 2 + y i 2 − y i − 1 2 ⋯ = 2 ( x i x − x i − 1 ) x + 2 ( y i − y i − 1 ) y … x_i^2-x_{i-1}^2+y_i^2-y_{i-1}^2\dots=2(x_ix-x_{i-1})x+2(y_i-y_{i-1})y\dots xi2xi12+yi2yi12=2(xixxi1)x+2(yiyi1)y

  • 这样我们就可以构造出增广矩阵了:

        //a里存的是原始数据,mat是增广矩阵
        for(int i=2;i<=n+1;i++){
            for(int j=1;j<=n;j++){
                mat[i-1][j]=2*(a[i-1][j]-a[i][j]);
                mat[i-1][n+1]+=(a[i-1][j]*a[i-1][j]-a[i][j]*a[i][j]);
            }
        }
    
  1. 代码:
#include<bits/stdc++.h>
using namespace std;

const int N = 15;
const double eps = 1e-7;

double a[N][N];
double mat[N][N],ans[N];
int n;

inline double Abs(double a){
    return a<0?-a:a;
}

inline void gauss(){
    for(int i=1;i<=n;i++){
        int maxx=i;
        for(int j=i+1;j<=n;j++){
            if(Abs(mat[j][i])>Abs(mat[maxx][i])) maxx=j;
        }
        if(maxx!=i){
            for(int j=1;j<=n+1;j++){
                swap(mat[i][j],mat[maxx][j]);
            }
        }
        for(int j=i+1;j<=n;j++){
            double tmp=mat[j][i]/mat[i][i];
            for(int k=i;k<=n+1;k++){
                mat[j][k]-=mat[i][k]*tmp;
            }
        }
    }
    for(int i=n;i>=1;i--){
        double tmp=mat[i][n+1];
        for(int j=n;j>i;j--){
            tmp-=ans[j]*mat[i][j]; //代入消元,减掉后面数的贡献 
        }
        ans[i]=tmp/mat[i][i];//最后除掉系数即为答案 
    }
    return ;
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n+1;i++){
        for(int j=1;j<=n;j++){
            scanf("%lf",&a[i][j]);
        }
    }
    for(int i=2;i<=n+1;i++){
        for(int j=1;j<=n;j++){
            mat[i-1][j]=2*(a[i-1][j]-a[i][j]);
            //cerr<<mat[i-1][j]<<' '; 
            mat[i-1][n+1]+=(a[i-1][j]*a[i-1][j]-a[i][j]*a[i][j]);
        }
        //cerr<<mat[i-1][n+1]<<'\n';
    }
    gauss();
    for(int i=1;i<=n;i++){
        printf("%.3lf",ans[i]);
        if(i==n) puts("");
        else putchar(' ');
    }
    return 0;
}

经过了上述的努力学习,我们终于可以来学点高级的了

高斯消元+期望

例题3: P3232 [HNOI2013] 游走
  1. 说在这里的前面
  • 联系说在前面,这道题就是促使我来学习高斯消元的那道题
  1. 思路:
  • 我们知道,对于每一条边,它都有一个期望经过次数,而 ∑ \sum 这个概率 × \times ×其边权即为分值和的期望

  • 题意要求分值和期望的最小值,存在一个贪心思路,即给访问期望次数最大的边赋上最小的边权值

  • 这样,我们要做的事情就成为了求出所有边的期望经过次数,最后排序赋值即可

  • 对于一条边u->v,它被访问的期望次数为 访问 u 的期望次数 × 1 d e g [ u ] + 访问 v 的期望次数 × 1 d e g [ v ] 访问u的期望次数\times \frac{1}{deg[u]}+访问v的期望次数\times \frac{1}{deg[v]} 访问u的期望次数×deg[u]1+访问v的期望次数×deg[v]1

  • 所以问题又转换为求访问某个点的期望次数

  • 我们发现,每个点的期望访问次数会由与它相连的点转移而来,即 f u = ∑ v ∈ t o [ u ] f v d e g v f_u=\sum_{v\in to[u]} \frac{f_v}{deg_v} fu=vto[u]degvfv

  • 特别的,当这个点为1时,还要加上它初始的一次访问

  • 则有:

    f i = ∑ v ∈ t o [ u ] f v d e g v   ( 1 < i < n ) f 1 = ∑ v ∈ t o [ 1 ] f v d e g v + 1 f_i=\sum_{v\in to[u]}\frac{f_v}{deg_v} \ (1<i<n)\\ f_1=\sum_{v\in to[1]}\frac{f_v}{deg_v}+1 fi=vto[u]degvfv (1<i<n)f1=vto[1]degvfv+1

  • 进一步:

    f i − ∑ v ∈ t o [ u ] f v d e g v = 0   ( 1 < i < n ) f 1 − ∑ v ∈ t o [ 1 ] f v d e g v = 1 f_i-\sum_{v\in to[u]}\frac{f_v}{deg_v}=0 \ (1<i<n)\\ f_1-\sum_{v\in to[1]}\frac{f_v}{deg_v}=1 fivto[u]degvfv=0 (1<i<n)f1vto[1]degvfv=1

  • 我们发现,这个转移会受到后继状态的影响,所以需要设 f i f_i fi为未知数, 1 d e g i \frac{1}{deg_i} degi1为系数(不要算n点,因为访问到n点后就结束了,没有贡献),列出n-1个方程进行求解

        --n;//避免第n个点被统计 
        a[1][n+1]=-1.0;//为了求和方便,将方程都乘上-1,使Σ前系数为1
        for(int u=1;u<=n;u++){//初始化,用于高斯消元求解a数组 
            a[u][u]=-1;
            for(int i=fi[u];i;i=e[i].nxt){
                int v=e[i].to;
                if(v!=n+1) a[u][v]=1.0/d[v];
            }
        }
    
  1. 代码:
#include<bits/stdc++.h>
using namespace std;

const int N = 510,M = 5e5+10;
const double eps=1e-9;

int n,m;
double f[M],a[N][N],ans;
int d[N],u[M],v[M];
struct edge{
    int to,nxt;
}e[M<<1];
int ecnt,fi[N];

inline void add(int x,int y){
    e[++ecnt].to=y;
    e[ecnt].nxt=fi[x];
    fi[x]=ecnt;
    d[x]++;

    e[++ecnt].to=x;
    e[ecnt].nxt=fi[y];
    fi[y]=ecnt;
    d[y]++;
}

inline void gauss(){
    for(int i=1;i<=n;i++){
        int maxx=i;
        for(int j=i+1;j<=n;j++){
            if(fabs(a[j][i])>fabs(a[maxx][i])) 
                swap(maxx,j);
        }
        if(maxx!=i){
            for(int j=1;j<=n+1;j++){
                swap(a[i][j],a[maxx][j]);
            }
        }
        if(fabs(a[i][i])<eps) continue;
        for(int j=1;j<=n;j++){
            if(j!=i){
                double tmp=a[j][i]/a[i][i];
                for(int k=i+1;k<=n+1;k++)
                    a[j][k]-=a[i][k]*tmp;
            }
        }
    }
    for(int i=1;i<=n;i++){
        a[i][n+1]/=a[i][i];
    }
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&u[i],&v[i]);
        add(u[i],v[i]);
    }
    --n;//避免第n个点被统计 
    a[1][n+1]=-1.0;
    for(int u=1;u<=n;u++){//初始化,用于高斯消元求解a数组 
        a[u][u]=-1;
        for(int i=fi[u];i;i=e[i].nxt){
            int v=e[i].to;
            if(v!=n+1) a[u][v]=1.0/d[v];
        }
    }
    gauss();
    for(int i=1;i<=m;i++){//来到第i条边的期望次数为从左端点来的期望次数+从右端点来的期望次数 
        f[i]=a[u[i]][n+1]/d[u[i]]+a[v[i]][n+1]/d[v[i]];
    } 
    sort(f+1,f+1+m);//贪心求解答案,给期望次数大的放小编号 
    for(int i=1;i<=m;i++){
        ans+=f[i]*(m-i+1);
    }
    printf("%.3lf",ans);
    return 0;
}
例题4:P3211 [HNOI2011] XOR和路径——高斯消元+期望+位运算
  1. 思路:
  • 看到xor,多少和位运算有点关系

  • 考虑到最终答案中每一位是否为1 只与计算过程中对应位是否为1有关,并且期望具有可加性: E ( a + b ) = E ( a ) + E ( b ) E(a+b)=E(a)+E(b) E(a+b)=E(a)+E(b),所以我们对每一位分别进行计算

  • f u f_u fu为从u走到n边权异或和为1的概率

  • 则有转移:

    f u = ∑ ( u , v ) ∈ E , w ( u , v ) = 1 ( 1 − f v ) d e g u + ∑ ( u , v ) ∈ E , w ( u , v = 0 ) f v d e g u ( 1 d e g u 为从 u 点走到 v 的概率 ) f_u=\sum_{(u,v)\in E,w(u,v)=1}\frac{(1-f_v)}{deg_u}+\sum_{(u,v)\in E,w(u,v=0)} \frac{f_v}{deg_u}\\ (\frac{1}{deg_u}为从u点走到v的概率) fu=(u,v)E,w(u,v)=1degu(1fv)+(u,v)E,w(u,v=0)degufv(degu1为从u点走到v的概率)

  • 进一步,有

    f u d e g u = ∑ ( u , v ) ∈ E , w ( u , v ) = 1 ( 1 − f v ) + ∑ ( u , v ) ∈ E , w ( u , v ) = 0 f v f u d e g u + ∑ ( u , v ) ∈ E , w ( u , v ) = 1 f v − ∑ ( u , v ) ∈ E , w ( u , v ) = 0 f v = ∑ ( u , v ) ∈ E , w ( u , v ) = 1 1 f_udeg_u=\sum_{(u,v)\in E,w(u,v)=1}(1-f_v)+\sum_{(u,v)\in E,w(u,v)=0 }f_v\\ f_udeg_u+\sum_{(u,v)\in E,w(u,v)=1}f_v-\sum_{(u,v)\in E,w(u,v)=0 }f_v=\sum_{(u,v)\in E,w(u,v)=1}1 fudegu=(u,v)E,w(u,v)=1(1fv)+(u,v)E,w(u,v)=0fvfudegu+(u,v)E,w(u,v)=1fv(u,v)E,w(u,v)=0fv=(u,v)E,w(u,v)=11

            a[n][n]-=1.0;
            for(int u=1;u<n;u++){
                a[u][u]=d[u];
                for(int i=fi[u];i;i=e[i].nxt){
                    int v=e[i].to;
                    int w=(e[i].w>>k)&1;//看一下边权在这一位上是否为1
                    if(w){
                       // cout<<u<<' '<<n+1<<' '<<a[u][n+1]<<'\n';
                        a[u][n+1]+=1.0;
                        a[u][v]+=1.0;
                        //cout<<d[u]<<' '<<a[u][v]<<' '<<a[u][n+1]<<'\n';
                    }else{
                        a[u][v]-=1.0;
                       // cout<<a[u][v]<<'\n';
                    }
                }
           }
    

    消元后,将每一位的 f 1 f_1 f1乘上这一位为1时对应权值( 2 i 2^i 2i),总和即为答案

  1. 代码
#include<bits/stdc++.h>
using namespace std;

const int N = 120;
const int M = 20005;

int n,m;
double a[N][N],f[N];

struct edge{
    int to,nxt,w;
}e[M];
int fi[N],ecnt=0,d[N];
inline void add(int u,int v,int w){
    e[++ecnt].to=v;
    e[ecnt].nxt=fi[u];
    fi[u]=ecnt;
    e[ecnt].w=w;
    d[u]++;
}

inline void gauss(){
    for(int i=1;i<n;i++){//f[n]不用单独求(为0)
        for(int j=1;j<=n;j++){
            if(i==j) continue;
            double tmp=a[j][i]/a[i][i];
            for(int k=1;k<=n+1;k++){
                a[j][k]-=tmp*a[i][k];
            }
        }
    }
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        if(u!=v) add(v,u,w);
    }
    double ans=0;
    for(int k=0;k<=30;k++){
        memset(a,0,sizeof(a));
        a[n][n]-=1.0;
        for(int u=1;u<n;u++){
            a[u][u]=d[u];
            for(int i=fi[u];i;i=e[i].nxt){
                int v=e[i].to;
                int w=(e[i].w>>k)&1;//看一下边权在这一位上是否为1
                if(w){
                   // cout<<u<<' '<<n+1<<' '<<a[u][n+1]<<'\n';
                    a[u][n+1]+=1.0;
                    a[u][v]+=1.0;
                    //cout<<d[u]<<' '<<a[u][v]<<' '<<a[u][n+1]<<'\n';
                }else{
                    a[u][v]-=1.0;
                   // cout<<a[u][v]<<'\n';
                }
            }
        }
        gauss();//注意位置!!
        ans+=(a[1][n+1]/a[1][1])*(1<<k);//记得要把系数除掉
    }
    printf("%.3lf\n",ans);
    return 0;
}

高斯消元求解异或方程组

例题5:P2447 [SDOI2010] 外星千足虫
  1. 思路
  • 题意很显然可以抽象为一个系数为1/0的异或方程组

  • 求解时,由于异或性质,我们可以直接用n个bitset存储整个方程组,直接对bitset进行xor来消元即可

    bitset

    b.set(pos,val)把b的第pos个二进制位赋值为val

    b.test(pos)询问b的第pos个二进制位是否位0

  1. 代码
#include<bits/stdc++.h>
using namespace std;

const int N = 2e3+10;

int n,m,ans=0;
string s;
bitset<1010>mat[N];

inline void gauss(){
    ans=-1;
    for(int i=1;i<=n;i++){
        int cur=i;
        while(cur<=m&&!mat[cur].test(i)){
            cur++;//寻找不为0系数
        }
        if(cur>m){
            ans=0;
            return ;//有多解
        }
        ans=max(ans,cur);
        if(cur!=i){
            swap(mat[cur],mat[i]);
        }
        for(int j=1;j<=m;j++){
            if(j!=i&&mat[j].test(i)!=0){
                mat[j]^=mat[i];
            }
        }
    }
    return ;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    int res;
    for(int i=1;i<=m;i++){
        cin>>s;
        cin>>res;
        for(int j=0;j<n;j++){
            mat[i].set(j+1,s[j]=='1');
        }
        mat[i].set(0,res);//系数存在第0位
    }
    gauss();
    if(ans){
        cout<<ans<<'\n';
        for(int i=1;i<=n;i++){
            if(mat[i].test(0)){//由于系数只能是1/0,所以不用除系数
                cout<<"?y7M#\n";
            }else{
                cout<<"Earth\n";
            }
        }
    }else{
        cout<<"Cannot Determine";
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值