暑期专题一 高斯消元法


一般来说,对于n个一次方程和n个未知数,可以通过高斯消元法来判断这个方程无解,有唯一解还是有多解。对于一个有唯一解的方程,我们可以通过程序实现加减消元和代入消元,以此来求得这个方程的解。

先贴一个解普通方程的模板:

void Gauss(int m, int n)
{
    int i = 0, j = 0;
    for (; i < m && j < n; i++, j++)
    {
        int mx = i;
        for (int k = i + 1; k < m; k++) if (fabs(a[k][j]) > fabs(a[mx][j])) mx = k;
        if (fabs(a[mx][i]) < eps) {i--; continue;}
        if (mx ^ i) swap(a[mx], a[i]);
        for (int k = 1; k < m; k++) if (k ^ i) 
        {
            double tmp = a[j][i] / a[i][i];
            for (int l = j; l <= n; l++) a[k][l] -= tmp * a[k][l];
        }
    } 
}

1. [JSOI2008 Round1] 球形空间产生器

问题描述

火星人不能忍受地球人对他们的歧视,终于发明了一种非常强大的武器:球形空间产生器。球形空间产生器能产生一个N维球体屏障,而且这个屏障是坚不可摧的,被困在球体内的地球人就被切断了与外界的联系。Js08现在就被困在了屏障中,情况十分危急,必须尽快找出并摧毁球形空间产生器。Js08经过摸索和碰壁,给出了球体上N+1个点的坐标,希望你能够帮Js08找出球形空间产生器的位置——它总是位于球形空间的球心。

输入格式

第一行为整数N,代表了空间的维度。
  接下来N+1行,每行N个实数代表一个坐标。输入数据精确到小数点后6位。
  输入数据保证输出结果唯一。

输出格式

输出一行N个实数,代表球心的坐标,精确到小数点后三位。相邻的数字之间用一个空格分开(行末无空格)。

数据规模:

对于40%的数据,1<=n<=3

对于100%的数据,1<=n<=10   思路:这个题其实就是一个高斯消元的板题。为什么呢?我们来看题。 首先,假设这个球心为 (X1, X2, X3, X4, ... , Xn) ,第i个球的坐标为(Xi1, Xi2, Xi3, Xi4, ... , Xin) 可知:对于第i个球,设有di ^ 2 = (Xi1 - X1) ^ 2 + (Xi2 - X2) ^ 2 + .. + (Xin - Xn) ^ 2,那么显然d1 = d2 = d3 = ... = dn 但是高斯消元只能解决一次方程,怎么办呢?我们发现对于每一个方程的两边,都必然含有X1 ^ 2,X2 ^ 2, ... , Xn ^ 2,于是可以消去。 这样方程就只剩下常数项和一次项,可以用高斯消元法解决。
#include 

using namespace std;

typedef double db;

const db eps = 1e-8;

db a[15][15];

void Gauss(int n)
{
    for (int i = 0; i < n; i++)
    {
        int mx = i;
        for (int j = i + 1; j < n; j++) if (fabs(a[mx][i]) < fabs(a[j][i])) mx = j;
        if (fabs(a[mx][i]) < eps) continue;
        for (int j = 0; j <= n; j++) swap(a[i][j], a[mx][j]);
        for (int j = 0; j < n; j++)
        {
            if (i ^ j)
            {
                db tmp = a[j][i] / a[i][i];
                for (int k = i + 1; k <= n; k++) a[j][k] -= a[i][k] * tmp;
            }
        }
    } for (int i = 0; i < n - 1; i++) printf("%.3lf ", a[i][n] / a[i][i]);
}

int main()
{
    int n;
    scanf("%d", &n); ++n;
    for (int i = 0; i < n; i++) for (int j = 0; j < n - 1; j++)
    {
        scanf("%lf", &a[i][j]), a[i][n] -= a[i][j] * a[i][j], a[i][j] *= -2.0;
        a[i][n - 1] = 1.0;
    } Gauss(n);
}

2.NKOJ1986 开关问题

问题描述

有N个相同的开关,每个开关都与某些开关有着联系,每当你打开或者关闭某个开关的时候,其他的与此开关相关联的开关也会相应地发生变化,即这些相联系的开关的状态如果原来为开就变为关,如果为关就变为开。你的目标是经过若干次开关操作后使得最后N个开关达到一个特定的状态。对于任意一个开关,最多只能进行一次开关操作。你的任务是,计算有多少种可以达到指定状态的方法。(不计开关操作的顺序)

