动态规划02 自由之路[C++]

  动态规划02 自由之路[C++]_第1张图片

图源:文心一言

leedcode每日一题,提供了常规解法及其详细解释,供小伙伴们参考~

  • 第1版:在力扣新手村刷题的记录~
    • 方法一:递归调用,可以运行,但是不能通过较长的测试用例<失败>~
    • 方法二:动态规划,普遍适用的方法~

编辑:梅头脑

审核:文心一言

题目:514. 自由之路 - 力扣(LeetCode)


目录

514. 自由之路

题目

方法一:哈希表 + 递归调用<超时的失败方案>

方法二:动态规划

结语


514. 自由之路

题目

电子游戏“辐射4”中,任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘,并使用表盘拼写特定关键词才能开门。

给定一个字符串 ring ,表示刻在外环上的编码;给定另一个字符串 key ,表示需要拼写的关键词。您需要算出能够拼写关键词中所有字符的最少步数。

最初,ring 的第一个字符与 12:00 方向对齐。您需要顺时针或逆时针旋转 ring 以使 key 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 key 中的所有字符。

旋转 ring 拼出 key 字符 key[i] 的阶段中:

  1. 您可以将 ring 顺时针或逆时针旋转 一个位置 ,计为1步。旋转的最终目的是将字符串 ring 的一个字符与 12:00 方向对齐,并且这个字符必须等于字符 key[i] 。
  2. 如果字符 key[i] 已经对齐到12:00方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 key 的下一个字符(下一阶段), 直至完成所有拼写。

 示例1:

动态规划02 自由之路[C++]_第2张图片

输入: ring = "godding", key = "gd"
输出: 4
解释:
 对于 key 的第一个字符 'g',已经在正确的位置, 我们只需要1步来拼写这个字符。 
 对于 key 的第二个字符 'd',我们需要逆时针旋转 ring "godding" 2步使它变成 "ddinggo"。
 当然, 我们还需要1步进行拼写。
 因此最终的输出是 4。

示例 2:

输入: ring = "godding", key = "godding"
输出: 13

方法一:哈希表 + 递归调用<超时的失败方案>

算法思路

  • 算法思想:
    • 使用哈希表来记录ring中每个字符出现的位置;

    • 对于key中的每个字符,查找它在ring中的所有出现位置,并计算从当前位置顺时针或逆时针旋转到这些位置的最小步数;

    • 累加每个字符的旋转步数和按下按钮的步数(每次按下按钮计为1步);在计算完一个字符后,更新当前位置为下一个字符的起始位置;

    • 最后,返回累加的总步数。

  • 时间复杂度:O(n^m),这是因为对于key中的每个字符,我们可能需要检查ring中的n个位置,而对于key中的下一个字符,我们再次可能需要检查ring中的n个位置,依此类推;
  • 空间复杂度:O(n+m),其中nring的长度,mkey的长度。这是因为除了递归栈之外,我们还需要额外的空间来存储charMap和DFS函数中的局部变量。

⌨️算法代码

#include   
#include   
#include   
#include   
using namespace std;

class Solution {
public:
    int findRotateSteps(string ring, string key) {
        // 构建字符到索引位置的映射  
        unordered_map> charMap;
        for (int i = 0; i < ring.size(); ++i) {
            charMap[ring[i]].push_back(i);
        }

        // 定义一个递归函数来计算步数

        // 参数:当前ring的索引位置,当前需要拼写的key的索引位置,以及已经走过的步数  
        return dfs(charMap, ring.size(), 0, 0, key);
    }

private:
    int dfs(unordered_map>& charMap, int ringSize, int ringPos, int keyPos, const string& key) {
        // 递归终止条件:已经拼写完key中的所有字符  
        if (keyPos == key.size()) {
            return 0;
        }

        int minSteps = INT_MAX;
        char currentChar = key[keyPos];

        // 查找当前字符在ring中的所有位置  
        if (charMap.count(currentChar) > 0) {
            for (int nextRingPos : charMap[currentChar]) {
                // 计算从当前位置到下一个位置的顺时针或逆时针最小步数  
                int stepsToRotate = min(abs(nextRingPos - ringPos), ringSize - abs(nextRingPos - ringPos));
                // 递归计算拼写下一个字符所需的总步数,并更新最小步数  
                int totalSteps = stepsToRotate + 1 + dfs(charMap, ringSize, nextRingPos, keyPos + 1, key);
                minSteps = min(minSteps, totalSteps);
            }
        }

        return minSteps;
    }
};

