矩阵简介
矩阵(Matrix)可以看作一个二维数组,就是一个有n行m列的填满数的数字阵。一个矩阵可以写作:
关于矩阵论的最基础理论可以参考1,这本书从需要的基础知识讲起,通过线性方程和向量空间、线性无关组、包和维度、矩阵的秩等概念逐步深入矩阵理论,之后引出行列式的计算与构造。在第三章还给出了近世代数的一些基本概念。对与OI来说,这本书的内容(从知识上)应该已经足够。
求解线性方程
通过直观的高斯消元法可以在
bzoj1013球形空间产生器
题意:给你n+1个n维空间中的点,求他们所在的球的球心。
我们大概都还记得平面上不同线的三点确定一个圆。这是为什么呢?设三点坐标为
也就是:
由于
当且仅当
n和n+1只要仿照这里就可以了。
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
double A[N][N];
int n;
void solve()
{
for (int i = 1; i <= n+1; i++) {
if (A[i][i] == 0) {
int k = i;
while (A[k][i]) k++;
swap(A[k], A[i]);
}
for (int j = i+1; j <= n; j++) {
for (int k = i+1; k <= n+1; k++)
A[j][k] -= A[i][k]*(A[j][i]/A[i][i]);
A[j][i] = 0;
}
}
for (int i = n; i > 1; i--) {
for (int j = 1; j < i; j++)
A[j][n+1] -= A[i][n+1]*(A[j][i]/A[i][i]), A[j][i] = 0;
}
for (int i = 1; i < n; i++)
printf("%.3f ", A[i][n+1]/A[i][i]);
printf("%.3f", A[n][n+1]/A[n][n]);
}
int main()
{
scanf("%d", &n);
double a[N], d[N];
for (int i = 1; i <= n; i++) scanf("%lf", &a[i]);
for (int i = 2; i <= n+1; i++) {
double ans = 0;
for (int j = 1; j <= n; j++)
scanf("%lf", &d[j]), A[i-1][j] = 2*(d[j]-a[j]), ans += d[j]*d[j]-a[j]*a[j];
A[i-1][n+1] = ans;
}
solve();
return 0;
}
求解带环dp
当dp方程并不满足最优子结构,即构成的决策图上有环时,如果决策会收敛(比如概率的指数衰减可以收敛),那么可以通过列方程求解。
HNOI2013 游走
一个无向连通图,顶点从1编号到N,边从1编号到M。
小Z在该图上进行随机游走,初始时小Z在1号顶点,每一步小Z以相等的概率随机选 择当前顶点的某条边,沿着这条边走到下一个顶点,获得等于这条边的编号的分数。当小Z 到达N号顶点时游走结束,总分为所有获得的分数之和。
现在,请你对这M条边进行编号,使得小Z获得的总分的期望值最小。
思路与解: 我们考虑用一个转移方程描述这个问题。设
解出该方程
然而我们显然不能随手写出
返回来看我们一开始的思路,其有大量优化余地的原因是右侧矩阵虽然是
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int g[N][N], n, m;
double A[N][N];
const double eps = 1e-9;
void solve(double ans[])
{
for (int i = 1; i < n; i++) {
int pos = i;
for (int k = i+1; k <= n; k++)
if (abs(A[k][i]) > abs(A[pos][i]))
pos = k;
if (pos != i)
swap(A[i], A[pos]);
for (int j = i+1; j <= n; j++) {
for (int k = i+1; k <= n+1; k++)
A[j][k] -= A[i][k]*(A[j][i]/A[i][i]);
A[j][i] = 0;
}
}
for (int i = n; i >= 1; i--) {
for (int j = 1; j < i; j++)
A[j][n+1] -= A[i][n+1]*(A[j][i]/A[i][i]), A[j][i] = 0;
ans[i] = A[i][n+1]/A[i][i];
}
}
double ans[N];
double E[N*N];
int top = 0;
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; scanf("%d%d", &u, &v);
g[u][v] = g[v][u] = 1;
g[u][u]++, g[v][v]++;
}
for (int i = 1; i < n; i++) {
A[i][i] = -1;
for (int j = 1; j <= n; j++)
if (g[i][j] && i != j)
A[i][j] = 1.0/g[j][j];
}
A[n][n] = 1, A[n][n+1] = 0, A[1][n+1] = -1;
solve(ans);
ans[n] = 1;
for (int i = 1; i < n; i++) {
for (int j = 1; j < i; j++)
if (g[i][j])
E[++top] = ans[i]*1.0/g[i][i]+ans[j]*1.0/g[j][j];
}
for (int i = 1; i < n; i++)
if (g[i][n])
E[++top] = ans[i]*1.0/g[i][i];
sort(E+1, E+m+1);
double a = 0;
for (int i = 1; i <= m; i++)
a += (m-i+1)*E[i];
printf("%.3f", a);
return 0;
}
Usaco2010 Hot Dotp驱赶猪猡
给定一个无向图,炸弹从1号点出发,每个时刻有P/Q的概率爆炸,如果没有爆炸,就会等概率沿着随机一条出边走到下一个城市,求最终每个城市的爆炸概率。
思路与解: 我们设
我们考虑最终答案
由于我们知道答案随i指数收敛,因此必然有
也就是无限递减几何级数。
然后只要求解矩阵方程:
嘴巴AC而已
行列式与Matrix_Tree定理
行列式是个奇怪的东西。它可以用许多方式定义,也有着丰富的应用,并且从每个应用都能引出对其的一种定义。矩阵树定理告诉我们:一个图的生成树个数等于其度数矩阵减去邻接矩阵,关于任一一个元素的余子式的绝对值。
bzoj4031 小Z的房间
Description
你突然有了一个大房子,房子里面有一些房间。事实上,你的房子可以看做是一个包含n*m个格子的格状矩形,每个格子是一个房间或者是一个柱子。在一开始的时候,相邻的格子之间都有墙隔着。
你想要打通一些相邻房间的墙,使得所有房间能够互相到达。在此过程中,你不能把房子给打穿,或者打通柱子(以及柱子旁边的墙)。同时,你不希望在房子中有小偷的时候会很难抓,所以你希望任意两个房间之间都只有一条通路。现在,你希望统计一共有多少种可行的方案。
Input
第一行两个数分别表示n和m。
接下来n行,每行m个字符,每个字符都会是.
或者*
,其中.
代表房间,*
代表柱子。
Output
一行一个整数,表示合法的方案数 Mod 10^9
思路与解: 裸题。随手A??
并不..注意到模数并不是素数,不能简单的用逆元表示除法。
我们需要一个被称为“模意义下行列式”的神奇方法。具体来说就是利用辗转相除的思路消元。辗转相除的过程中我们可以把
注意在消元的过程中只能严格的用两种初等变换,即
- 交换两行
- 将一行乘以常数,加到另一行
// heoi2015 小z的房间
#include <bits/stdc++.h>
using namespace std;
int p[10][10], num[15][15], d[一个101], top = 0;
long long g[101][101];
int n, m;
char str[10];
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
long long mod = 1e9;
long long Gauss(long long M[101][101], int n)
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
((M[i][j] %= mod) += mod ) %= mod;
int dsgn = 1;
for (int i = 1; i <= n; i++) {
if (M[i][i] == 0) {
int k = i+1;
while (k <= n && M[k][i] == 0) k++;
if (k > n) return 0; // 无解
swap(M[i], M[k]);
dsgn *= -1;
}
for (int j = i+1; j <= n; j++) {
if (M[j][i] > M[i][i])
swap(M[i], M[j]), dsgn *= -1;
long long A = M[i][i], B = M[j][i];
while (B) {
long long t = A/B; A %= B, swap(A, B);
for (int k = 1; k <= n; k++)
((M[i][k] -= t*M[j][k]) += mod) %= mod;
swap(M[i], M[j]);
dsgn *= -1;
}
}
}
long long ans = dsgn;
for (int i = 1; i <= n; i++)
(ans *= M[i][i]) %= mod;
return (ans += mod) %= mod;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%s", str+1);
for (int j = 1; j <= m; j++)
if (str[j] == '.')
p[i][j] = 1, num[i][j] = ++top;
else
p[i][j] = 0;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int k = 0; k < 4; k++)
if (num[i+dx[k]][j+dy[k]]) {
g[num[i][j]][num[i+dx[k]][j+dy[k]]] = -1;
g[num[i][j]][num[i][j]]++;
}
long long ans = Gauss(g, top-1);
cout << ans << endl;
return 0;
}
逆矩阵
我们知道非奇异n阶矩阵和矩阵乘法构成一群,也就是说每个元素都有逆元。考虑如何求解逆矩阵。使用Gauss-Jordan
方法,即对矩阵
bzoj4128 Matrix
对于非奇异矩阵A,B和一质数p,求满足
的最小的x。
思路与解:一眼的BSGS。就是设
然而这题卡常数…我每次循环使用三次乘法果断gg…使用有理有据的底层优化才能AC..
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 75;
int n, mod;
int inv[20005];
int power(int a, int n)
{
if (n == 0) return 1;
int p = power(a, n>>1);
p = p*p%mod;
if (n&1) p = p*a%mod;
return p;
}
void read(int A[MAXN][MAXN])
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
scanf("%d", &A[i][j]);
}
void put(int A[MAXN][MAXN])
{
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
printf("%d ", A[i][j]);
puts("");
}
}
int M[MAXN][MAXN], A[MAXN][MAXN], B[MAXN][MAXN], ret[MAXN][MAXN];
void inverse(int A[MAXN][MAXN], int ans[MAXN][MAXN])
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
M[i][j] = A[i][j], ans[i][j] = 0;
for (int i = 1; i <= n; i++) ans[i][i] = 1;
for (int i = 1; i <= n; i++) {
int pos = i;
for (register int k = i; k <= n; k++)
if (M[k][i] != 0) {
pos = k; break;
}
swap(M[i], M[pos]);
int iv = inv[M[i][i]];
for (int j = 1; j <= n; j++)
M[i][j] = M[i][j]*iv%mod, ans[i][j] = ans[i][j]*iv%mod;
for (register int j = 1; j <= n; j++) {
if (j == i) continue;
int tab = M[j][i];
for (register int k = 1; k <= n; k++) {
M[j][k] = ((M[j][k]-M[i][k]*tab)%mod+mod)%mod;
ans[j][k] = ((ans[j][k]-ans[i][k]*tab)%mod+mod)%mod;
}
}
}
}
void mul(int A[MAXN][MAXN], int B[MAXN][MAXN], int C[MAXN][MAXN])
{
memset(ret, 0, sizeof ret);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
int mid = n/4, t = mid*4;
for (register int k = 1; k < t; k += 4) {
ret[i][j] = (ret[i][j] + A[i][k]*B[k][j])%mod;
ret[i][j] = (ret[i][j] + A[i][k+1]*B[k+1][j])%mod;
ret[i][j] = (ret[i][j] + A[i][k+2]*B[k+2][j])%mod;
ret[i][j] = (ret[i][j] + A[i][k+3]*B[k+3][j])%mod;
}
ret[i][j] = (ret[i][j] + A[i][t+1]*B[t+1][j])%mod;
ret[i][j] = (ret[i][j] + A[i][t+2]*B[t+2][j])%mod;
ret[i][j] = (ret[i][j] + A[i][t+3]*B[t+3][j])%mod;
}
memcpy(C, ret, sizeof ret);
}
unsigned int hash_val(int A[MAXN][MAXN])
{
unsigned int val = 0;
for (int i = 1; i <= n; i++)
val = (val+A[i][i])*mod;
return val;
}
map<int,int> mp;
int I[MAXN][MAXN], C[MAXN][MAXN], D[MAXN][MAXN], T[MAXN][MAXN];
int main()
{
scanf("%d%d", &n, &mod);
for (register int i = 1; i < mod; i++)
inv[i] = power(i, mod-2);
for (int i = 1; i <= n; i++)
I[i][i] = 1;
read(A);
read(B);
int stp = sqrt(mod)+1;
memcpy(C, I, sizeof I);
for (int i = 0; i < stp; i++) {
mp[hash_val(C)] = i;
mul(C, A, C);
}
inverse(C, D);
memcpy(T, I, sizeof I);
for (int i = 0; i <= stp; i++) {
int ht = hash_val(B);
if (mp[ht]) {
cout << i*stp+mp[ht] << endl;
return 0;
}
mul(B, D, B);
}
return 0;
}
bzoj3168钙铁锌硒维生素
留一个坑。大概思路就是求是否存在一组非零向量使其通过A线性组合出一组向量。放在矩阵里一起处理即可。
线性基
就是对一个自然数集合
由于异或可以看成模2意义下的加法,线性基其实是S对应向量空间的极大线性无关组。由于矩阵行秩等于列秩,线性基中元素只有
如何构造一个线性基?只要在线的加入即可。注意逐位考虑。
bool add(long long num)
{
for (int i = 1; i <= tp; i++) {
if ((num^k[i]) < num && (num^k[i]) < k[i])
num ^= k[i];
}
if (num != 0) {
k[++tp] = num;
for (int i = tp; i-1 && k[i-1] < num; i--)
swap(k[i-1], k[i]);
return 1;
}
return 0;
}
2460: [BeiJing2011]元素
线性基裸题,只要贪心加入即可。
证明?由于不会有不相交的线性无关组,考虑两个序列,容易说明贪心策略不劣于任何策略。
#include <bits/stdc++.h>
using namespace std;
long long k[70], n, tp = 0;
struct p {
long long num, mag;
} ps[1005];
bool cmp(const p &a, const p &b)
{ return a.mag > b.mag; }
bool add(long long num)
{
for (int i = 1; i <= tp; i++) {
if ((num^k[i]) < num && (num^k[i]) < k[i])
num ^= k[i];
}
if (num != 0) {
k[++tp] = num;
for (int i = tp; i-1 && k[i-1] < num; i--)
swap(k[i-1], k[i]);
return 1;
}
return 0;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%lld%d", &ps[i].num, &ps[i].mag);
sort(ps+1, ps+n+1, cmp);
int ans = 0;
for (int i = 1; i <= n; i++)
if (add(ps[i].num))
ans += ps[i].mag;
cout << ans << endl;
return 0;
}
[Scoi2016]幸运数字
问树上路径走过的元素形成的集合的最大异或值。
思路与解: 由于所有元素的异或值和线性基的异或值等价,且线性基合并是满足结合律的,我们只要用树上倍增维护线性基合并即可。注意判断
#include <bits/stdc++.h>
using namespace std;
struct linear_basis {
long long k[64];
int top;
inline void clear() { top = 0; }
linear_basis() { clear(); }
void push(long long num)
{
for (int i = 1; i <= top; i++)
if ((k[i]^num) < k[i] && (k[i]^num) < num)
num ^= k[i];
if (num) {
k[++top] = num;
for (int i = top; i-1 && k[i-1] < k[i]; i--)
swap(k[i-1], k[i]);
}
}
friend linear_basis operator + (linear_basis a, linear_basis b)
{
for (int i = 1; i <= a.top; i++)
b.push(a.k[i]);
return b;
}
long long max()
{
long long ans = k[1];
for (int i = 2; i <= top; i++)
if ((ans^k[i]) >= ans)
ans ^= k[i];
return ans;
}
void print()
{
for (int i = 1; i <= top; i++)
cout << k[i] << " ";
cout << endl;
}
} ;
const int MAXN = 20005;
linear_basis f[MAXN][16];
int fa[MAXN][16], depth[MAXN];
long long a[MAXN];
int n, q;
struct node {
int to, next;
} edge[MAXN*2];
int head[MAXN], top, u, v;
inline void push(int i, int j)
{ ++top, edge[top] = {j, head[i]}, head[i] = top; }
void dfs(int nd, int ft = 0)
{
fa[nd][0] = ft, f[nd][0].push(a[ft]), depth[nd] = depth[ft]+1;
for (int i = head[nd]; i; i = edge[i].next)
if (edge[i].to != ft)
dfs(edge[i].to, nd);
}
linear_basis lca(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
linear_basis lb;
for (int i = 0; i < 16; i++)
if ((depth[a]-depth[b])&(1<<i))
lb = lb + f[a][i], a = fa[a][i];
if (a == b) return lb;
for (int i = 15; i >= 0; i--)
if (fa[a][i] != fa[b][i]) {
lb = lb + f[a][i] + f[b][i];
a = fa[a][i], b = fa[b][i];
}
if (a != b) lb = lb + f[a][0] + f[b][0];
return lb;
}
int main()
{
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++) scanf("%lld", &a[i]), f[i][0].push(a[i]);
for (int i = 1; i < n; i++) {
scanf("%d%d", &u, &v);
push(u, v), push(v, u);
}
dfs(1);
for (int j = 1; j < 16; j++)
for (int i = 1; i <= n; i++)
fa[i][j] = fa[fa[i][j-1]][j-1];
for (int j = 1; j < 16; j++)
for (int i = 1; i <= n; i++)
f[i][j] = f[i][j-1]+f[fa[i][j-1]][j-1];
for (int i = 1; i <= q; i++) {
scanf("%d%d", &u, &v);
if (u == v) printf("%lld
", a[u]);
else printf("%lld
", lca(u, v).max());
}
return 0;
}
线性齐次递推
我们考虑一个递推式
则有
由于矩阵乘法满足结合律,可以用快速幂计算。
[HNOI2008]GT考试
阿申准备报名参加GT考试,准考证号为
他的不吉利数学
思路与解: 一个数位dp的套路,
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30;
int n, m, p;
char str[30];
int pi[30];
int trans[30][10];
int M[MAXN][MAXN];
void kmp_init()
{
pi[0] = pi[1] = 0;
int p = 0;
for (int i = 2; i <= m; i++) {
while (p && str[p+1] != str[i]) p = pi[p];
if (str[p+1] == str[i]) p++;
pi[i] = p;
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < 10; j++) {
int k = i;
while (k && str[k+1] != j+'0') k = pi[k];
if (str[k+1] == j+'0')
trans[i][j] = k+1, M[k+1][i]++;
else
M[0][i]++;
}
}
M[m][m] = 10;
}
int R[MAXN][MAXN];
void mul(int A[MAXN][MAXN], int B[MAXN][MAXN]) // A *= B
{
memset(R, 0, sizeof R);
for (int i = 0; i <= m; i++)
for (int j = 0; j <= m; j++)
for (int k = 0; k <= m; k++)
(R[i][j] += A[i][k]*B[k][j]) %= p;
for (int i = 0; i <= m; i++)
for (int j = 0; j <= m; j++)
A[i][j] = R[i][j];
}
int B[33][MAXN][MAXN], top = 0;
void quick_power(int A[MAXN][MAXN], int n)
{
if (n == 1) return;
++top;
for (int i = 0; i <= m; i++)
for (int j = 0; j <= m; j++)
B[top][i][j] = A[i][j];
quick_power(B[top], n>>1);
mul(B[top], B[top]);
if (n&1) mul(B[top], A);
for (int i = 0; i <= m; i++)
for (int j = 0; j <= m; j++)
A[i][j] = B[top][i][j];
}
int main()
{
scanf("%d%d%d%s", &n, &m, &p, str+1);
kmp_init();
quick_power(M, n);
int ans = 0;
for (int i = 0; i < m; i++)
(ans += M[i][0]) %= p;
cout << ans << endl;
return 0;
}
[HNOI2011]数学作业
给定n和m,求:
思路与解: 可以不同位数分开考虑。考虑第k位上的情况:
由于要记录i的信息,我们要扩充状态表示:
可以构造矩阵:
即可。注意取模的技巧..否则会
#include <bits/stdc++.h>
using namespace std;
long long n, m;
long long D[5][5];
void mul(long long A[5][5], long long B[5][5], long long C[5][5])
{
memset(D, 0, sizeof D);
for (int i = 1; i <= 4; i++)
for (int j = 1; j <= 4; j++)
for (int k = 1; k <= 4; k++)
(D[i][j] += A[i][k]*B[k][j]%m) %= m;
for (int i = 1; i <= 4; i++)
for (int j = 1; j <= 4; j++) {
C[i][j] = D[i][j];
if (C[i][j] < 0) throw;
}
}
long long B[5][5];
void matrix_power(long long A[5][5], long long n)
{
memset(B, 0, sizeof B);
for (int i = 1; i <= 5; i++) B[i][i] = 1;
while (n) {
if (n&1) mul(B, A, B);
mul(A, A, A); n >>= 1;
}
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
A[i][j] = B[i][j];
}
long long power(long long a, long long n, long long mod = 3e18)
{
long long t = 1;
while (n) {
if (n&1) t = t*a%mod;
a = a*a%mod;
n >>= 1;
}
return t;
}
long long M[5][5];
int main()
{
scanf("%lld%lld", &n, &m);
long long ret = 0;
for (int k = 1; k <= 19; k++) {
if (power(10, k-1) > n) break;
long long beg = power(10, k-1), tar = min(power(10, k)-1, n);
memset(M, 0, sizeof M);
M[1][1] = power(10, k, m), M[1][3] = 1;
M[2][1] = M[3][3] = M[3][4] = M[4][4] = 1;
matrix_power(M, tar-beg+1);
ret = ret*power(power(10, (tar-beg+1), m)%m, k, m)%m;
ret = (ret+M[1][3]*(beg%m)%m+M[1][4])%m;
}
cout << ret << endl;
return 0;
}
// bug记录,函数默认参数忘记下传gg...
// 尽量不用默认参数...
参考资料3
- 《代数学引论I(BA I)》,前苏联-柯斯特利金著,张英伯译,高等教育出版社 ↩
- http://blog.csdn.net/u013010295/article/details/47451451 ↩
- 某集训,妹主席ppt ↩