输入格式

输入第一行有一个数K,表示以下有K组测试数据。
每组测试数据的格式如下:
第一行 一个数N(0 < N < 29)
第二行 N个0或者1的数,表示开始时N个开关状态。
第三行 N个0或者1的数,表示操作结束后N个开关的状态。
接下来 每行两个数I J,表示如果操作第 I 个开关,第J个开关的状态也会变化。每组数据以 0 0 结束。

输出格式

如果有可行方法,输出总数,否则输出“Oh,it's impossible~!!” 不包括引号。

思路:非常经典的高斯消元解异或方程组。和普通的高斯消元其实类似,只是状态和未知数都变成了0和1。

贴个异或方程组的模板(其实还可以用bitset优化)

int Gauss(int n)
{
    int ans = 1;
    for (int i = 0; i < n; i++)
    {
        int mx = i;
        if (!a[mx][i])
        {
            for (int j = i + 1; j < n; j++) if (a[mx][j])
            {
                mx = j;
                break;
            }
        }
        if (!a[mx][i]) continue;
        swap(a[i], a[mx]);
        for (int j = 0; j < n; j++)
        {
            if ((i ^ j) && a[j][i])
            {
                for (int k = i + 1; k <= n; k++) a[j][k] ^= a[i][k];
            } 
        }
    } 
    for (int i = n - 1; ~i; i--)
    {
        if (a[i][i] == 0 && a[i][n]) return 0;
        else if (a[i][i] == 0) ans <<= 1;
        else for (int j = i - 1; ~j; j--)
        {
            if (a[j][i]) a[j][i] ^= a[i][i], a[j][n] ^= a[i][n];
        }
    } return ans;
}

3.手机游戏

问题描述

有一个有趣的手机游戏,有n*n个正方形的小按钮,有的按钮是黄色,有的按钮是白色。玩家的任务是通过点击按钮,让所有按钮都变成黄色,点按钮的次数越少,得分越高。
但是按钮有个奇怪的特点,当你点击了坐标为(x,y)的按钮后,坐标为(x,y),(x+1,y),(x-1,y),(x,y-1),(x,y+1)的五个按钮会同时改变自身的颜色,是白色的变成黄色,黄色的变成白色。完成游戏最少需要点击多少次按钮呢?请找出答案。

输入格式

第一行,一个整数n,表示有nn个按钮。(1<=n<=40)
接下来是一个由小写字母'y'和字符'w'构成的n
n的矩阵,描述了游戏开始时的情景。'y'表示该按钮式黄色,'w'表示该按钮式白色。

输出格式

如果能够完成游戏,输出一个整数,表示所需最小点击次数。
如果无法完成游戏,输出"inf"。

思路:同样是异或方程组,但是这次是一个二维的矩阵,怎么转化为高斯消元呢?其实非常简单,把每一个方块的状态当作一个方程,显然,它旁边的四个点同样会影响它的状态,所以把这五个点在这一个方程的系数设为1,然后高斯消元即可。

#include 

using namespace std;

char ch[55][55];
bool a[1805][1805], st[1805][1805];

int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, -1, 0, 1};
int ans = 1e9;

void dfs(int u, int n)
{
    if (u == -1)
    {
        int cnt = 0;
        for (int i = 0; i < n; i++) cnt += a[i][n];
        ans = min(ans, cnt);
        return;
    }
    bool flg = 0;
    if (!a[u][u])
    {
        if (a[u][n]) return;
        dfs(u - 1, n), flg = 1, a[u][n] = 1;
    }
    for (int i = 0; i < u; i++)
    {
        st[u][i] = a[i][n];
        a[i][n] = a[i][n] - a[u][n] * a[i][u] + 2 & 1;
    }
    dfs(u - 1, n);
    for (int i = 0; i < u; i++) a[i][n] = st[u][i];
    if (flg) a[u][n] = 0;
}