/*  
int main() {  
    Solution solution;  
    string ring = "godding";  
    string key = "godding";  
    cout << "Minimum steps required: " << solution.findRotateSteps(ring, key) << endl;  
    return 0;  
}
*/

作者:文心一言 + 梅头脑

备注:这个思路可以用于较为简单的字符串,官方提供的2个测试用例本地可以通过,但是提交时的隐藏测试用例会因超时被打下来~~

代码解释

1:构建哈希表记录字符出现的位置

unordered_map> charMap;

for (int i = 0; i < ring.size(); ++i) { charMap[ring[i]].push_back(i); }

  • 使用 ring[i] 作为键来访问 charMap 中的元素。如果 charMap 中还没有这个键,那么会创建一个新的键值对,其中键是 ring[i],值是一个空的 vector

  • 调用 push_back(i) 方法将当前索引 i 添加到与 ring[i] 关联的 vector 中。

举个栗子,如果 ring 是字符串 "godding",那么在执行完这段循环代码后,charMap:

动态规划02 自由之路[C++]_第3张图片

2:计算从当前位置到下一个位置的顺时针或逆时针最小步数  

int stepsToRotate = min(abs(nextRingPos - ringPos), ringSize - abs(nextRingPos - ringPos));

  • ringpos为指针当前位置,nextringpos为哈希表记录的位置;
  • min计算最小值,abs为绝对值,即取abs(nextRingPos - ringPos)或ringSize - abs(nextRingPos - ringPos)的最小值,表示计算从当前位置到下一个位置顺时针或逆时针最小步数;

3:累加每个字符的旋转步数和按下按钮的步数,在计算完一个字符后,更新当前位置为下一个字符的起始位置;

int totalSteps = stepsToRotate + 1 + dfs(charMap, ringSize, nextRingPos, keyPos + 1, key);

  • 这一圈的旋转次数stepsToRotate,这一圈的按动次数1;
  • 下一圈的旋转与按动次数,通过递归调用dfs实现,传入的参数:charMap(步骤1的哈希表),ringsize(字符串长度), nextRingPos(更新当前ring字符串的索引位置),keyPos + 1(key的下一个字符),key(题目中给到的key字符串);

动态规划02 自由之路[C++]_第4张图片

4:返回累加的总步数:

return minSteps;  

方法二:动态规划

算法思路

  • 算法思想:
    • 记录ring中每个字符出现的位置;

    • 动态规划:定义一个状态数组来存储子问题的解,并通过状态转移方程来计算当前问题的解。在这个问题中,我们可以定义一个二维DP数组,其中 dp[i][j] 表示拼写完 key 的前 i 个字符,并且最后一个字符是通过旋转 ring 使得 ring[j] 对齐到 key[i-1] 的最小步数。

    • 最后,返回累加的总步数。

  • 时间复杂度:O(mn^2),动态规划部分有三层循环:
    • 外层循环遍历键(key)的每个字符,时间复杂度为 O(m),其中 m 是键的长度。
    • 中间循环遍历与当前键字符匹配的环形字符串中的字符索引,这在最坏情况下是 O(n)(假设每个字符在环形字符串中都出现)。
    • 内层循环遍历环形字符串的所有可能起始位置,时间复杂度为 O(n)。
    • 因此,动态规划部分的总时间复杂度为 O(m * n * n)。

  • 空间复杂度:O(mn),其中 m 和 n 分别是key和ring字符串的长度。

⌨️算法代码

#include   
#include   
#include   
#include   
#include   
#include 

