模板及讲解
高斯消元是什么
例题
我们发现给定的方程组解只和系数有关,所以将系数和常数提取出来构造矩阵:
例
$$
\begin{cases}
2x+y+z=1\\
6x+2y+z=-1\\
-2x+2y+z=7
\end{cases}
$$
则构造矩阵:
$$
\left[
\begin{array}{ccc|c}
2 & 1 & 1&1 \\
6 & 2 & 1&-1\\
-2&2&1&7
\end{array}
\right]
$$
高斯消元的操作有(初等行变换):
1、交换两行。
2、将一行用非零数乘。
3、将一行的若干倍加到另一行上。
也就是差不多是小学学的加减消元带入消元的操作。
高斯消元目标:
将原矩阵转化为一个阶梯形矩阵,即
$$
\left[
\begin{array}{ccc|c}
2 & 1 & 1&1 \\
0 & -1& -2&-4\\
0&0&-4&-4
\end{array}
\right]
$$
将后面的行带入上面的行,可得一个简化阶梯形矩阵
$$
\left[
\begin{array}{ccc|c}
1 & 0 & 0&-1 \\
0 & 1& 0&2\\
0&0&1&1
\end{array}
\right]
$$
直接可以读出每个元素的解。
高斯消元一般步骤:
逐步处理每一个$x_i$,将$i$列除了$i$行的其他系数都变为0,即我们可以先保证$i$行$i$列不为0,然后以这一行为基准将其他行$i$位置的系数都变为0。用初等行变换的交换可以满足第一个条件,用初等行变换将一行的若干倍加到另一行上可以做到第二个条件。做完之后只有对角线和常数有值,每一行的常数除以对角线的值即为第$i$个元素的解。
无解情况:系数都为0,常数不为0
无穷解情况:系数都为0,常数也为0
两个概念:
1、主元:确定唯一的未知量
2、自由元:不确定值未知量,对应情况为该行全为0,有无穷解
注意在判这两个之前要先判有没有不是对角线上的系数有值,如果有则这一行不进行检查。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
#include<cmath>
#define ms(i, j) memset(i, j, sizeof i)
#define LL long long
#define db double
#define fir first
#define sec second
#define mp make_pair
using namespace std;
namespace flyinthesky {
const int MAXN = 105;
const db eps = 1e-12;
int n;
db c[MAXN][MAXN], b[MAXN];
// c, b 为消元矩阵
void clean() {
ms(b, 0);
}
int solve() {
clean();
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) scanf("%lf", &c[i][j]);
scanf("%lf", &b[i]);
}
for (int i = 1; i <= n; ++i) { // 处理 xi[i] 的系数
for (int j = i; j <= n; ++j) { // 从后面没处理的行找 xi[i] 系数不为 0 的行交换
if (fabs(c[j][i]) > eps) {
swap(b[i], b[j]);
for (int k = 1; k <= n; ++k) swap(c[i][k], c[j][k]);
}
}
if (fabs(c[i][i]) < eps) continue ; // 注意
for (int j = 1; j <= n; ++j) { // 消去其他方程 xi[i] 的系数
if (i == j) continue ;
db rat = c[j][i] / c[i][i];
for (int k = 1; k <= n; ++k) c[j][k] -= rat * c[i][k];
b[j] -= rat * b[i];
}
}
int bj = 1;
for (int i = 1; i <= n; ++i) {
int fl = 0;
for(int j = 1; j <= n; ++j){
if (i != j && fabs(c[i][j]) > eps) {fl = 1; break ;}
}
if (fl) continue ; // 必须判
if (fabs(c[i][i]) < eps && fabs(b[i]) > eps) bj = -1;
if (fabs(c[i][i]) < eps && fabs(b[i]) < eps && bj != -1) bj = 0;
}
if (bj != 1) return printf("%d\n", bj), 0;
for (int i = 1; i <= n; ++i) {
if (fabs(b[i] / c[i][i]) < eps) printf("x%d=0\n", i); else printf("x%d=%.2lf\n", i, b[i] / c[i][i]);
}
return 0;
}
}
int main() {
flyinthesky::solve();
return 0;
}
异或方程组
高斯消元可以用来解线性的异或方程组,方法与前者类似。
例题:poj 1830
设$x_i$为$i$开关是否(0/1)操作,$a_{i,j}$为$j$开关操作后是否(1/0)会影响$i$开关
则可以列出线性异或方程组
$$
\begin{cases}
a_{1,1}x_1 xor a_{1,2}x_2 xor … xor a_{1,n}x_n = st_1 xor ed_1 \\
a_{2,1}x_1 xor a_{2,2}x_2 xor … xor a_{2,n}x_n = st_2 xor ed_2 \\
\vdots \\
a_{n,1}x_1 xor a_{n,2}x_2 xor … xor a_{n,n}x_n = st_n xor ed_n
\end{cases}
$$
然后对于异或实际上我们也可以进行消元得到简化阶梯形矩阵,并且不需要乘除,因为只有 0 和 1。
可以用 bitset
加速运算且代码更简洁高效。
无解即为形如$0 xor 0 xor … xor 0 =1$的,显然不成立。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
#include<bitset>
#define ms(i, j) memset(i, j, sizeof i)
#define LL long long
#define db double
#define fir first
#define sec second
#define mp make_pair
using namespace std;
namespace flyinthesky {
const int MAXN = 35;
int n, a[MAXN];
bitset<MAXN> bs[MAXN];
int ksm(int a, int b) {
int bs = a, ans = 1;
while (b) {
if (b & 1) ans *= bs;
bs *= bs;
b >>= 1;
}
return ans;
}
void clean() {
}
int solve() {
clean();
cin >> n;
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
for (int x, i = 1; i <= n; ++i) scanf("%d", &x), a[i] ^= x;
for (int i = 1; i <= n; ++i) bs[i].reset(), bs[i][0] = a[i], bs[i][i] = 1;
int x, y;
while (scanf("%d%d", &x, &y) == 2 && x && y) bs[y][x] = 1;
int ans = 1, whw = 0;
for (int i = 1; i <= n; ++i) { // 每列
if (bs[i][i] == 0) { // 找一个非零的
for (int j = i + 1; j <= n; ++j) if (bs[j][i]) {swap(bs[j], bs[i]); break;}
}
if (bs[i][0] == 1 && bs[i].count() == 1) {ans = 0; break ;} // 无解
if (bs[i][i] == 0) ++whw; // 自由元
for (int j = i + 1; j <= n; ++j) if (bs[j][i]) bs[j] ^= bs[i];
}
if (ans == 0) printf("Oh,it's impossible~!!\n"); else printf("%d\n", ksm(2, whw));
return 0;
}
}
int main() {
int k; cin >> k;
while (k--) flyinthesky::solve();
return 0;
}
同余高斯消元
模板题:CF 1155E
求解带环期望DP
1、CF 24D:列与列之间不满足无后效性 (注意此时高斯消元做成阶梯矩阵即可,不需要简化,简化直接回带)
线性基
定义
一下内容部分来自 线性基学习笔记 | Menci’s Blog
异或和:设 $S$ 为一无符号整数集(下文中除非特殊说明,集合均指无符号整数集),定义其异或和为$S_1 xor S_2 xor \cdots xor S_{|S|}$。
线性相关:集合中存在一个元素$S_j$,使得集合中其他元素可以通过异或和的方式得到$S_j$,则$S$线性相关。若不存在,则$S$线性无关。
线性基:称集合 $B$ 是集合 $S$ 的线性基,当且仅当:
- $B$ 线性无关
- $B$ 中元素可以通过异或和方式得到$S$中的所有值
线性基性质:
- $B$ 是极小的满足线性基性质的集合,它的任何真子集都不可能是线性基
- $S$ 中的任意元素都可以唯一表示为 $B$ 中若干个元素异或起来的结果。
构造
构造出来的线性基相当于于所有插入值高斯消元后的简化阶梯矩阵。
设维护的数的二进制长度为$L$,线性基数组为$a_i(0 \leq i \leq L)$
为了保证线性无关,$a_i$的第$i$位若为$1$,则其他线性基上第$i$位都为$0$,并且$a_i$高于$i$位上的值都必须为$0$(否则会影响后面线性基的”其他线性基上第$i$位都为$0$”)。
所以构造方法即为(插入数$t$):
1、将$t$倒序枚举二进制位$i$,然后
2、若$t$的$i$位为0,则continue
否则:
- 如果$a_i$不为$0$,则用$a_i$消去$t$的$i$位上的二进制$1$,然后
continue
- 如果$a_i$为$0$,那么$t$就能插进$i$位。但是要先用$[1, i)$的线性基消去$t$的相应位置的$1$,以保证”其他线性基上第$i$位都为$0$”,然后再用得到的线性基消去$[i+1, L]$线性基的$i$位上的$1$,因为一路上我们都将高位消去,所以这里不用担心影响其他位的情况。
例题1 (集合异或和最大):Luogu 3812
给定$n$个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大。
对于构造的线性基,$a_i$的第$i$位若是$1$,则将答案异或$a_i$,这样一定能得到更大的值,因为这个位置的$1$之后不会再被异或消去,而因为二进制的性质,贪心选取更高位数的$1$,所以将所有线性基异或起来即为答案。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<set>
#include<cmath>
#define ms(i, j) memset(i, j, sizeof i)
#define LL long long
#define db double
#define fir first
#define sec second
#define mp make_pair
using namespace std;
namespace flyinthesky {
const LL MAXLEN = 51;
LL n, t, a[55];
void clean() {
ms(a, 0);
}
int solve() {
clean();
scanf("%lld", &n);
for (LL o = 1; o <= n; ++o) {
scanf("%lld", &t);
for (LL i = MAXLEN; i >= 0; --i) { // 倒序枚举二进制
if (!((t >> i) & 1)) continue ; // t 当前位不是 1
if (a[i]) t ^= a[i]; // 用 a[i] 消去 t 的 i 位上的 1
else {
for (LL j = 1; j < i; ++j) if ((t >> j) & 1) t ^= a[j]; // 用 a[j] 消去 t 低于 i 位上的 1
for (LL j = i + 1; j <= MAXLEN; ++j) if ((a[j] >> i) & 1) a[j] ^= t; // 用 t 消去 a[j] 的 i 位上的 1
a[i] = t; // 插入线性基
break ;
}
}
}
LL ans = 0;
for (LL i = MAXLEN; i >= 0; --i) ans ^= a[i];
printf("%lld\n", ans);
return 0;
}
}
int main() {
flyinthesky::solve();
return 0;
}
例题2 (选出集合不出现异或和等于0):Bzoj 3105
选出集合不出现异或和等于0即选出数线性无关,所以直接最大到最小贪心即可。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<queue>
#include<string>
#define ms(i, j) memset(i, j, sizeof i)
#define LL long long
#define db double
#define fir first
#define sec second
#define mp make_pair
using namespace std;
namespace flyinthesky {
int k, a[105], b[105];
int ins(int t) {
for (int i = 32; ~i; --i) {
if (!(t & (1 << i))) continue ;
if (a[i]) t ^= a[i];
else {
for (int j = 0; j < i; ++j) if (t & (1 << j)) t ^= a[j];
for (int j = i + 1; j <= 32; ++j) if (a[j] & (1 << j)) a[j] ^= t;
a[i] = t;
return 1;
}
}
return 0;
}
void clean() {
}
int solve() {
clean();
cin >> k;
for (int i = 1; i <= k; ++i) cin >> b[i];
sort(b + 1, b + 1 + k, greater<int>());
LL ans = 0;
for (int i = 1; i <= k; ++i) if (!ins(b[i])) ans += b[i];
cout << ans;
return 0;
}
}
int main() {
flyinthesky::solve();
return 0;
}
例题3 求集合异或和$k$大
我们考虑将秩编号,那么我们如果求$k$大, 我们就分解$k$的二进制, 然后按照二进制的位置异或秩就是答案,注意0的存在
例题4 求线性基的交 / 并
并很简单,一个插另一个即可
交
例题5 求集合异或和排名
类似例题三的做法,Bzoj 2844
此题还需要去重,我们发现每个数都出现一样的次数$2^{n-|\mathbb{lb}|}$
证明:假如要考虑异或出一个数$x$,基外选出的数的异或和为$s$,那么还需要$x \mathbb{xor} s$,对于它,基内的线性表示的方法是唯一的。