day56补

博客围绕力扣583题,即求两个字符串变为相同所需的最小删除步数展开。介绍了两种动态规划解法,一是分析动规五部曲,确定dp数组含义、递推公式、初始化及遍历顺序;二是求出两字符串最长公共子序列长度,用总长度减去其长度得到最少删除步数。

583. 两个字符串的删除操作

力扣题目链接(opens new window)

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: "sea", "eat"
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

#算法公开课

《代码随想录》算法视频公开课 (opens new window)LeetCode:583.两个字符串的删除操 (opens new window),相信结合视频再看本篇题解,更有助于大家对本题的理解

#思路

#动态规划一

本题和动态规划:115.不同的子序列 (opens new window)相比,其实就是两个字符串都可以删除了,情况虽说复杂一些,但整体思路是不变的。

这次是两个字符串可以相互删了,这种题目也知道用动态规划的思路来解,动规五部曲,分析如下:

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

这里dp数组的定义有点点绕,大家要撸清思路。

  1. 确定递推公式
  • 当word1[i - 1] 与 word2[j - 1]相同的时候
  • 当word1[i - 1] 与 word2[j - 1]不相同的时候

当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];

当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:

情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1

情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1

情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2

那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});

因为 dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);

这里可能不少录友有点迷糊,从字面上理解 就是 当 同时删word1[i - 1]和word2[j - 1],dp[i][j-1] 本来就不考虑 word2[j - 1]了,那么我在删 word1[i - 1],是不是就达到两个元素都删除的效果,即 dp[i][j-1] + 1。

  1. dp数组如何初始化

从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是一定要初始化的。

dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。

dp[0][j]的话同理,所以代码如下:

vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;

1
2
3

  1. 确定遍历顺序

从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和dp[i][j] = dp[i - 1][j - 1]可以看出dp[i][j]都是根据左上方、正上方、正左方推出来的。

所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。

  1. 举例推导dp数组

以word1:"sea",word2:"eat"为例,推导dp数组状态图如下:

583.两个字符串的删除操作1

以上分析完毕,代码如下:

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
        for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
        for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
        for (int i = 1; i <= word1.size(); i++) {
            for (int j = 1; j <= word2.size(); j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
                }
            }
        }
        return dp[word1.size()][word2.size()];
    }
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

  • 时间复杂度: O(n * m)
  • 空间复杂度: O(n * m)

#动态规划二

本题和动态规划:1143.最长公共子序列 (opens new window)基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。

代码如下:

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
        for (int i=1; i<=word1.size(); i++){
            for (int j=1; j<=word2.size(); j++){
                if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
                else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
        return word1.size()+word2.size()-dp[word1.size()][word2.size()]*2;
    }
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14

  • 时间复杂度: O(n * m)
  • 空间复杂度: O(n * m)

#其他语言版本

#Java:

// dp数组中存储word1和word2最长相同子序列的长度
class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];

        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return len1 + len2 - dp[len1][len2] * 2;
    }
}

// dp数组中存储需要删除的字符个数
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i;
        for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j;
        
        for (int i = 1; i < word1.length() + 1; i++) {
            for (int j = 1; j < word2.length() + 1; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }else{
                    dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,
                                        Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
                }
            }
        }
        
        return dp[word1.length()][word2.length()];
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