class Solution {
public:
    int findRotateSteps(std::string ring, std::string key) {
        int n = ring.size();
        int m = key.size();

        // 构建字符到索引位置的映射  
        std::unordered_map> charMap;
        for (int i = 0; i < n; ++i) {
            charMap[ring[i]].push_back(i);
        }

        // 初始化DP数组  
        std::vector> dp(m + 1, std::vector(n, INT_MAX));

        // 对于ring中的每个字符,如果它是key的第一个字符,则初始化dp[1][j]  
        for (int j = 0; j < n; ++j) {
            if (ring[j] == key[0]) {
                dp[1][j] = std::min(j, n - j); // 使用std::min  
            }
        }

        // 动态规划计算最小步数  
        for (int i = 2; i <= m; ++i) {
            for (int j : charMap[key[i - 1]]) {
                for (int k = 0; k < n; ++k) {
                    // 如果ring[k]是key的前一个字符  
                    if (dp[i - 1][k] != INT_MAX) {
                        // 计算从位置k旋转到位置j的最小步数  
                        int steps = std::min(abs(j - k), n - abs(j - k)); // 使用std::min  
                        dp[i][j] = std::min(dp[i][j], dp[i - 1][k] + steps); // 使用std::min  
                    }
                }
            }
        }

        // 找到拼写完整个key的最小步数  
        int minSteps = *std::min_element(dp[m].begin(), dp[m].end()); // 使用std::min_element  
        return minSteps + m; // 加上m,因为每次旋转后都需要按下一个字符  
    }
};

/*
int main() {
    Solution solution;
    std::string ring = "godding";
    std::string key = "gd";
    std::cout << "Minimum steps required: " << solution.findRotateSteps(ring, key) << std::endl;

    // 更长的测试用例  
    ring = "caotmcaataijjxi";
    key = "oatjiioicitatajtijciocjcaaxaaatmctxamacaamjjx";
    std::cout << "Minimum steps required for longer test case: " << solution.findRotateSteps(ring, key) << std::endl;

    return 0;
}
*/

作者:文心一言 

代码解释

备注:我之前没有接触过动态规划的题目,所以对于这个问题的理解若隐若现。先把文心一言的回答与运行输出贴在下面,另外补充知乎博主的入门讲解——

捡田螺的小男孩捡田螺的小男孩博文:看一遍就理解:动态规划详解 - 知乎 (zhihu.com)

1:构建哈希表记录字符出现的位置(同方法一)

unordered_map> charMap;

for (int i = 0; i < ring.size(); ++i) { charMap[ring[i]].push_back(i); }
  • 使用 ring[i] 作为键来访问 charMap 中的元素。如果 charMap 中还没有这个键,那么会创建一个新的键值对,其中键是 ring[i],值是一个空的 vector

  • 调用 push_back(i) 方法将当前索引 i 添加到与 ring[i] 关联的 vector 中。

举个栗子,如果 ring 是字符串 "godding",那么在执行完这段循环代码后,charMap:

动态规划02 自由之路[C++]_第5张图片

2:初始化动态数组

std::vector> dp(m + 1, std::vector(n, INT_MAX));​​​​​​​ 
  • 这行代码声明了一个二维的 std::vector,名为 dp。具体来说,它是一个向量(vector),其元素也是向量(vector),内部的这些向量又包含了整数(int)。
  • min计算最小值,abs为绝对值,即取abs(nextRingPos - ringPos)或ringSize - abs(nextRingPos - ringPos)的最小值,表示计算从当前位置到下一个位置顺时针或逆时针最小步数;
  • dp(m + 1, std::vector(n, INT_MAX)) 是 dp 的构造函数调用,它做了两件事:

    • 设置 dp 的大小为 m + 1。也就是说,dp 会有 m + 1 个元素,每个元素都是一个 std::vector

    • 初始化这 m + 1 个元素。每个元素(也就是内部的 std::vector)都被初始化为具有 n 个元素,且每个元素的值为 INT_MAXINT_MAX 是在  头文件中定义的,表示 int 类型可以存储的最大值。

  • 这种初始化方式常用于动态规划(Dynamic Programming,DP)问题中,因为 DP 通常需要一个二维数组(或在这里是二维向量)来存储子问题的解。将 dp 的所有元素初始化为 INT_MAX 是为了在后续的计算中,可以通过比较和更新这些值来找到最小的解。

