算法题练习 (第二周)

矩阵快速幂

前置知识

做这道题需要先了解 矩阵乘法

大致来说就是 $ 如:AB=C (解释:A_{i,k} * B_{k,j},就是 C 矩阵的第 i 行第 j 列就是 A 矩阵的第 i 行的每一个数乘以 B 矩阵的第 j 行的每一个数然后求和) $

那么理解了这个剩下的就简单了,因为剩下的其实就是快速幂。

题目:快速幂P1226

大致思路

  1. 先做个结构体存矩阵,对于 $ 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;
  1. 因为需要用到快速幂,所以 重载 一下运算符 $ \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;
}
  1. 快速幂想必都会吧,敲一个板子就可以了(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. 字符串 ABABCnext 数组为 -1,0,0,1,2

构造匹配表的方法

  1. 初始化: $ next_0 = -1 $ , $ k = -1 $ , $ j = 0 $

  2. 循环处理每个字符:

  • 若 $ 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 $

  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

算法思路

  1. 使用Dijkstra算法两次:第一次在正向图gr中计算从节点1到各节点的最短距离(dis1数组)。

  2. 第二次在反向图re中计算从节点1出发的最短距离(dis2数组),这相当于原图中各节点到节点1的最短路径。

  3. 最终累加所有节点(除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 类里面的函数 substrfind

方法思路

  1. 读入:把两个长度不一的字符串读入,同时进行一些处理方便后续进行处理。

  2. 遍历:从较小的字符串长度开始,逐步递减至 \(0\) ,检查每种可能的字符串长度。

  3. 匹配:匹配对于每个长度,检查第一个字符串的前缀是否与第二个字符串的后缀匹配,或者第二个的前缀。

贪心

题目链接

思路

大体思路就是先走斜的方向,然后再走直线方向,因为这样会节省步数,最优长度就是存储的字符串长度。

标解

#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题目。

对于模板题,都是需要经常复习和巩固的,所以后面也需要把这些题重复刷一遍。

posted @ 2025-03-09 12:23  Easoncalm  阅读(12)  评论(0)    收藏  举报