void Gauss(int n)
{
    int i, j;
    for (i = 0, j = 0; i < n && j < n; i++, j++)
    {
        int mx = i;
        for (int k = i + 1; k < n; k++)
        {
            if (abs(a[k][j]) > abs(a[mx][j])) mx = k;
        }
        if (a[mx][j] == 0) 
        {
            --i;
            continue;
        }
        if (mx ^ i) for (int k = j; k <= n; k++) swap(a[i][k], a[mx][k]);
        for (int k = i + 1; k < n; k++)
        {
            if (a[k][j] != 0) 
            {
                for (int l = j; l <= n; l++) a[k][l] ^= a[i][l];
            }
        }
    }
    dfs(n - 1, n);
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) for (int j = 0; j < n; j++)
    {
        int pos = i * n + j;
        a[pos][pos] = 1;
        for (int t = 0; t < 4; t++)
        {
            int x = i + dx[t], y = j + dy[t];
            if (x >= 0 && x < n && y >= 0 && y < n) a[x * n + y][pos] = 1;
        }
    }
    for (int i = 0; i < n; i++) 
    {
        scanf("%s", ch[i]);
        for (int j = 0; j < n; j++)
        {
            if (ch[i][j] == 'w') a[i * n + j][n * n] = 1;
            else a[i * n + j][n * n] = 0;
        }
    } 
    Gauss(n * n);
    if (ans >= 1e9) return puts("inf"), 0;
    printf("%d", ans);
}

4.[HNOI2013 DAY2]游走

问题描述

一个无向连通图,顶点从1 编号到N,边从1 编号到M。小Z 在该图上进行随机游走,初始时小Z 在1 号顶点,每一步小Z 以相等的概率随机选择当前顶点的某条边,沿着这条边走到下一个顶点,获得等于这条边的编号的分数。当小Z到达N 号顶点时游走结束,总分为所有获得的分数之和。现在,请你对这M 条边进行编号,使得小Z 获得的总分的期望值最小。

输入格式

第一行是正整数N和M,分别表示该图的顶点数和边数,接下来M行每行是整数u,v(1≤u,v≤N),表示顶点u与顶点v之间存在一条边。


输入保证30%的数据满足N≤10,

100%的数据满足2≤N≤500,  1<=M<=150000且是一个无向简单连通图。

输出格式

仅包含一个实数,表示最小的期望值,保留3 位小数。

思路:非常经典的一道期望+高斯消元的题目。首先看到期望应该把它转化为方程。 显然,让经过期望次数较大的边权值尽可能小,总分的期望值就会最小。那么如何算出每条边经过的期望次数呢?假设这条边的两个端点为u和v,设f[i]为走出第i个点的期望次数,d[i]为第i个点的度数,那么这条边经过的期望次数 = f[u] / d[u] + f[v] / d[v]。然后如何计算f[i]呢?首先终点为n的边不能计算贡献,因为我并不能走出n点。其次,如果起点为1,那么首先会有一个期望值1。那么这样的话,对于第i个点,f[i] = f[v1] / d[v1] + f[v2] / d[v2] + ... (其中vi是与i有边直接相连的点)。这个东西没有拓扑序,不能用递推或dfs解决,于是考虑高斯消元,n个未知数,m个方程,可以解决本题。
#include 

using namespace std;

struct node {
    int to, nxt;
}e[400005]; int head[1005], tot;

inline void add_e(int u, int v) {e[++tot].to = v; e[tot].nxt = head[u]; head[u] = tot;}

typedef double db;

const db eps = 1e-7;

db d[1005], val[200005], a[605][605];
int st[200005], ed[200005];

void Gauss(int n)
{
    for (int i = 1; i < n; i++)
    {
        int mx = i;
        for (int j = i + 1; j < n; j++) if (fabs(a[j][i]) > fabs(a[mx][i])) mx = j;
        if (mx ^ i) swap(a[i], a[mx]);
//        for (int j = i; j <= n; j++) swap(a[i][j], a[mx][j]);
        if (fabs(a[i][i]) < eps) continue;
        for (int j = 1; j < n; j++)
        {
            if (i ^ j)
            {
                db tmp = a[j][i] / a[i][i];
                for (int k = i + 1; k <= n; k++) a[j][k] -= tmp * a[i][k]; 
            }
        }
    }
    for (int i = 1; i < n; i++) a[i][n] /= a[i][i];
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) 
    {
        scanf("%d%d", &st[i], &ed[i]);
        d[st[i]] += 1.0, d[ed[i]] += 1.0;
        add_e(st[i], ed[i]), add_e(ed[i], st[i]);
    } 
    for (int i = 1; i < n; i++)
    {
        a[i][i] = -1.0;
        for (int j = head[i]; j; j = e[j].nxt)
        {
            int v = e[j].to;
            if (v == n) continue;
            a[i][v] = 1.0 / d[v];
        }
    } a[1][n] = -1.0;
    Gauss(n);
    for (int i = 1; i <= m; i++) val[i] = a[st[i]][n] / d[st[i]] + a[ed[i]][n] / d[ed[i]];
    sort(val + 1, val + m + 1); db ans = 0;