//DP - longest common subsequence (用最長公共子序列反推)
class Solution {
    public int minDistance(String word1, String word2) {
        char[] char1 = word1.toCharArray();
        char[] char2 = word2.toCharArray();

        int len1 = char1.length;
        int len2 = char2.length;

        int dp[][] = new int [len1 + 1][len2 + 1];

        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; j++){
                if(char1[i - 1] == char2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }

        return len1 + len2 - (2 * dp[len1][len2]);//和leetcode 1143只差在這一行。
    }
}
<think> 我们有一个任务:读取一个8位数的整数作为日期,例如20240519,表示2024年5月19日。 我们需要使用scanf将其解析为年、月、日三个变量,然后按标准格式输出,比如:2024年05月19日。 思路: 1. 用户输入一个8位整数,我们可以将其视为一个整数,然后通过数学运算分离出年、月、日。 但是题目要求使用scanf来解析,所以我们可以考虑使用格式字符串来直接读取。 2. 由于输入是一个连续的8位数,我们可以将其作为字符串读入,然后分别取前4位作为年,接下来2位作为月,后2位作为日。 但是题目要求用scanf解析,并且要存储到三个整数变量中。我们可以用scanf的格式字符串来分割这个8位数。 3. 使用scanf的格式说明符:我们可以指定读取8位整数,然后通过除以10000得到前四位(年),取余10000得到后四位,再除以100得到月,取余100得到日。 但是这样需要数学运算,而题目要求用scanf解析为三个变量。 4. 另一种方法:将8位整数看作一个字符串,然后使用scanf按照指定宽度读取。例如: scanf("%4d%2d%2d", &year, &month, &day); 但是这里有一个问题:输入是一个整数,而不是字符串。如果我们这样读取,那么当用户输入20240519时,实际上scanf会将其作为一个整数读入,然后按照格式字符串分割。但是,格式字符串中的%4d会读取4位整数,接着%2d读取2位,再%2d读取2位。这是可行的,因为输入是连续的8位数字(没有空格)。 5. 注意:如果用户输入的数字不足8位,或者有非数字字符,则可能出错。但题目要求输入8位整数,我们假设用户输入正确。 6. 输出时,要求按标准格式:年(4位)月(2位)日(2位),并且月份和日如果是一位数前面0。因此输出格式为:printf("%04d年%02d月%02d日", year, month, day); 7. 但是,我们使用scanf("%4d%2d%2d", ...)时,输入是20240519,那么: year = 2024, month = 5, day = 19 输出时,月份是5,我们希望输出05,所以用%02d来格式化。 8. 但是,这样写格式字符串:%4d%2d%2d,实际上在输入时,用户输入的是连续的数字,没有分隔符,所以这样读取是可以的。 9. 然而,我们也可以直接用一个8位整数读入,然后分割,但是题目要求用scanf解析为三个变量,所以我们采用上面的方法。 10. 但是,有一个潜在问题:如果输入的数字超过8位怎么办?比如输入202405191,那么scanf会读取前8位,然后后面的1会留在缓冲区,可能导致后续输入问题。但题目要求输入8位整数,我们可以要求用户必须输入8位,或者我们读取8位后忽略后面的内容。不过,题目没有特别说明,我们就按照8位整数读取。 11. 我们也可以使用scanf读取一个8位数字的字符串,然后分别解析三个部分,但这样就需要三个整数变量,我们可以用atoi转换,但题目要求用scanf解析。 12. 实际上,我们可以这样: int year, month, day; scanf("%4d%2d%2d", &year, &month, &day); 13. 但是,这种写法可能会遇到问题:输入2024 05 19(有空格)就不行了,但题目要求输入一个8位整数,所以中间没有空格。 14. 因此,我们按照上述格式读取。 15. 代码实现: 注意:由于月份和日都是两位,所以用%2d可以正确读取(即使第一位是0,比如05,那么读取后month=5,因为%d会忽略前导零,所以5和05读取后都是5)。这样没有问题,因为我们输出的时候会零。 16. 但是,如果用户输入的数字不足8位,那么读取会出错。例如输入2024519(7位),那么scanf会尝试读取4位整数,2位整数,2位整数,但最后一部分不足两位,所以day可能只读到19(如果后面没有数字了,day可能只读到一位?)。所以我们要确保输入8位。 17. 我们可以增加输入的校验:如果读取的变量个数不是3,则提示错误。 18. 代码示例: 19. 另外,我们也可以使用字符串方式读取8个字符,然后转换成整数,但题目要求用scanf解析为三个整数变量,所以我们还是用上面的方法。 20. 但是,有一个问题:如果输入以0开头,比如00120519,那么%4d会读入12(因为%d会忽略前导0,所以0012会读成12),这样年份就变成了12,而不是0012。但是我们要求输出4位,所以输出0012年?但是题目要求输入8位整数,00120519也是8位,但是作为整数,前面的0会被忽略。所以我们需要用字符串来保留前导零? 21. 重新思考:题目要求输入8位整数,整数不能有前导零(除了0本身),但日期中可能出现0开头的月份和日,但年份通常不会有前导零。然而,如果用户输入00120519,这实际上是一个8位数,但作为整数输入,scanf会将其看作12年5月19日(因为0012就是12)。所以,我们不应该将输入视为整数,而应该视为字符串。 22. 修正方案:题目要求输入8位整数,但我们可以使用字符串来接收,然后分割字符串,再转换成整数。这样前导零就不会丢失。但是,题目要求用scanf解析为年、月、日三个变量,并且是整数变量。所以,如果我们用字符串方式,需要先读取字符串,然后分割,然后转换,但这样就不是直接用scanf解析到三个整数变量了。 23. 另一种方式:使用%4c%2c%2c读取四个字符,两个字符,两个字符,然后分别转换为整数。这样我们可以保留前导零,但输出时我们要求是整数,所以转换后还是整数,输出时再零。 24. 但是,题目要求的是三个整数变量,所以我们读取字符后需要转换为整数,这样就不直接。而且,题目要求用scanf解析,所以我们还是希望直接读取整数。 25. 考虑到实际使用中,年份通常不会有前导零(除非是公元前的年份,但这里我们只考虑正整数年份),而且8位整数日期,年份是4位,一般不会出现前导零(比如0001年,但这种情况很少)。所以,我们按照整数读取,忽略前导零。输出年份时,我们使用%04d来保证输出4位,不足4位前面零。 26. 但是,如果用户输入00010501,那么scanf("%4d%2d%2d", ...)会得到year=1, month=5, day=1。然后输出:0001年05月01日?因为年份用%04d,所以会零成0001。这样符合要求。 27. 因此,我们采用: scanf("%4d%2d%2d", &year, &month, &day); 然后输出: printf("%04d年%02d月%02d日\n", year, month, day); 28. 但是,注意:输入00010501,用%d读取时,前4位0001会被读成1(因为%d是十进制整数,会忽略前导零)。所以这样读取后,year=1,然后我们输出%04d就会变成0001,所以最终输出是符合要求的。 29. 所以,我们这样写代码: 30. 代码实现: 31. 另外,需要检查输入是否成功,如果输入的不是数字,则scanf无法正确读取。 32. 完整代码: ```c #include <stdio.h> int main() { int year, month, day; printf("请输入一个8位数的日期(格式:YYYYMMDD):"); int count = scanf("%4d%2d%2d", &year, &month, &day); if (count != 3) { printf("输入格式错误!请确保输入8位数字。\n"); return 1; } printf("解析后的日期:%04d年%02d月%02d日\n", year, month, day); return 0; } ``` 33. 测试: 输入:20240519 输出:2024年05月19日 输入:00010501 输出:0001年05月01日 输入:2024519 (7位数字) scanf只能读取前4位2024,然后5和19(但19只读2位,但输入只有三位519,所以%2d只能读到51?不对) 实际上,输入2024519,第一个%4d读取2024,然后%2d读取51,%2d读取9?因为后面只剩下519,%2d会读取51(两位数),然后最后一个%2d只能读取9(一位数,但要求两位,所以只匹配到9,然后等待输入?)所以这时scanf会返回2(只成功读取了两个变量)。 因此,我们检查返回值count,如果不是3,则报错。 34. 所以,上面的代码可以处理错误输入。 35. 但是,如果用户输入123456789(9位),那么scanf会读取前8位:1234,56,78,然后最后一个9留在输入缓冲区。所以会正确解析为1234年56月78日,这显然是不合法的日期,但是程序不会报错。所以我们需要对日期进行合法性检查(比如月份在1-12,日在1-31,并且根据月份和年份判断日是否合法)。但题目只要求解析和输出,所以我们可以先不做校验。 36. 根据题目要求,我们只需要解析并按格式输出。如果输入8位,则正常输出;如果输入不是8位数字(比如不足8位),则报错。但是,我们使用%4d%2d%2d可以读取8位数字,如果输入超过8位,则多出的部分留在缓冲区。所以,我们要求用户输入8位整数,我们读取8位,多出的部分不管。 37. 但是,如果用户输入非数字,比如2024a519,那么第一个%4d读取2024,然后%2d会尝试读取两个数字,但遇到字母'a',所以会停止,只读取到一个数字(可能失败),然后%2d也会失败。所以scanf返回1(只成功读取了year)。然后我们报错。 38. 所以,代码如上所示。 39. 总结:这个程序可以满足题目的基本要求。 40. 但是,如果用户输入负数,比如-20240519,那么第一个%4d会读取-202,因为-202是4位(负号也算一位?),然后后面的4位05和19会被分别读取。这样得到year=-202, month=5, day=19,显然不对。所以我们需要确保输入是正整数。 41. 如何避免负数?我们可以读取整个字符串,然后检查第一个字符是不是负号,或者我们可以用无符号整数?但是题目要求输入整数,scanf的%d可以读取负数。 42. 我们可以修改格式字符串,只读取8位数字,不管符号。但scanf的%d会读取符号。所以我们需要用字符串读取,然后检查。 43. 但题目要求用scanf解析为三个整数变量,所以我们不能改变读取方式。因此,我们可以在读取后检查年份是否为负数,或者要求输入为正数。或者,我们可以要求用户输入8位数字(不带符号)。 44. 我们可以在程序中检查年份、月份、日是否为正数,并且年份大于0,月份在1-12,日在1-31。但题目没有要求,所以我们可以暂时不做。如果用户输入负数,则输出会异常(比如负数用%04d输出,会有负号和数字,例如-202)。 45. 因此,我们增加一个简单的检查:年份大于0,月份在1-12,日在1-31。但题目没有要求,这里我们只按照题目要求实现,所以先不做。 46. 最终,我们按照上面的代码实现。 47. 如果用户输入带前导空格,scanf会跳过前导空格。所以输入" 20240519"也是可以的,因为%d会跳过空格。 48. 所以,程序可以写成这样。 49. 注意:如果用户输入不足4位,那么第一个%4d会失败(因为要求4位数字,如果不足4位,比如输入123,那么第一个%4d会等待更多输入?实际上,如果输入123后按回车,那么第一个%4d只读到了123,不足4位,所以这个读取失败,后面的也不会再读。所以scanf返回0)。 50. 因此,我们的错误处理是合理的。 51. 我们输出代码。 52. 但是,题目要求输出标准格式,我们使用printf的格式化输出。 53. 所以,最终代码如上。 54. 运行示例: 输入:20240519 输出:2024年05月19日 输入:20241332 (月份13不合法,但我们不检查) 输出:2024年13月32日 55. 如果需要检查日期合法性,可以在解析后添加校验,但题目无要求。 56. 因此,我们提供上述代码。 </think>以下是一个C语言程序,实现从用户输入的8位整数日期中解析出年、月、日,并按标准格式输出: ```c #include <stdio.h> int main() { int year, month, day; // 提示用户输入 printf("请输入8位整数日期(格式:YYYYMMDD):"); // 使用scanf解析输入 if (scanf("%4d%2d%2d", &year, &month, &day) != 3) { printf("输入格式错误!请确保输入8位数字日期\n"); return 1; } // 按标准格式输出 printf("解析结果:%04d年%02d月%02d日\n", year, month, day); return 0; } ``` ### 程序说明: 1. **输入处理**: - 使用`scanf("%4d%2d%2d", ...)`直接按4位年、2位月、2位日解析输入 - `%4d`截取前4位作为年份 - `%2d`截取中间2位作为月份 - `%2d`截取最后2位作为日期 - 通过返回值验证是否成功解析3个值[^1][^2] 2. **输出格式化**: - `%04d`:年份保持4位,不足时零 - `%02d`:月/日保持2位,不足时零 - 输出格式示例:`2024年05月19日` 3. **错误处理**: - 若输入非8位数字(如包含字母/符号)会提示错误 - 若输入不足8位会读取失败 ### 示例执行: ```plaintext 请输入8位整数日期(格式:YYYYMMDD):20240519 解析结果:2024年05月19日 ``` ```plaintext 请输入8位整数日期(格式:YYYYMMDD):20001122 解析结果:2000年11月22日 ``` ### 注意事项: 1. 输入必须是连续的8位数字(不需要分隔符) 2. 输入前导零会被保留(如`05082024` → 0508年02月02日) 3. 程序不验证日期有效性(如20241332会输出无效日期)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值