简单线性代数
向量
向量线性相关:两条向量在同一直线上,此时变换完的空间会被压少若干维。反之则不是线性相关。
线性变换:经过一个 \(A\) 的线性变换后,得到的坐标系仍然平行且等距。
秩(rank):行列式在空间上形成的维度,或是高斯消元时的主元(非 0)个数,即他们线性无关。
貌似下面有一些地方把 基向量 写成 基向量 了,请仔细甄别。
矩阵乘法
若现在有两个矩阵 \(A,B\),定义他们的矩阵乘为:
for(int i = 1; i <= 2; i++)
for(int j = 1; j <= 2; j++)
for(int k = 1; k <= 2; k++)
res.a[i][j] += a[i][k] * b.a[k][j];
请自行意会。具体就是 \(A\) 每一行和 \(B\) 每一列相乘再加在一起。
几何意义:
先定义基向量是一个 \(m\) 维空间中的一个类似 单位长度 的向量。如二维平面就有
拓展到 \(m\) 维就是一个 \(m\) 行 \(1\) 列的向量。
对于原空间上的一个向量 \(A\),他有着 \(m\) 个数字来确定这个向量的终点,我们想让他变换,一个普遍的方式是平移,旋转。那其实可以通过操控这 \(m\) 个基向量来实现。具体的就是 \(A\) 乘上这个基向量,可以在这个基向量控制的这一维度上进行移动。
现在一个向量集合 \(B\) 中是 \(n\) 个 \(m\) 维向量,现在他们要做变换,而 \(A\) 中是 \(m\) 个对应维上的 \(m\) 维基向量。那么 \(AB\) 就代表 \(B\) 中的每个向量根据这些基向量做完变换后的向量集的情况。这恰好就对应了上文的 \(B\) 每一行(一个向量)和 \(A\) 每一列(一个基向量)相乘的原理。
其实了解这个几何意义貌似没有什么用处,大抵是可以和向量一起理解,以及这是了解矩阵乘法是如何推出的。
实际应用上,大多都是用一个 \(m\) 行 \(1\) 列的向量进行变换。
考虑何时会降维,当有一个基向量和其他若干基向量线性相关时,那么这个基向量就可以被其他的这些基向量表示出来,也就相当于这个基向量对于维数是没有作用的。但是本来就只有 \(m\) 个基向量也同时有 \(m\) 个维度,损失一个基向量就代表着你会损失这一个维度,也就产生了降维。
应用
这个有啥用呢,可以快速处理递推的第 \(k\) 项。
比如我们现在有一个斐波那契数列 \(f(n)=f(n-1)+f(n-2)\)。
那么我们尝试把它丢到矩阵上,就是我们现在有的向量是
我们想推出
那么我们现在的基向量就是 \(2\) 维的:
因为 \(f(n)=f(n-1)+f(n-2)\),那么我们显然 \(f(n-1)\) 和 \(f(n-2)\) 对新的向量中的 \(f(n)\) 贡献,具体的,我们在基向量上填上:
然后要补上新向量的 \(f(n-1)\),其实就是直接等于旧向量的 \(f(n-1)\) 部分,即:
那么就补完了,将 \(f(1)=1,f(2)=1\) 填进初始的向量 \(A\) 中,答案就是
那么 ans.a[1][1] 就是 \(f(n)\) 了。
summary: 其实就是第 \(m\) 行基向量的取值决定了 \(ans\) 第 \(m\) 行的值,根据需要推出基向量这一行的取值。
::::info[code]
注意这里的 \(A\) 才是基向量。
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 5e3 + 10, M = 2e2 + 10, inf = 1e18, mod = 1e9 + 7;
using namespace std;
typedef pair<int, int> pii;
struct Matr
{
int a[3][3];
Matr() { memset(a, 0, sizeof a); }
Matr operator * (const Matr &b) const
{
Matr res;
for(int i = 1; i <= 2; i++)
for(int j = 1; j <= 2; j++)
for(int k = 1; k <= 2; k++)
(res.a[i][j] += a[i][k] * b.a[k][j]) %= mod;
return res;
}
};
Matr mpower(Matr a, int b)
{
Matr ans; ans.a[1][1] = ans.a[2][2] = 1;
while(b)
{
if(b & 1) ans = ans * a;
a = a * a, b >>= 1;
}
return ans;
}
int n;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
if(n <= 2) return cout << "1\n", 0;
Matr A; A.a[1][1] = A.a[1][2] = A.a[2][1] = 1;
cout << mpower(A, n - 1).a[1][1] << "\n";
return 0;
}
/*
*/
::::
AT_dp_r
设 \(dp_{i,j,t}\) 表示 \(i \to j\) 走了 \(t\) 步的方案数,那么类似 floyd 的,我们有转移:
欸你发现,这个 dp 如果去掉 \(t\) 维时恰好与矩阵乘法的形式一模一样。显然的,我们有答案 \(ans=\sum dp_{i,j,t}\)。又因为现在的形式是 \(f_t=f_{t-1} \times f_1\),所以 \(ans=f_K=G^K\)。
::::info[code]
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 1e2 + 10, M = 2e2 + 10, inf = 1e18, mod = 1e9 + 7;
using namespace std;
typedef pair<int, int> pii;
int n, K;
struct Matr
{
int a[N][N];
Matr() { memset(a, 0, sizeof a); }
Matr operator * (Matr &b) const
{
Matr res;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
for(int k = 1; k <= n; k++)
(res.a[i][j] += a[i][k] * b.a[k][j] % mod) %= mod;
return res;
}
};
Matr A;
Matr mpower(Matr a, int b)
{
Matr res;
for(int i = 1; i <= n; i++) res.a[i][i] = 1;
while(b)
{
if(b & 1) res = res * a;
a = a * a, b >>= 1;
}
return res;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> K;
for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) cin >> A.a[i][j];
Matr ans = mpower(A, K);
int sum = 0;
for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) (sum += ans.a[i][j]) %= mod;
cout << sum << "\n";
return 0;
}
/*
dp_{i,j,t}=\sum_{k=1}^{n} dp_{i,k,t-1} \times dp_{k,j,1}
*/
::::
P6569
蓝的码字了,直接贴 Reynard 的:

::::info[code]
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 1e2 + 10, M = 2e2 + 10, inf = 1e18, mod = 2009;
using namespace std;
typedef pair<int, int> pii;
int n, K;
struct Matr
{
int a[N][N];
Matr() { memset(a, 0, sizeof a); }
Matr operator * (Matr &b) const
{
Matr res;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
for(int k = 1; k <= n; k++)
(res.a[i][j] ^= a[i][k] * b.a[k][j]);
return res;
}
};
struct Vec
{
int a[N];
Vec() { memset(a, 0, sizeof a); }
};
Vec operator * (Matr &b, Vec &a)
{
Vec res;
for(int j = 1; j <= n; j++)
for(int k = 1; k <= n; k++)
(res.a[j] ^= a.a[k] * b.a[k][j]);
return res;
}
Matr A;
Matr mpower(Matr a, int b)
{
Matr res;
for(int i = 1; i <= n; i++) res.a[i][i] = 1;
while(b)
{
if(b & 1) res = res * a;
a = a * a, b >>= 1;
}
return res;
}
int m, q, a[N];
Matr g[33];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> q;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1, x, y; i <= m; i++) cin >> x >> y, g[0].a[x][y] = g[0].a[y][x] = 1;
for(int i = 1; i < 32; i++) g[i] = g[i - 1] * g[i - 1];
for(int t = 1, x; t <= q; t++)
{
cin >> x;
Vec f;
for(int i = 1; i <= n; i++) f.a[i] = a[i];
for(int i = 0; i < 32; i++) if((x >> i) & 1) f = g[i] * f;
cout << f.a[1] << "\n";
}
return 0;
}
/*
*/
::::
广义的矩阵乘法
综上我们可以定义广义矩阵乘法为:
对于矩阵 \(A_{n×m}\) 和 \(B_{m×p}\),定义 \(C_{i,j}= \oplus^m_ {k=1}(A_{i,k}\otimes B_{k,j})\),我们将其称为 \((\oplus,\otimes)\) 的矩阵乘法。
其中需要满足:
- \(\oplus\) 满足交换律,结合律。
- \(\otimes\) 满足结合律。
- \(\otimes\) 对于 \(\oplus\) 满足分配律。
常见的 \((\oplus,\otimes)\) 组合有 \((+,\times)\),\((\min,+)\),\((\max,+)\),\((\vee,\wedge)\),\((\otimes,\wedge)\)。注意不同运算下的零元(转移不存在)、单位元(原地不动)和单位矩阵也需要重新定义。
高斯消元
逆矩阵:定义 \(n\) 阶方阵 \(A\),若存在方阵 \(B\) 使得:
其实没太听懂。本质上是模拟做一个加减消元求解线性方程组的过程。