//    for (int i = 1; i <= m; i++) printf("%.3lf ", val[i]);
    for (int i = 1; i <= m; i++) ans += val[i] * (m - i + 1.0) * 1.0;
    printf("%.3lf", ans);
}

5.[HNOI2011]XOR和路径

题目描述

给定一个无向连通图,其节点编号为 1 到 N,其边的权值为非负整数。试求出一条从 1 号节点到 N 号节点的路径,使得该路径上经过的边的权值的“XOR 和”最大。该路径可以重复经过某些节点或边,当一条边在路径中出现多次时,其权值在计算“XOR 和”时也要被重复计算相应多的次数。

直接求解上述问题比较困难,于是你决定使用非完美算法。具体来说,从 1 号节点开始,以相等的概率,随机选择与当前节点相关联的某条边,并沿这条边走到下一个节点,重复这个过程,直到走到 N 号节点为止,便得到一条从 1 号节点到 N 号节点的路径。显然得到每条这样的路径的概率是不同的并且每条这样的路径的“XOR 和”也不一样。现在请你求出该算法得到的路径的“XOR 和”的期望值。

输入格式

输入文件的第一行是用空格隔开的两个正整数N和M,分别表示该图的节点数和边数。紧接着的M行,每行是用空格隔开的三个非负整数u,v和w(1≤u,v≤N,0≤w≤10^9),表示该图的一条边(u,v),其权值为w。输入的数据保证图连通。

输出格式

输出文件仅包含一个实数,表示上述算法得到的路径的“XOR 和”的期望值,要求保留三位小数。(建议使用精度较高的数据类型进行计算)

思路:和游走那个题比较类似,但是我感觉这个题还要复杂一些。 要注意XOR这个东西,对于这个东西,我们有很多处理它的方案,但是原理都是一样:有类似前缀的性质,并且在二进制下每一位独立计算互不干扰。那么这个题就利用了它每一位可以独立计算的性质,对二进制下的每一位分别进行高斯消元,然后将每一位的答案合并。这就是这个题的核心考点。其余的对于期望的处理和上一题类似。
#include 

using namespace std;

typedef double db;

const db eps = 1e-8;

db a[105][105];

struct node {
    int to, nxt, len;
}e[20005]; int head[105], tot, d[105];
inline void add_e(int u, int v, int w) {e[++tot].to = v; e[tot].nxt = head[u]; head[u] = tot; e[tot].len = w;}

void Gauss(int n)
{
    for (int i = 1; i <= n; i++)
    {
        int mx = i;
        for (int j = i + 1; j <= n; j++) if (fabs(a[j][i]) - fabs(a[mx][i]) > eps) mx = j;
        if (mx ^ i) swap(a[i], a[mx]);
        for (int j = 1; j <= n; j++)
        {
            if ((i ^ j) && fabs(a[j][i]) > eps)
            {
                db tmp = a[j][i] / a[i][i];
                for (int k = i; k <= n + 1; k++)
                {
                    a[j][k] -= tmp * a[i][k];
                }
            }
        }
    } 
    for (int i = 1; i <= n; i++) a[i][n + 1] /= a[i][i];
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1, x, y, z; i <= m; i++)
    {
        scanf("%d%d%d", &x, &y, &z);
        if (x == y) {add_e(x, y, z), ++d[x]; continue;} 
        add_e(x, y, z), add_e(y, x, z);
        ++d[x], ++d[y];
    } db ans = 0;
    for (int k = 30; ~k; k--)
    {
        memset(a, 0, sizeof a);
        for (int i = 1; i < n; i++)
        {
            a[i][i] = -d[i];
            for (int j = head[i]; j; j = e[j].nxt)
            {
                int v = e[j].to, w = e[j].len >> k & 1;
                w ? a[i][v] -= 1, a[i][n + 1] -= 1 : a[i][v] += 1;
            }
        } a[n][n] = 1;
        Gauss(n);
        ans += (1 << k) * a[1][n + 1];
    } printf("%.3lf", ans);
}