算法刷题笔记:从滑动窗口到哈夫曼编码,我的算法进阶之路

在算法的学习过程中,刷题是提升编程能力和逻辑思维的重要途径。最近我刷了几道不同类型的算法题,包括字符串处理、广度优先搜索(BFS)、动态规划以及哈夫曼编码等,这些题目涵盖了不同的算法思想,让我对算法的应用有了更深入的理解。现在我将这几道题的解题思路和实现过程整理成这篇博客,希望能对同样在算法学习路上的小伙伴们有所帮助。

一、包含不超过两种字符的最长子串(滑动窗口)

题目描述

给定一个长度为 n的字符串,找出最多包含两种字符的最长子串 t,返回这个最长的长度。字符串仅包含小写英文字母,数据范围:1 ≤ n ≤ 10^5

解题思路

这道题可以使用滑动窗口(双指针) 的方法来解决。我们维护一个窗口 [left, right],窗口内的字符种类数不超过 2 种。具体步骤如下:

  1. 使用一个哈希表(这里用数组模拟,因为字符是小写字母,所以数组大小为 26)来统计窗口内每种字符的出现次数。

  2. 使用变量 count记录窗口内不同字符的种类数。

  3. 右指针 right不断向右移动,将当前字符加入窗口:

    • 如果该字符在窗口内的出现次数从 0 变为 1,说明窗口内多了一个新字符,count加 1。
  4. count > 2时,需要移动左指针 left缩小窗口,直到 count ≤ 2

    • 如果左指针指向的字符在窗口内的出现次数从 1 变为 0,说明窗口内少了一个字符,count减 1。
  5. 每次调整窗口后,更新最长子串的长度。

代码实现

复制代码
#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 适合寻找最短路径,因为它按层遍历,第一次到达目标节点时的步数就是最短距离。具体步骤如下:

  1. 读取输入,找到 koton 的起始位置 (x1, y1)和所有出口的位置。

  2. 初始化一个距离数组 dist,用于记录每个位置到起始点的最短距离,初始值为 -1(表示未访问)。

  3. 使用队列进行 BFS,从起始点开始,依次访问其上下左右四个方向的相邻位置:

    • 如果相邻位置在迷宫范围内、不是墙壁、且未被访问过,则更新其距离,并将其加入队列。

    • 注意:如果是出口,也需要加入队列继续传播距离,因为可能其他出口的距离更近。

  4. 遍历结束后,统计所有出口中可到达的数量和最小距离。

代码实现

复制代码
#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;
}

三、字符串的编辑距离(动态规划)

题目描述

给定两个字符串 word1word2,计算将 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个权值,构造哈夫曼树,并输出每个权值对应的哈夫曼编码。哈夫曼编码是一种变长编码,用于数据的压缩,出现频率高的字符使用较短的编码,频率低的字符使用较长的编码。

解题思路

哈夫曼编码的构造过程是典型的贪心算法应用,每次选择两个权值最小的节点合并,直到形成一棵哈夫曼树。具体步骤如下:

  1. 将所有权值放入优先队列(小根堆)中。

  2. 每次从队列中取出两个最小的权值,合并成一个新的节点,新节点的权值为两个子节点权值之和,并将新节点重新放入队列中。

  3. 重复步骤 2,直到队列中只剩下一个节点(哈夫曼树的根节点)。

  4. 在合并的过程中,记录每个权值的父节点和左右子节点的关系,然后递归生成每个权值的哈夫曼编码(左子树编码为 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;
}

总结

这几道题分别涵盖了滑动窗口、广度优先搜索、动态规划和贪心算法(哈夫曼编码)等不同的算法思想。通过解决这些题目,我不仅巩固了算法的基础知识,还学会了如何根据不同的问题场景选择合适的算法。刷题的过程虽然有时会遇到困难和挫折,但每解决一道题带来的成就感都是巨大的,也让我更加热爱算法学习。希望这篇博客能对你有所启发,也欢迎大家在评论区交流讨论,一起进步!


以上是我对这几道算法题的总结和分享,如果你有任何问题或建议,欢迎留言交流!

相关推荐
七夜zippoe2 小时前
Java技术未来展望:GraalVM、Quarkus、Helidon等新趋势探讨
java·开发语言·python·quarkus·graaivm·helidon
枫叶落雨2222 小时前
ClassPathXmlApplicationContext
java·开发语言
MicroTech20252 小时前
突破虚时演化非酉限制:MLGO微算法科技发布可在现有量子计算机运行的变分量子模拟技术
科技·算法·量子计算
hssfscv2 小时前
软件设计师下午题六——Java的各种设计模式
java·算法·设计模式
珂朵莉MM2 小时前
第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第3赛季优化题--多策略混合算法
人工智能·算法
罗西的思考2 小时前
【OpenClaw】通过 Nanobot 源码学习架构---(6)Skills
人工智能·深度学习·算法
枫叶林FYL2 小时前
【自然语言处理 NLP】7.2 红队测试与对抗鲁棒性(Red Teaming & Adversarial Robustness)
人工智能·算法·机器学习
qiqsevenqiqiqiqi2 小时前
字符串模板
算法
十五年专注C++开发2 小时前
Oat++: 一个轻量级、高性能、零依赖的 C++ Web 框架
开发语言·c++·web服务·oatpp