高斯消元从左到右处理每一列,每次尝试在当前列找到一个主元。
主元:当前列中选出的一个非零元素,用它消掉其他行在这一列的系数。
设有m个方程、n个未知数,构造增广矩阵 \([a|\vec{b}]\)。就是把 \(\vec{b}\) 和 \(a\) 放在一起。
- 从左到右枚举变量列;
- 在当前还没有处理的行中,寻找这一列的非零元素作为主元;
- 如果找到了主元,将主元所在行交换到当前行;
- 将主元归一化,并用主元消去其他行在这一列的系数;
- 进入下一行,继续处理后面的列;
- 消元结束后判断是否存在矛盾行,并根据主元个数判断解的类型;
- 若需要求具体解,则根据主元列和自由变量进行回代。
其实就是在每一列中确定一个主元让它的系数控制为 \(1\),那么它需要变为 \(1\) 的话要把它同行的元素同比进行放缩,同列的元素进行加减。就是在它这一列只保留下它自己为 \(1\)。
若这一列能找到主元,那么这个维度上的向量就与其他向量线性无关,换句话说就是方程组的秩(rank)可以 \(+1\)。
那么这个就是原始的高斯消元,最后可以形成一个右上角有数的一个矩阵,如:
此时右下角的 \(1\) 可以直接解出当前这个元的解,回代就是带进上面的数。
还存在更高级的高斯-约旦消元。
不仅消去主元下面的元素,也消去主元上面的元素。最后左边接近单位矩阵,可以直接读出答案。
意思就是不只是消下面的行,上面的行也一起消,最后可以形成:
其中 \(a,b,c\) 是上面通过计算已知数。这样不需要回代。
::::info[code]
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 1e2 + 10, M = 2e2 + 10, inf = 1e18, mod = 2009;
const double eps = 1e-6;
using namespace std;
typedef pair<int, int> pii;
int n;
double a[N][N];
int rnk; // 秩
void gauss()
{
vector<int> where(n + 1);
for(int col = 1; col <= n; col++) // 枚举的是列
{
int p = rnk + 1;
while(p <= n && fabs(a[p][col]) < eps) p++; // 找当前列的非 0 元素
if(p > n) continue;
swap(a[p], a[++rnk]); where[col] = rnk;
double pivot = a[rnk][col]; // 先存下主元的系数,不然一会会被直接除成 0
for(int j = col; j <= n + 1; j++) a[rnk][j] /= pivot; // 这里枚举的是列
for(int i = 1; i <= n; i++) // Jordan 直接全部删完 现在枚举的是行
{
if(i == rnk || fabs(a[i][col]) < eps) continue;
double t = a[i][col];
for(int j = col; j <= n + 1; j++) a[i][j] -= t * a[rnk][j];
}
}
for(int i = 1; i <= n; i++)
{
bool zro = 1;
for(int j = 1; j <= n; j++) if(fabs(a[i][j]) > eps) { zro = 0; break; }
if(zro && fabs(a[i][n + 1]) > eps) return cout << "-1", void();
}
if(rnk != n) return cout << "0\n", void();
for(int i = 1; i <= n; i++)
{
cout << "x" << i << "=";
// cout << where[i] << "\n";
cout << fixed << setprecision(10) << a[where[i]][n + 1] << "\n";
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) for(int j = 1; j <= n + 1; j++) cin >> a[i][j];
gauss();
return 0;
}
/*
*/
::::
要求输出方程组 \(A_x=0\) 解空间的维数,一组特解和每个维度上的一个基。
我们之前求出来的秩是线性无关的主元个数,但是要输出的是维度,维度就是 解可以自由移动的空间。自由移动,那自然就是自由元造成的。自由元可以任意取值,能造成解的移动,那么第一问输出 \(m-rank\)。
一组特解就和上面那个是一样的。每一个维度上的基就是把这一维上的自由元设为 \(1\) 后求出其他解。几何意义就是这个空间上的一个基向量。
::::info[code]
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 1e3 + 10, M = 2e2 + 10, inf = 1e18, mod = 998244353;
const double eps = 1e-6;
using namespace std;
typedef pair<int, int> pii;
int n, m, a[N][N];
int power(int a, int b)
{
int ans = 1;
while(b)
{
if(b & 1) ans = ans * a % mod;
a = a * a % mod, b >>= 1;
}
return ans;
}
int rnk;
void gauss()
{
vector<int> where(m + 1);
for(int col = 1; col <= m && rnk < n; col++)
{
int p = rnk + 1;
while(p <= n && a[p][col] == 0) p++;
if(p > n) continue;
swap(a[p], a[++rnk]); where[col] = rnk;
int inv = power(a[rnk][col], mod - 2);
for(int j = col; j <= m + 1; j++) (a[rnk][j] *= inv) %= mod;
for(int i = 1; i <= n; i++)
{
if(i == rnk || a[i][col] == 0) continue;
int t = a[i][col];
for(int j = col; j <= m + 1; j++) (a[i][j] = a[i][j] - t * a[rnk][j] % mod + mod) %= mod;
}
}
for(int i = 1; i <= n; i++)
{
bool zro = 1;
for(int j = 1; j <= m; j++) zro &= (a[i][j] == 0);
if(zro && a[i][m + 1] != 0) return cout << "-1\n", void();
}
cout << m - rnk << "\n";
for(int i = 1; i <= m; i++) cout << a[where[i]][m + 1] << " ";
cout << "\n";
for(int i = 1; i <= m; i++)
{
if(where[i]) continue;
for(int j = 1; j <= m; j++)
{
if(i == j) cout << 1 << " ";
else cout << (-a[where[j]][i] + mod) % mod << " ";
}
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) cin >> a[i][j];
for(int i = 1; i <= n; i++) cin >> a[i][m + 1];
gauss();
return 0;
}
/*
*/
::::
线性基
感觉有点懂了。
线性基维护的是一个集合 \(S\),使得这个 \(S\) 中的元素相互异或可以表示出原数集的元素的所有相互异或的值,同时大大减少元素规模。
具体就是对于每一位只留下一个有价值的数。就比如第 \(1\) 位我就只留一个这一位上是 \(1\) 的数。
因为每个位只有 \(\{0,1\}\) 两个取值,所以如果每一位都可以表示出原数集上的 \(\{0, 1\}\) 就意味着 \(S\) 与原数集在我们现在的异或需求下本质相同,也就意味着\(S\) 中的元素相互异或可以表示出原数集的元素的所有相互异或的值。
想对于每一位只留下一个有价值的数,定义 \(p_i\) 为第 \(i\) 位上保留的数。若我当前想向线性基中插入一个数 \(x\),从 \(x\) 的高位开始,若 \(x\) 的最高位第 \(i\) 位为 \(1\):
-
\(p_i \neq 0\),这一位上有数字存在,那么这一位上的 \(\{0, 1\}\) 可以通过原来的 \(p_i\) 的是否选择来控制。因此 \(x\) 对于第 \(i\) 位没有贡献,但是 \(x \oplus p_i\) 呢?她可能可以在后续的位中占领某一个 \(p_j\),让他在 \(p_j\) 上继续贡献。
-
\(p_i= 0\) 这一位上没有数字存在。由上文可知,\(x\) 此时对于 \(i\) 这一位是有贡献的,因此 \(p_i \gets x\)。
这样就构造出来了一个线性基,每一个 \(p_i\) 要么为 \(0\) 要么就是一个第 \(i\) 位上为 \(1\) 的 \(x\)。
bool ins(int x) // 低配线性基,为普通高斯消元版,现在每一位是一个int
{
for(int i = 60; ~i; i--) if((x >> i) & 1ll)
{
if(p[i]) x ^= p[i];
else
{
rnk++, p[i] = x;
return 1;
}
}
return 0;
}
现在看看她为什么是对的。先考虑一个感性理解:若 \(p_i=a\),当前想加入的 \(b\) 在第 \(i\) 位上有数。我们现在将 \(b\) 变为 \(a \oplus b\)。想表示出原来的 \(a,b,a \oplus b\) 都是可行的,或者想要 \(b \oplus c\) 也有 \(a \oplus (a \oplus b) \oplus c=b \oplus c\)。因此可以表示出原来的所有的数。
或者再来一个严谨一点的。线性基,就是线性变换中的一个基。将每个数变成二进制后理解为一个线性方程组,每个元的系数只有 \(\{0,1\}\),也就是一个线性变换,那么我想要找出这个方程组的主元。因为主元就是每个二进制位上的一个 \(1\),也就是上文说的用来表示出 \(\{0,1\}\)。那么解线性方程组,可以使用高斯消元。
考虑高斯消元的一个过程,在第 \(rank\) 行我确定了一个主元,然后想要消去下面方程这一位上的 \(1\)。因为现在是异或那么消去系数就是两数异或,我们现在每次在线性基插入一个数等于将这个两数异或消去系数的操作延后了,但本质上还是相同的。最终,高斯消元将这个方程组消成了一个上三角,其中对角线为 \(1\) 就代表 \(p_i \neq 0\),更进一步的,\(p_i\) 此时就恰好等于这一行的十进制值。例如现在可能是这样一个矩阵:
那么 \(p_2\)(其实严谨的说是在二进制上的第 \(3\) 位所以是 \(p_3\),这里指标红的这行)的值就是 \(1\times 2^2+1\times2^1+0\times2^0=6\)。
getmax
找线性基里面的最大值。因为每一位的选择一定会让 \(res\) 这一位有变化,若 \(res\) 这一位为 \(0\) 的话就异或,这样一定会变大。
getmin
若线性基里面可以合成出 \(0\) 那么就是 \(0\) 了,反之就找一个线性基里面最小的值即可。
::::info[板子code]
约旦写法貌似有点局限性(?
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 1e6 + 10, M = 2e2 + 10, inf = 1e18, mod = 2009;
const double eps = 1e-6;
using namespace std;
typedef pair<int, int> pii;
int n, a[60];
struct LB
{
int p[65], rnk;
LB() { memset(p, 0, sizeof p); rnk = 0; }
bool ins(int x) // 低配线性基,为普通高斯消元版,现在每一位是一个int
{
for(int i = 60; ~i; i--) if((x >> i) & 1ll)
{
if(p[i]) x ^= p[i];
else
{
rnk++, p[i] = x;
return 1;
}
}
return 0;
}
bool ins(int x) // 约旦线性基,每一位只有 2^B
{
for(int i = 60; ~i; i--) if((x >> i) & 1ll)
{
if(p[i]) x ^= p[i];
else
{
for(int j = i - 1; ~j; j--) if((x >> j) & 1ll) x ^= p[j];
rnk++, p[i] = x;
for(int j = 60; j != i; j--) if((p[j] >> i) & 1) p[j] ^= x;
return 1;
}
}
return 0;
}
int get_max()
{
int res = 0;
for(int i = 60; ~i; i--) res ^= p[i];
return res;
}
}b;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i], b.ins(a[i]);
cout << b.get_max() << "\n";
return 0;
}
/*
*/
::::
同样的,高斯消元也可以使用高斯-约旦。具体就是看之前插进线性基中的数在我现在这一位是否为 \(1\) 来更新之前插入的数。这样操作的话最后留在线性基中的数一定都形如 \(2^k\)。
bool ins(int x) // 约旦线性基,每一位只有 2^B
{
for(int i = 60; ~i; i--) if((x >> i) & 1ll)
{
if(p[i]) x ^= p[i];
else
{
for(int j = i - 1; ~j; j--) if((x >> j) & 1ll) x ^= p[j];
rnk++, p[i] = x;
for(int j = 60; j != i; j--) if((p[j] >> i) & 1) p[j] ^= x;
return 1;
}
}
return 0;
}
约旦写法和普通写法一样,若返回 \(1\) 则代表这个值是有用的,即她与之前的数都线性无关;返回 \(0\) 就是这个数都线性相关。
其实你发现,约旦写法单次插入是 \(O(W^2)\),普通的插入单次是 \(O(W)\),写的时候可以自己感知一下。
ok 我感知完了,每次都约旦一下确实有点拉了,可以在最后做一遍 standard 来消掉。
void standard()
{
for(int i = 0; i <= 61; i++) if(p[i])
for (int j = i - 1; j >= 0; --j)
if (p[i] >> j & 1) p[i] ^= p[j];
}
全局异或第 k 大/小
你发现做了约旦版的线性基后,异或的所有情况就有 \(2^B\) 种,你又发现这个东西是升序的,因此第 \(K\) 小就是 \(2^B\) 里的第 \(K\) 个,去掉 \(p_i=0\) 后一位一位取就行。
inline int get_min(int k)
{
int res = 0;
vector<int> v;
for(int i = 0; i <= 61; i++) if(p[i]) v.pb(p[i]);
for(int i = 0; i < v.size(); i++) if((k >> i) & 1) res ^= v[i];
return res;
}
P4570
你发现她要求的是选出的元素全部线性无关,那么若一个更优的元素没有被选到,则可以通过调整前面的元素来得到,因此将 \(v_i\) 降序排序后贪心选即可。
::::info[code]
#include <bits/stdc++.h>
#define int long long
#define mk make_pair
#define fi first
#define se second
#define pb push_back
const int N = 1e6 + 10, M = 2e2 + 10, inf = 1e18, mod = 2008;
const double eps = 1e-6;
using namespace std;
typedef pair<int, int> pii;
int n, ans;
pii a[N];
struct LB
{
int p[65], rnk;
LB() { memset(p, 0, sizeof p); rnk = 0; }
bool ins(int x)
{
for(int i = 60; ~i; i--) if((x >> i) & 1ll)
{
if(p[i]) x ^= p[i];
else return ++rnk, p[i] = x, 1;
}
return 0;
}
}b;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i].se >> a[i].fi;
sort(a + 1, a + n + 1);
for(int i = n; i; i--) // v 从大到小拿
if(b.ins(a[i].se)) ans += a[i].fi;
cout << ans << "\n";
return 0;
}
/*
*/
::::

浙公网安备 33010602011771号