算法题练习 (第二周)
矩阵快速幂
前置知识
做这道题需要先了解 矩阵乘法 。
大致来说就是 $ 如:AB=C (解释:A_{i,k} * B_{k,j},就是 C 矩阵的第 i 行第 j 列就是 A 矩阵的第 i 行的每一个数乘以 B 矩阵的第 j 行的每一个数然后求和) $
那么理解了这个剩下的就简单了,因为剩下的其实就是快速幂。
题目:快速幂P1226
大致思路
- 先做个结构体存矩阵,对于 $ A^0 $ 我们规定其初 $ 1 \sim n $ 行 $ 1 \sim n $ 列为 $ 1 $ 之外,其余为 $ 0 $。
struct Matrix {
ll a[MAXN][MAXN];
Matrix() {//初始化
memset(a, 0, sizeof(a));
}
inline void build() {//按照定义构造矩阵
for (int i = 1; i <= n; i++)
a[i][i] = 1;
}
} a;
- 因为需要用到快速幂,所以 重载 一下运算符 $ \times $ ,将它改为矩阵乘法。
Matrix operator*(const Matrix &x, const Matrix &y) {
Matrix z;
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
z.a[i][j] = (z.a[i][j] + x.a[i][k] * y.a[k][j] % mod) % mod;//这里的取模是防止乘法时炸空间
return z;
}
- 快速幂想必都会吧,敲一个板子就可以了(Tips:
*=
不能使用,因为重载的是*
)。
Matrix matrix_ksm(Matrix base, ll exp) {
if (exp == 0) {
Matrix identity;
identity.build();
return identity;
}
Matrix half = matrix_ksm(base, exp >> 1);
half = half * half;
return (exp & 1) ? half * base : half;
}
完整代码
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
typedef long long ll;
const int MAXN = 105;
const int mod = 1e9 + 7;
int n;
struct Matrix {
ll a[MAXN][MAXN];
Matrix() { memset(a, 0, sizeof(a)); }
void build() { for (int i = 1; i <= n; i++) a[i][i] = 1; }
};
Matrix operator*(const Matrix &x, const Matrix &y) {
Matrix z;
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
z.a[i][j] = (z.a[i][j] + x.a[i][k] * y.a[k][j] % mod) % mod;
return z;
}
Matrix matrix_ksm(Matrix base, ll exp) {
if (exp == 0) {
Matrix identity;
identity.build();
return identity;
}
Matrix half = matrix_ksm(base, exp >> 1);
half = half * half;
return (exp & 1) ? half * base : half;
}
int main() {
Robin();
ll k;
cin >> n >> k;
Matrix a;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
cin >> a.a[i][j];
Matrix result = matrix_ksm(a, k);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j)
cout << result.a[i][j] << " ";
cout << "\n";
}
return 0;
}
KMP
朴素字符串的比较算法很慢,即 $ O(nm) $,只能说还有巨大的提升空间。
优化过的KMP理论达到 \(O(n+m)\)。
让我们看一下关键思路:
概念:最长公共前后缀
分开理解就是 前缀 和 后缀 两个概念。
前缀:所有子串(不包含最后一个字符)
e.g. 字符串
AB
的前缀是A
后缀:所有子串(不包含第一个字符)
e.g. 字符串
AB
的前缀是B
最长公共前后缀:前缀和后缀中最长的相同部分
e.g. 字符串
ABABC
的最长公共前后缀是AB
Tips:可以把这个字符串的前缀和后缀先求出来,然后比较就行了。
部分匹配表
就是记录当前位置的最长公共前后缀。
即,next[j]
表示模式串前j
个字符的最长公共前后缀长度。
e.g. 字符串
ABABC
的next
数组为-1,0,0,1,2
构造匹配表的方法
-
初始化: $ next_0 = -1 $ , $ k = -1 $ , $ j = 0 $
-
循环处理每个字符:
-
若 $ k \ge 0 $ 且 $ P_j \ne P_k $ ,则 $ P_k = N_k $
-
若 $ P_j = P_k $ ,则 $ k = k + 1 $
-
更新 $ N_{j+1} = k $ , $ j = j + 1 $
- 匹配过程
初始化指针:主串指针i=0,模式串指针j=0。
逐个比较:
-
若 $ j = -1 $ 或当前字符匹配,则 i 和 j 均加1。
-
若失配,则 $ j = N_j $(原匹配串跳转)。
终止条件:
-
若j超过模式串长度,匹配成功。
-
若i超过主串长度,匹配失败。
标解
下面呈上标准程序:
#include <bits/stdc++.h>
#define MAXN 1000010
using namespace std;
int kmp[MAXN];
int la,lb,j;
char a[MAXN],b[MAXN];
int main(){
cin>>a+1;
cin>>b+1;
la=strlen(a+1);
lb=strlen(b+1);
for (int i=2;i<=lb;i++){
while(j&&b[i]!=b[j+1])
j=kmp[j];
if(b[j+1]==b[i])j++;
kmp[i]=j;
}
j=0;
for(int i=1;i<=la;i++){
while(j>0&&b[j+1]!=a[i])
j=kmp[j];
if (b[j+1]==a[i])
j++;
if (j==lb) {cout<<i-lb+1<<endl;j=kmp[j];}
}
for (int i=1;i<=lb;i++)
cout<<kmp[i]<<" ";
return 0;
}
总结
这个算法是很大程度上解决了朴素字符串比较的劣势,提升了字符串比较的速度。
总而言之,KMP是信奥字符串类型的一个极重要的算法。
最小生成树
概念
在理解最小生成树之前,我们需要把最小生成树拆解成三个部分:
并查集 图 树
解析
由于题目需要我们把图中的每个点排序,然后找到最小的最优方案,所以我们可以把它看作是一棵树。
每次插入一个结点,由于不能构成回路,所以我们是按照每一层来进行构造的。
综上,我们只需要找到两个结点的公共祖先,也就是需要用到并查集。
标程
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
struct st {
int s, e, w;
} a[200005];
int f[200005];
bool cmp(st a, st b) {
return a.w < b.w;
}
int find(int a) {
if (f[a] == a)
return a;
else
return f[a] = find(f[a]);
}
int main() {
int n, m, k;
cin >> n >> m >> k;
for (int i = 1; i <= n; i++)
f[i] = i;
for (int i = 1; i <= m; i++)
cin >> a[i].s >> a[i].e >> a[i].w;
sort(a + 1, a + 1 + m, cmp);
int cnt = 0, sum = 0;
for (int i = 1; i <= m; i++) {
if (find(a[i].s) != find(a[i].e)) {
f[find(a[i].s)] = find(a[i].e);
sum += a[i].w;
cnt++;
}
if (cnt >= n - k)
break;
}
if (cnt >= n - k)
cout << sum;
else
cout << "No Answer";
return 0;
}
树型DP
理解与分析
我们知道,题目需要我们找到树上点权之和最大的一个联通分量。
我们用一个 \(f_i\) 来记录 以 i 为根的子树中点权和最大的一棵子树(或只选根)
选择哪个点为根对结果没有影响,毕竟任一连通分量在任一时刻总是可以看成一棵以某个点为根的树
具体思路
本题的关键在于 \(f_i\) 的计算 。
∵ \(f_u\) 所表示的连通分量中必包含点 \(u\)
∴把 \(f_u\) 初始化为点 \(u\) 的点权 \(a_u\)
对于每一个边,我们都选择 剪 或者 不剪 ,以达到剪枝目的。
对于 \(u\) 的一个儿子 \(v\) ,显然当 $ f_v < 0$ 时就剪断 \(u − v\) 这条枝,反之亦然。
得到递推式:$ f_u = a_u + (f_v > 0) ? f_v : 0 $( \(v\) 为 \(u\) 的儿子)
标准程序
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
const int N = 2e4 + 10;
int n, a[N], f[N], ans = INT_MAX + 1;
vector<int> G[N];
void dfs(int u, int fa) {
f[u] = a[u];
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == fa)
continue;
dfs(v, u);
if (f[v] >= 1)
f[u] += f[v];
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, 0);
for (int i = 1; i <= n; i++)
ans = max(ans, f[i]);
cout << ans << endl;
return 0;
}
Dijisktra
算法思路
-
使用Dijkstra算法两次:第一次在正向图gr中计算从节点1到各节点的最短距离(dis1数组)。
-
第二次在反向图re中计算从节点1出发的最短距离(dis2数组),这相当于原图中各节点到节点1的最短路径。
-
最终累加所有节点(除1)的dis1[i] + dis2[i]作为总和输出。
标程
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
typedef long long LL;
const int MAXN = 1005;
const LL INF = 0x3f3f3f3f3f3f3f3fLL;
int n, m;
vector<pair<int, int> > gr[MAXN];
vector<pair<int, int> > re[MAXN];
LL dis1[MAXN], dis2[MAXN];
bool vis[MAXN];
void dijkstra(int st, vector<pair<int, int> > adj[], LL dis[]) {
memset(vis, 0, sizeof(vis));
fill(dis, dis + MAXN, INF);
dis[st] = 0;
priority_queue<pair<LL, int>, vector<pair<LL, int> >, greater<pair<LL, int> > > pq;
pq.push(make_pair(0, st));
while (!pq.empty()) {
pair<LL, int> top = pq.top();
pq.pop();
int u = top.second;
if (vis[u])
continue;
vis[u] = true;
for (size_t i = 0; i < adj[u].size(); ++i) {
int v = adj[u][i].first;
int w = adj[u][i].second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
pq.push(make_pair(dis[v], v));
}
}
}
}
int main() {
Robin();
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
gr[u].push_back(make_pair(v, w));
re[v].push_back(make_pair(u, w));
}
dijkstra(1, gr, dis1);
dijkstra(1, re, dis2);
LL total = 0;
for (int i = 2; i <= n; ++i) {
total += dis1[i] + dis2[i];
}
cout << total << endl;
return 0;
}
字符串
运用到 string
类里面的函数 substr
和 find
。
方法思路
-
读入:把两个长度不一的字符串读入,同时进行一些处理方便后续进行处理。
-
遍历:从较小的字符串长度开始,逐步递减至 \(0\) ,检查每种可能的字符串长度。
-
匹配:匹配对于每个长度,检查第一个字符串的前缀是否与第二个字符串的后缀匹配,或者第二个的前缀。
贪心
思路
大体思路就是先走斜的方向,然后再走直线方向,因为这样会节省步数,最优长度就是存储的字符串长度。
标解
#include <bits/stdc++.h>
using namespace std;
int main() {
char a, b, c, d;
cin >> a >> b >> c >> d;
int x1 = a - 'a' + 1, y1 = b - '0';
int x2 = c - 'a' + 1, y2 = d - '0';
int dx = x2 - x1;
int dy = y2 - y1;
vector<string> arr;
while (dx != 0 || dy != 0) {
string dir;
if (dx > 0 && dy > 0) {
arr.push_back("RU");
dx--;
dy--;
} else if (dx > 0 && dy < 0) {
arr.push_back("RD");
dx--;
dy++;
} else if (dx < 0 && dy > 0) {
arr.push_back("LU");
dx++;
dy--;
} else if (dx < 0 && dy < 0) {
arr.push_back("LD");
dx++;
dy++;
} else if (dx > 0) {
arr.push_back("R");
dx--;
} else if (dx < 0) {
arr.push_back("L");
dx++;
} else if (dy > 0) {
arr.push_back("U");
dy--;
} else {
arr.push_back("D");
dy++;
}
}
cout << arr.size() << endl;
for (int i = 0; i < arr.size(); i++) {
cout << arr[i] << endl;
}
return 0;
}
周总结
这一周主要是完成了关于算法模板的类型题目。
例如:单源最短路径 并查集 矩阵快速幂 KMP 最小生成树 等
总共完成 15 道题,部分题目没有写总结。
算是达成了预期,但是对于题单剩下来的题目基本都是 上位黄 绿 蓝 这些题目,所以下一周的预期定在13题左右。
这一周做的不好的地方是很多题目没能花费少量次数AC,下面希望能花费更少的次数AC题目。
对于模板题,都是需要经常复习和巩固的,所以后面也需要把这些题重复刷一遍。