在算法的学习过程中,刷题是提升编程能力和逻辑思维的重要途径。最近我刷了几道不同类型的算法题,包括字符串处理、广度优先搜索(BFS)、动态规划以及哈夫曼编码等,这些题目涵盖了不同的算法思想,让我对算法的应用有了更深入的理解。现在我将这几道题的解题思路和实现过程整理成这篇博客,希望能对同样在算法学习路上的小伙伴们有所帮助。
一、包含不超过两种字符的最长子串(滑动窗口)
题目描述
给定一个长度为 n的字符串,找出最多包含两种字符的最长子串 t,返回这个最长的长度。字符串仅包含小写英文字母,数据范围:1 ≤ n ≤ 10^5。
解题思路
这道题可以使用滑动窗口(双指针) 的方法来解决。我们维护一个窗口 [left, right],窗口内的字符种类数不超过 2 种。具体步骤如下:
-
使用一个哈希表(这里用数组模拟,因为字符是小写字母,所以数组大小为 26)来统计窗口内每种字符的出现次数。
-
使用变量
count记录窗口内不同字符的种类数。 -
右指针
right不断向右移动,将当前字符加入窗口:- 如果该字符在窗口内的出现次数从 0 变为 1,说明窗口内多了一个新字符,
count加 1。
- 如果该字符在窗口内的出现次数从 0 变为 1,说明窗口内多了一个新字符,
-
当
count > 2时,需要移动左指针left缩小窗口,直到count ≤ 2:- 如果左指针指向的字符在窗口内的出现次数从 1 变为 0,说明窗口内少了一个字符,
count减 1。
- 如果左指针指向的字符在窗口内的出现次数从 1 变为 0,说明窗口内少了一个字符,
-
每次调整窗口后,更新最长子串的长度。
代码实现
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
cin >> s;
int left = 0, right = 0, n = s.size();
int hash[26] = {0}; // 统计窗口内每种字符出现的次数
int count = 0; // 统计窗口内一共有多少种字符
int ret = 0;
while (right < n) {
if (hash[s[right] - 'a']++ == 0) count++; // 0->1,窗口内多了一种字符
while (count > 2) {
if (hash[s[left++] - 'a']-- == 1) count--; // 1->0,窗口内少了一种字符
}
ret = max(ret, right - left + 1);
right++;
}
cout << ret << endl;
return 0;
}
二、迷宫出口问题(广度优先搜索 BFS)
题目描述
koton 在一个 n*m迷宫里,迷宫的最外层被岩浆淹没,无法涉足,迷宫内有 k个出口。koton 只能上下左右四个方向移动。她想知道有多少出口是她能到达的,最近的出口离她有多远?
解题思路
这道题是典型的**广度优先搜索(BFS)**应用场景。BFS 适合寻找最短路径,因为它按层遍历,第一次到达目标节点时的步数就是最短距离。具体步骤如下:
-
读取输入,找到 koton 的起始位置
(x1, y1)和所有出口的位置。 -
初始化一个距离数组
dist,用于记录每个位置到起始点的最短距离,初始值为 -1(表示未访问)。 -
使用队列进行 BFS,从起始点开始,依次访问其上下左右四个方向的相邻位置:
-
如果相邻位置在迷宫范围内、不是墙壁、且未被访问过,则更新其距离,并将其加入队列。
-
注意:如果是出口,也需要加入队列继续传播距离,因为可能其他出口的距离更近。
-
-
遍历结束后,统计所有出口中可到达的数量和最小距离。
代码实现
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 35;
int x1, y1;
int n, m;
char arr[N][N];
int dist[N][N];
queue<pair<int, int>> q;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
void bfs() {
memset(dist, -1, sizeof dist);
dist[x1][y1] = 0;
q.push({x1, y1});
while (q.size()) {
auto [x2, y2] = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int a = x2 + dx[i], b = y2 + dy[i];
if (a >= 1 && a <= n && b >= 1 && b <= m && dist[a][b] == -1 && arr[a][b] != '*') {
dist[a][b] = dist[x2][y2] + 1;
if (arr[a][b] != 'e') { // 出口不需要再入队,因为已经是终点,但其他路径可能经过
q.push({a, b});
}
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> arr[i][j];
if (arr[i][j] == 'k') {
x1 = i, y1 = j;
}
}
}
bfs();
int count = 0, ret = 1e9;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (arr[i][j] == 'e' && dist[i][j] != -1) {
count++;
ret = min(ret, dist[i][j]);
}
}
}
if (count == 0) cout << -1 << endl;
else cout << count << " " << ret << endl;
return 0;
}
三、字符串的编辑距离(动态规划)
题目描述
给定两个字符串 word1和 word2,计算将 word1转换成 word2所使用的最少操作数。操作包括:插入一个字符、删除一个字符、替换一个字符。数据范围:1 ≤ word1.length, word2.length ≤ 500。
解题思路
这道题是经典的动态规划 问题。我们可以定义一个二维数组 dp[i][j]表示将 word1的前 i个字符转换成 word2的前 j个字符所需的最少操作数。状态转移方程如下:
-
如果
word1[i-1] == word2[j-1],则dp[i][j] = dp[i-1][j-1](不需要操作)。 -
否则,
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中:-
dp[i-1][j]表示删除word1的第i个字符。 -
dp[i][j-1]表示在word1中插入word2的第j个字符。 -
dp[i-1][j-1]表示替换word1的第i个字符为word2的第j个字符。
-
代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 初始化边界条件
for (int i = 0; i <= n; i++) dp[i][0] = i; // word1前i个字符转成空字符串,需要删除i次
for (int j = 0; j <= m; j++) dp[0][j] = j; // 空字符串转成word2前j个字符,需要插入j次
// 状态转移
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1[i-1] == word2[j-1]) {
dp[i][j] = dp[i-1][j-1];
}se {
el dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
}
}
}
return dp[n][m];
}
int main() {
string word1, word2;
cin >> word1 >> word2;
cout << minDistance(word1, word2) << endl;
return 0;
}
四、哈夫曼编码(贪心 + 优先队列)
题目描述
给定 n个权值,构造哈夫曼树,并输出每个权值对应的哈夫曼编码。哈夫曼编码是一种变长编码,用于数据的压缩,出现频率高的字符使用较短的编码,频率低的字符使用较长的编码。
解题思路
哈夫曼编码的构造过程是典型的贪心算法应用,每次选择两个权值最小的节点合并,直到形成一棵哈夫曼树。具体步骤如下:
-
将所有权值放入优先队列(小根堆)中。
-
每次从队列中取出两个最小的权值,合并成一个新的节点,新节点的权值为两个子节点权值之和,并将新节点重新放入队列中。
-
重复步骤 2,直到队列中只剩下一个节点(哈夫曼树的根节点)。
-
在合并的过程中,记录每个权值的父节点和左右子节点的关系,然后递归生成每个权值的哈夫曼编码(左子树编码为 0,右子树编码为 1)。
代码实现
#include <iostream>
#include <queue>
#include <vector>
#include <map>
using namespace std;
typedef long long LL;
typedef pair<LL, int> PII; // 权值,节点编号
struct Node {
LL w;
int l, r;
Node(LL w = 0, int l = 0, int r = 0) : w(w), l(l), r(r) {}
bool operator < (const Node& other) const {
return w > other.w; // 小根堆,所以重载<为w大的排后面
}
};
vector<Node> huff;
map<LL, string> code;
// 生成哈夫曼编码
void dfs(int u, string s) {
if (huff[u].l == 0 && huff[u].r == 0) { // 叶子节点
code[huff[u].w] = s;
return;
}
if (huff[ul)]. dfs(huff[u].l, s + "0");
if (huff[u].r) dfs(huff[u].r, s + "1");
}
int main() {
int n;
cin >> n;
priority_queue<PII> pq; // 小根堆,存储权值和节点编号(这里节点编号可以暂时用权值代替,或者后续处理)
for (int i = 0; i < n; i++) {
LL w;
cin >> w;
pq.push({w, i}); // 节点编号暂时用i,后续可能需要调整
huff.emplace_back(w, 0, 0); // 初始化叶子节点
}
while (pq.size() > 1) {
auto [w1, u] = pq.top(); pq.pop();
auto [w2, v] = pq.top(); pq.pop();
LL w = w1 + w2;
int node = huff.size();
huff.emplace_back(w, u, v); // 新节点,左孩子是u,右孩子是v
pq.push({w, node});
}
if (!huff.empty()) {
dfs(pq.top().second, ""); // 从根节点开始遍历
for (int i = 0; i < n; i++) {
cout << huff[i].w << " " << code[huff[i].w] << endl;
}
}
return 0;
}
总结
这几道题分别涵盖了滑动窗口、广度优先搜索、动态规划和贪心算法(哈夫曼编码)等不同的算法思想。通过解决这些题目,我不仅巩固了算法的基础知识,还学会了如何根据不同的问题场景选择合适的算法。刷题的过程虽然有时会遇到困难和挫折,但每解决一道题带来的成就感都是巨大的,也让我更加热爱算法学习。希望这篇博客能对你有所启发,也欢迎大家在评论区交流讨论,一起进步!
以上是我对这几道算法题的总结和分享,如果你有任何问题或建议,欢迎留言交流!