for (int j = 0; j < n; ++j) { if (ring[j] == key[0]) { dp[1][j] = std::min(j, n - j); } }
  • ​​​​​​​对于ring(环形字符串)中的每个字符,检查它是否与目标键key的第一个字符匹配。如果匹配,我们需要计算将这个字符旋转到索引0位置(即环形字符串的起始位置)所需的最小步骤数。
  • 旋转到索引0位置可以通过两种方式实现:

    • 顺时针旋转j步,如果j是当前字符的索引。
    • 逆时针旋转n - j步,其中nring的长度。这是因为环形字符串是闭合的,所以从任何位置逆时针旋转n - j步也会将字符带到索引0位置。
  • 在这两种旋转方式中,我们选择步数较少的那一种,因此使用了std::min(j, n - j)来计算所需的最小步骤数,并将这个值存储在dp[1][j]中。这里,dp[1][j]表示在匹配key的第一个字符时,将ring[j]旋转到起始位置所需的最小步骤数。
  • 这样做是为了建立一个基础状态,之后的动态规划过程将基于这些初始状态进行计算。

3:动态规划计算最小步数

        for (int i = 2; i <= m; ++i) {
            for (int j : charMap[key[i - 1]]) {
                for (int k = 0; k < n; ++k) {
                    // 如果ring[k]是key的前一个字符
                    if (dp[i - 1][k] != INT_MAX) {
                        // 计算从位置k旋转到位置j的最小步数
                        int steps = std::min(abs(j - k), n - abs(j - k)); // 使用std::min
                        dp[i][j] = std::min(dp[i][j], dp[i - 1][k] + steps); // 使用std::min
                    }
                }
            }
        }
  1. 外层循环 for (int i = 2; i <= m; ++i) 遍历键(key)的每个字符,从第二个字符开始(因为第一个字符的初始化已经在之前的步骤中完成了)。

  2. 中间循环 for (int j : charMap[key[i - 1]]) 遍历与当前键字符匹配的环形字符串中的所有字符索引。这里使用了 charMap,它是一个哈希表,将环形字符串中的字符映射到它们在字符串中出现的所有索引上。

  3. 内层循环 for (int k = 0; k < n; ++k) 遍历环形字符串的所有可能起始位置。对于每个位置 k,我们检查它是否能够通过旋转环形字符串与上一个键字符匹配。

  4. 在内层循环中,if (dp[i - 1][k] != INT_MAX) 检查上一个键字符匹配时的最小步数是否已经被计算过(即不是初始化的最大值)。如果是这样,我们就继续计算从位置 k 旋转到当前键字符位置 j 所需的最小步数。

  5. 计算旋转步数的逻辑是 int steps = std::min(abs(j - k), n - abs(j - k));,它比较了顺时针和逆时针旋转到目标位置所需的步数,并选择了较小的一个。

  6. 最后,dp[i][j] = std::min(dp[i][j], dp[i - 1][k] + steps); 更新了匹配当前键字符时,将环形字符串旋转到位置 j 所需的最小步数。这里采用了状态转移方程,将当前状态的最小步数与之前状态的最小步数加上旋转步数进行比较,保留了较小的一个。

计算过程

以key:godding,ring:godding为例:

初始化后的矩阵:g可以通过顺时针旋转0次或者逆时针旋转1次得到,因此以0作为记录,计算下一列;

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN NAN
o NAN NAN NAN NAN NAN NAN NAN
d NAN NAN NAN NAN NAN NAN NAN
d NAN NAN NAN NAN NAN NAN NAN
i NAN NAN NAN NAN NAN NAN NAN
n NAN NAN NAN NAN NAN NAN NAN
g 1 NAN NAN NAN NAN NAN NAN

