高斯消元学习笔记
a_cow_of_FJ · · 算法·理论
距离 NOIP 还剩两个多星期,写篇文章攒攒 RP,同时巩固一下高斯消元。
引入
高斯消元是求解线性方程组的经典算法,由德国数学家高斯提出。
例一(P2455 线性方程组)
这道题目就是高斯消元的模板。
题意
解以下
其中
若存在唯一解,则输出;若无解,输出
高斯消元
消元法
首先回顾一下初中学过的解二/三元一次方程组的方法。主要思想便是消元。而消元方法又分为两种:代入消元法和加减消元法。
前者顾名思义,将一未知数用含有其它未知数的代数式表示,并将其带入到别的方程中,从而消去这一未知数。比如对于下面的方程组:
由第二个方程可得
将其带入方程组中第一个方程,消元
解得
再将其回代到方程
再说后者,即将方程组中的一方程倍乘某个常数加到另外的方程中去,实现消元。仍以上面的方程组为例:
直接把两方程相加,消元
解得
再把
通过比较我们发现:如果不考虑回代,那么显然加减消元法更方便实现(因为只要令每一项系数相加),所以我们今天重点考虑加减消元法。
我们总结出:对方程组做初等变换,解不变。具体来说,我们可以:
- 互换两方程
- 一方程左右两边同乘以非零数
k - 一方程加上另一方程的
k 倍
高斯消元法
首先,考虑到消元过程中参与计算和发生改变的是方程中各变量的系数,和各变量无关,方便起见,我们把题目中的线性方程组的系数表示成一个
对于方程右边的常数,作为一个
此时线性方程组的初等变换对应矩阵的初等行变换(列可不能乱动)。
通过初等行变换,把增广矩阵化为行阶梯矩阵,模拟消元过程,从而求解线性方程组,这就是最基础的高斯消元法。
到这里,你可能有些没听懂(好吧其实说的是笔者自己),所以我们举个简单的例子:
解以下方程组:
先写出该方程组对应的的增广矩阵:
接下来化为行阶梯矩阵:
发现有
接下来自底向上依次回代可得
通过上面的例子,大致流程可以轻松看出:枚举列
具体来说,对于每个
理想情况下矩阵变为上三角矩阵。
最后做自底向上的回代即可。
实现
方便起见,定义初等行变换函数,用来实现形如
void row_add(double r1[], double r2[], double k) {
for (int j = 1; j <= n + 1; j++) r1[j] += k * r2[j];
}
下面考虑增广矩阵化为行阶梯矩阵。之前提到,枚举列
int cur = 1;
for (int j = 1; j <= n; j++) {
int k = cur;
for (int i = cur + 1; i <= n; i++)
if (abs(a[i][j]) > abs(a[k][j])) k = i;
if (abs(a[k][j]) < EPS)
continue;
swap(a[cur], a[k]);
for (int i = j + 1; i <= n; i++)
row_add(a[i], a[cur], -a[i][j] / a[cur][j]);
cur++;
}
这里不用第一个主元系数非零的行而是用主元系数最大的行,目的是最小化
最后考虑回代。假设数据存在唯一解,经过高斯消元后我们得到一个上三角矩阵。矩阵第
解得
显然
for (int i = n; i >= 1; i--) {
x[i] = a[i][n + 1];
for (int j = i + 1; j <= n; j++)
x[i] -= a[i][j] * x[j];
x[i] /= a[i][i];
}
注意,这么做的前提是存在唯一解,如果无解或有无穷多组解则矩阵并不是上三角矩阵,上述代码第
到这里我们已经可以解决有唯一解的数据。
判断无解、无穷多组解
首先如果没有出现自由元,则一定存在唯一解。
当出现自由元时,矩阵底部必定存在系数部分全为
在这些行中,若有至少一行右侧常数不为
否则如果右侧常数也全为
实现
int flag = 1;
if (--cur < n) {
for (int i = cur + 1; i <= n; i++)
if (abs(a[i][n + 1]) > EPS) {
flag = -1; // 无解
break;
}
if (flag == 1) flag = 0; // 无穷多组解
}
注意,
至此,你已经学会了基础的高斯消元法。
高斯-约旦消元法
不过遗憾地告诉你,刚才的基础款高斯消元法我们平时根本不用。因为我们有升级款——高斯-约旦消元法。
注意到上述高斯消元法旨在将增广矩阵变换为行阶梯矩阵,最后再回代。因而我们每次只对当前行之后的行进行消元。那么我们为什么不把当前行上面的行也顺带消元呢?
这样一来,理想情况下矩阵的系数部分最终变为对角矩阵,也就是说每行都是一个一元一次方程,两边同除以系数即可得到方程的解,省去了回代的步骤。
当用第
至于无解、无穷多组解的情况,矩阵的系数部分只能化为行最简形。
实现
方便起见定义初等行变换函数,实现形如
void row_div(double r[], double k) {
for (int j = 1; j <= n + 1; j++) r[j] /= k;
}
易得消元部分代码。
int cur = 1;
for (int j = 1; j <= n; j++) {
int k = cur;
for (int i = cur + 1; i <= n; i++)
if (abs(a[i][j]) > abs(a[k][j])) k = i;
if (abs(a[k][j]) < EPS)
continue;
swap(a[cur], a[k]);
row_div(a[cur], a[cur][j]);
for (int i = 1; i <= n; i++)
if (i != cur) row_op(a[i], a[cur], -a[i][j]);
cur++;
}
无解、无穷多组解的判定与普通高斯消元法相同,此处略。
时间复杂度
显然是未知数数量的平方乘以方程条数,对于这道题是
例二(P2447 外星千足虫)
题意
有
还有
其中
如果存在唯一解,则输出最小的
如果存在无穷多组解输出 Cannot Determine;
数据保证不会无解。
高斯消元求解异或方程组
我们发现异或满足交换律、结合律,且逆运算仍为异或,所以我们可以通过把两个方程异或在一起来消元,进而将增广矩阵系数部分化为行最简形。
bitset 优化
由于矩阵为 01 矩阵,把两行异或在一起这一操作不值得线性枚举,可以使用 std::bitset 压位,直接异或,从而把时间复杂度优化到
求最少到第几条方程就可以确定唯一解
一个暴力的方式是直接二分。但是这样不仅麻烦,而且时间复杂度会多一层
有一个简单的思路:记
不过事实上只需要找第一个主元系数非零的行即可。原因也很简单,设有行
这个方法同样适用于线性方程组。
参考代码
#include <iostream>
#include <bitset>
using namespace std;
const int N = 1000 + 1, M = 2000 + 5;
bitset<N> a[M];
int n, m, id[M];
void gauss() {
int cur = 1, mx = 0;
for (int j = 0; j < n; j++) {
int k = cur;
while (k <= m && !a[k][j]) k++;
if (k > m) {
cout << "Cannot Determine";
return;
}
swap(a[k], a[cur]); swap(id[k], id[cur]);
for (int i = 1; i <= m; i++)
if (i != cur && a[i][j]) a[i] ^= a[cur];
mx = max(mx, id[cur++]);
}
cout << mx << endl;
for (int i = 1; i <= n; i++)
cout << (a[i][n] ? "?y7M#\n" : "Earth\n");
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; id[i] = i, i++)
for (int j = 0; j <= n; j++) {
char c; cin >> c;
if (c == '1') a[i][j] = 1;
}
gauss();
return 0;
}
例三(P2962 Lights G)
题意
在
数据保证有解。
分析
容易发现,每个点的最终状态即为该点与其相邻节点的操作次数之和的奇偶性,且每个点至多操作一次。
设
在第
高斯消元解方程组即可。
但是题目只保证有解,并未保证解唯一。因此可能出现自由元的情况。
对自由元的处理
顾名思义,自由元取值不受约束,然而非自由元的取值可能受到自由元的限制。
为使最终取值为
实现
高斯消元时,用
由于高斯消元后,矩阵系数部分已经化为行最简形,因此非自由元
每次搜到一个未知数,若为自由元,分别取值
记得加上最优性剪枝,快得飞起。
参考代码
#include <iostream>
#include <bitset>
using namespace std;
const int N = 35 + 1, M = 595 + 5;
int n, m;
bitset<N> a[M];
int r[N], p[N];
int ans = 2e9;
void gauss() {
int cur = 1;
for (int j = 1; j <= n; j++) {
int k = cur;
while (k <= m && !a[k][j]) k++;
if (k > m) continue;
swap(a[k], a[cur]);
r[j] = cur;
for (int i = j + 1; i <= m; i++)
if (a[i][j]) a[i] ^= a[cur];
cur++;
}
}
void dfs(int step, int res) {
if (res >= ans)
return;
if (!step) {
ans = res;
return;
}
if (!r[step]) {
p[step] = 0;
dfs(step - 1, res);
p[step] = 1;
dfs(step - 1, res + 1);
return;
}
p[step] = a[ r[step] ][0];
for (int j = 1; j <= n; j++)
if (j != step && a[ r[step] ][j])
p[step] ^= p[j];
dfs(step - 1, res + p[step]);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
a[i][i] = a[i][0] = 1;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
a[u][v] = a[v][u] = 1;
}
gauss();
dfs(n, 0);
cout << ans;
}
例四(P10315 原神,启动!)
题意
有
给定每个方碑的初始状态
无解输出 niuza。若有多解,输出任意一组。
分析
和上一题相似,容易得到方程组:
不同的是,每条方程都是在模意义下的。
高斯消元求解模意义下的方程组
由同余的性质,仍然可以在同余式两边作乘法,或把两方程相加,唯独不能作除法来使系数化为
另外,两式相加时可以给系数和常数都取模。
其余部分与普通的线性方程组无异。
虽然逆元需要每次用快速幂或扩展欧几里得算法来求,但时间复杂度仍为
参考代码
#include <iostream>
using namespace std;
using ll = long long;
const int N = 100 + 5;
int n, m, s[N], t[N];
ll a[N][N];
int ans[N];
int qpow(ll a, int b) {
ll ret = 1;
while (b) {
if (b & 1) ret = ret * a % m;
a = a * a % m;
b >>= 1;
}
return ret;
}
void row_mul(ll r[], int k) {
for (int j = 1; j <= n + 1; j++) r[j] = (r[j] * k % m + m) % m;
}
void row_add(ll r1[], ll r2[], int k) {
for (int j = 1; j <= n + 1; j++) (r1[j] += k * r2[j] % m + m) %= m;
}
bool gauss() {
int cur = 1;
for (int j = 1; j <= n; j++) {
for (int i = cur; i <= n; i++)
if (a[i][j]) {
swap(a[cur], a[i]);
break;
}
if (!a[cur][j]) continue;
row_mul(a[cur], qpow(a[cur][j], m - 2));
for (int i = 1; i <= n; i++)
if (i != cur) row_add(a[i], a[cur], -a[i][j]);
cur++;
}
for (int i = cur; i <= n; i++)
if (a[i][n + 1]) return 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (a[i][j]) {
ans[j] = a[i][n + 1];
break;
}
return 1;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int k; cin >> k;
a[i][i] = 1;
while (k--) {
int j; cin >> j;
a[j][i] = 1;
}
}
for (int i = 1; i <= n; i++)
cin >> s[i];
for (int i = 1; i <= n; i++)
cin >> t[i], a[i][n + 1] = ((t[i] - s[i]) % m + m) % m;
bool flag = gauss();
if (!flag) {
cout << "niuza";
} else {
for (int i = 1; i <= n; i++)
cout << ans[i] << ' ';
}
return 0;
}
总结
高斯还是太强了,%%%。