DP array after processing key character 'o' (1/6): o可以通过旋转1次得到;

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN NAN
o NAN 1 NAN NAN NAN NAN NAN
d NAN NAN NAN NAN NAN NAN NAN
d NAN NAN NAN NAN NAN NAN NAN
i NAN NAN NAN NAN NAN NAN NAN
n NAN NAN NAN NAN NAN NAN NAN
g 1 NAN NAN NAN NAN NAN NAN

---------------------
DP array after processing key character 'd' (2/6): 
d可以通过顺时针旋转1或2次;

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN NAN
o NAN 1 NAN NAN NAN NAN NAN
d NAN NAN 2 NAN NAN NAN NAN
d NAN NAN 3 NAN NAN NAN NAN
i NAN NAN NAN NAN NAN NAN NAN
n NAN NAN NAN NAN NAN NAN NAN
g 1 NAN NAN NAN NAN NAN NAN

---------------------
DP array after processing key character 'd' (3/6):

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN NAN
o NAN 1 NAN NAN NAN NAN NAN
d NAN NAN 2 2 NAN NAN NAN
d NAN NAN 3 3 NAN NAN NAN
i NAN NAN NAN NAN NAN NAN NAN
n NAN NAN NAN NAN NAN NAN NAN
g 1 NAN NAN NAN NAN NAN NAN

---------------------
DP array after processing key character 'i' (4/6):

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN NAN
o NAN 1 NAN NAN NAN NAN NAN
d NAN NAN 2 2 NAN NAN NAN
d NAN NAN 3 3 NAN NAN NAN
i NAN NAN NAN NAN 4 NAN NAN
n NAN NAN NAN NAN NAN NAN NAN
g 1 NAN NAN NAN NAN NAN NAN

---------------------
DP array after processing key character 'n' (5/6):

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN NAN
o NAN 1 NAN NAN NAN NAN NAN
d NAN NAN 2 2 NAN NAN NAN
d NAN NAN 3 3 NAN NAN NAN
i NAN NAN NAN NAN 4 NAN NAN
n NAN NAN NAN NAN NAN 5 NAN
g 1 NAN NAN NAN NAN NAN NAN

---------------------
DP array after processing key character 'g' (6/6):

ring \ key g o d d i n g
g 0 NAN NAN NAN NAN NAN 7
o NAN 1 NAN NAN NAN NAN NAN
d NAN NAN 2 2 NAN NAN NAN
d NAN NAN 3 3 NAN NAN NAN
i NAN NAN NAN NAN 4 NAN NAN
n NAN NAN NAN NAN NAN 5 NAN
g 1 NAN NAN NAN NAN NAN 6

---------------------
Minimum steps required: 13

根据动态规划矩阵,最后1列的最小值为6,即匹配7轮后,最少需要旋转6次能够完成字符匹配。另外,加上需要按动7次按钮,所以一共需要的最小步数为6+7=13。

以key:gd,ring:godding为例,最后的旋转矩阵:

ring \ key g d
g 0 NAN
o NAN NAN
d NAN 2
d NAN 3
i NAN NAN
n NAN NAN
g 1 NAN

Minimum steps required: 4

根据动态规划矩阵,最后1列的最小值为2,即匹配2轮后,最少需要旋转2次能够完成字符匹配。另外,加上需要按动2次按钮,所以一共需要的最小步数为2+2=4。


结语

博文到此结束,写得模糊或者有误之处,欢迎小伙伴留言讨论与批评,督促博主优化内容{例如有错误、难理解、不简洁、缺功能}等,博主会顶锅前来修改~~‍️‍️

我是梅头脑,本片博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下,感谢点赞小伙伴对于博主的支持~~

同系列的博文1:动态规划_梅头脑_的博客-CSDN博客

同系列的博文2:数据结构_梅头脑_的博客-CSDN博客

同博主的博文:随笔03 笔记整理-CSDN博客

你可能感兴趣的:(#,动态规划,动态规划,算法,c++,笔记)