LeetCode第190场周赛(Weekly Contest 190)解题报告

周日收拾行李,准备返校,就没时间参加了,听说是手速题,还是挺简单的,最后一题两种假设 DP 状态的方法,第一种方法笔记简单,容易想到。第二种方法,如果做过类似的题目,比如 LeetCode 上的第 72 题 “编辑距离”,那就很好做了。

第一题:字符串分割 + 模拟(手动分割 或者 C++ stringstream 分割)。

第二题:滑动窗口。

第三题:DFS。

第四题:动态规划 DP,有两种状态假设方法。

详细题解如下。


1.检查单词是否为句中其他单词的前缀

           AC代码(手动实现字符串分割  C++)

           AC代码(利用stringstream进行分割  C++)

2. 定长子串中元音的最大数目

           AC代码(C++)

3.二叉树中的伪回文路径

           AC代码(C++)

4.两个子序列的最大点积

           AC代码(状态一、O(n ^ 3)  C++)

           AC代码(状态二、O(n ^ 2)  C++)


LeetCode第190场周赛地址:

https://leetcode-cn.com/contest/weekly-contest-190/


1.检查单词是否为句中其他单词的前缀

题目链接

https://leetcode-cn.com/problems/check-if-a-word-occurs-as-a-prefix-of-any-word-in-a-sentence/

题意

给你一个字符串 sentence 作为句子并指定检索词为 searchWord ,其中句子由若干用 单个空格 分隔的单词组成。

请你检查检索词 searchWord 是否为句子 sentence 中任意单词的前缀。

  • 如果 searchWord 是某一个单词的前缀,则返回句子 sentence 中该单词所对应的下标(下标从 1 开始)。
  • 如果 searchWord 是多个单词的前缀,则返回匹配的第一个单词的下标(最小下标)。
  • 如果 searchWord 不是任何单词的前缀,则返回 -1 。

字符串 S 的 「前缀」是 S 的任何前导连续子字符串。

示例 1:

输入:sentence = "i love eating burger", searchWord = "burg"
输出:4
解释:"burg" 是 "burger" 的前缀,而 "burger" 是句子中第 4 个单词。

示例 2:

输入:sentence = "this problem is an easy problem", searchWord = "pro"
输出:2
解释:"pro" 是 "problem" 的前缀,而 "problem" 是句子中第 2 个也是第 6 个单词,但是应该返回最小下标 2 。

示例 3:

输入:sentence = "i am tired", searchWord = "you"
输出:-1
解释:"you" 不是句子中任何单词的前缀。

提示:

  • 1 <= sentence.length <= 100
  • 1 <= searchWord.length <= 10
  • sentence 由小写英文字母和空格组成。
  • searchWord 由小写英文字母组成。
  • 前缀就是紧密附着于词根的语素,中间不能插入其它成分,并且它的位置是固定的——-位于词根之前。(引用自 前缀_百度百科 )

解题思路

根据题意,其实主要是将 字符串 分割得到各个单词,然后枚举各个单词的前缀是不是 searchWord 即可。

大概的时间复杂度,分割字符串需要 O(n),判断是不是前缀,需要 O(m),所以总时间复杂度是 O(n * m),其中 n 是 sentence 的长度,m 是 searchWord 的长度。

那么分割 字符串时,由于 C++ 没有类似 Java 或 Python 中的 split 函数,所以相当于要自己实现。

一般实现,可以手动实现,遍历 字符串,当是 空格时,说明得到了一个 单词。

或者可以利用 stringstream,即将 sentence 作为 stringstream,然后输入(那么此时由于 输入是会按照 空格 进行分割),所以也就得到了各个单词(不断的读取)

判断是不是前缀,那就很简单, 也就是判断这个单词的前面 m 个字符,是不是和 searchWord  完全一样即可。

AC代码(手动实现字符串分割  C++)

class Solution {
public:
    int isPrefixOfWord(string sT, string sW) {
        sT += " ";  // 最后加上一个 空格,为了方便下面的处理
        string cur = "";
        int n = sW.size();
        int idx = 1;  // 记录是第几个 单词
        for(auto c : sT)
        {
            if(c == ' ')
            {
                int m = cur.size();
                if(n <= m) 
                {
                    bool flag = true;
                    for(int i = 0;i < n; ++i)
                    {
                        if(cur[i] != sW[i]) flag = false;
                    }
                    if(flag) return idx;
                }
                ++idx;
                cur = "";
            }
            else
            {
                cur += c;
            }
        }
        return -1;
    }
};

AC代码(利用stringstream进行分割  C++)

class Solution {
public:
    int isPrefixOfWord(string sentence, string searchWord) {
        stringstream ssin(sentence);  // 利用 stringstream
        string word;
        int m = searchWord.size();
        for(int i = 1; ssin >> word; ++i)
        {
            if(word.size() < m) continue;
            bool flag = true;
            for(int j = 0;j < m && flag; ++j)  // 判断是不是前缀
            {
                if(word[j] != searchWord[j]) flag = false;
            }
            if(flag) return i;
        }
        return -1;
    }
};

2. 定长子串中元音的最大数目

题目链接

https://leetcode-cn.com/problems/maximum-number-of-vowels-in-a-substring-of-given-length/

题意

给你字符串 s 和整数 k 。

请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。

英文中的 元音字母 为(a, e, i, o, u)。

示例 1:

输入:s = "abciiidef", k = 3
输出:3
解释:子字符串 "iii" 包含 3 个元音字母。

示例 2:

输入:s = "aeiou", k = 2
输出:2
解释:任意长度为 2 的子字符串都包含 2 个元音字母。

示例 3:

输入:s = "leetcode", k = 3
输出:2
解释:"lee"、"eet" 和 "ode" 都包含 2 个元音字母。

提示:

  • 1 <= s.length <= 10^5
  • s 由小写英文字母组成
  • 1 <= k <= s.length

解题思路

怎么说呢,一看到题目,就知道是一个 固定长度范围的,那就想到了用 滑动窗口,也就是我们一开始取了 k 长度,然后下一个  k 长度,其实就是,区间右边往后移动一个,区间左边往后移动一个(相当于原来区间,加上新的,去掉一开始的,得到新区间)

所以这样子的,利用滑动窗口的时间复杂度是 O(n)

AC代码(C++)

class Solution {
public:
    bool check(char c)  // 判断是不是元音字母
    {
        if(c == 'a') return true;
        else if(c == 'e') return true;
        else if(c == 'i') return true;
        else if(c == 'o') return true;
        else if(c == 'u') return true;
        return false;
    }

    int maxVowels(string s, int k) {
        int n = s.size();
        int ans = 0;
        int cur = 0;
        for(int i = 0;i < k; ++i)  // 一开始的区间
        {
            if(check(s[i])) ++cur;
        }
        ans = cur;
        for(int i = k; i < n; ++i)  // 然后不断加入新的点,去掉最前面的点,得到新区间
        {
            if(check(s[i])) ++cur;
            if(check(s[i - k])) --cur;
            ans = max(ans, cur);
        }
        return ans;
    }
};

3.二叉树中的伪回文路径

题目链接

https://leetcode-cn.com/problems/pseudo-palindromic-paths-in-a-binary-tree/

题意

给你一棵二叉树,每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的,当它满足:路径经过的所有节点值的排列中,存在一个回文序列。

请你返回从根到叶子节点的所有路径中 伪回文 路径的数目。

示例 1:

【示例有图,具体看链接】
输入:root = [2,3,1,3,1,null,1]
输出:2 
解释:上图为给定的二叉树。总共有 3 条从根到叶子的路径:红色路径 [2,3,3] ,绿色路径 [2,1,1] 和路径 [2,3,1] 。
     在这些路径中,只有红色和绿色的路径是伪回文路径,因为红色路径 [2,3,3] 存在回文排列 [3,2,3] ,绿色路径 [2,1,1] 存在回文排列 [1,2,1] 。

示例 2:

【示例有图,具体看链接】
输入:root = [2,1,1,1,3,null,null,null,null,null,1]
输出:1 
解释:上图为给定二叉树。总共有 3 条从根到叶子的路径:绿色路径 [2,1,1] ,路径 [2,1,3,1] 和路径 [2,1] 。
     这些路径中只有绿色路径是伪回文路径,因为 [2,1,1] 存在回文排列 [1,2,1] 。

提示:

  • 给定二叉树的节点数目在 1 到 10^5 之间。
  • 节点值在 1 到 9 之间。

解题分析

其实就是,我们要统计,从 根节点 到任意一个叶节点的情况。

伪回文串,只要求是一个排列,也就是说,只要 1 - 9 这 9 个数字各自出现的次数,可以排列出一种 回文串即可。那么根据回文串,我们可以知道,是对称的,所以 出现次数应该是 偶数,除了 可以最中间的那一个数 是 奇数出现。因此,只要 1 -9 这 9 个数各自的出现次数中,奇数的情况 <= 1 即可是 伪回文。

那么剩下的就是 DFS,注意,应该是 DFS + 回溯,因为我们要统计 从 根节点到 另一个 节点的 出现次数,比如当了 a 节点,那么继续往下 dfs 那没问题,如果 从 a 节点返回,去到 和  a 同层的其他节点开始,那么 a 节点这个 出现次数 就要去掉。

所以是 dfs + 回溯,时间复杂度是,需要遍历每一个节点,到了叶节点的时候,需要去枚举 1- 9 每个数字各自的出现次数,所以总的时间复杂度是 O(9 * n)

AC代码(C++)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int cnt[10];
    int ans;

    void dfs(TreeNode* root)
    {
        if(root == nullptr) return;
        ++cnt[root->val];  // 把该节点的值,保存下来
        if(root->left == nullptr && root->right == nullptr)  // 如果这个节点是 叶节点,那么就说明这是一条路径了,就要判断 是不是伪回文。
        {
            int c = 0;
            for(int i = 1;i <= 9; ++i)
            {
                if(cnt[i] % 2 == 1) ++c;
            }
            if(c <= 1) ++ans;  // 奇数的出现次数 <= 1,说明是可以排列得到一个回文串的。
        }
        else  // 如果节点不是叶节点,那就要继续 dfs 下去,直到叶节点。
        {
            dfs(root->left);
            dfs(root->right);
        }
        --cnt[root->val];  // 最后是 回溯
    }

    int pseudoPalindromicPaths (TreeNode* root) {
        ans = 0;
        dfs(root);
        return ans;
    }
};

4.两个子序列的最大点积

题目链接

https://leetcode-cn.com/problems/max-dot-product-of-two-subsequences/

题意

给你两个数组 nums1 和 nums2 。

请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。

数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。

示例 1:

输入:nums1 = [2,1,-2,5], nums2 = [3,0,-6]
输出:18
解释:从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。
它们的点积为 (2*3 + (-2)*(-6)) = 18 。

示例 2:

输入:nums1 = [3,-2], nums2 = [2,-6,7]
输出:21
解释:从 nums1 中得到子序列 [3] ,从 nums2 中得到子序列 [7] 。
它们的点积为 (3*7) = 21 。

提示:

  • 1 <= nums1.length, nums2.length <= 500
  • -1000 <= nums1[i], nums2[i] <= 100

解题分析

方法一、O(n ^ 4) --> O(n ^ 3) 的状态设置

状态设置:dp[ i ][ j ] 为 选择 A 的第 i 个 和 选择 B 的第 j 个,的最大点积和

那么我们就需要 去找到 所有 dp[ 0 ~ i - 1][0 ~ j - 1] 中的最大值,这样子转移过来,所以转移方程是 

dp[ i ][ j ] = max(dp[ 0 ~ i - 1][0 ~ j - 1]) + A[ i ] * B[ j ]

那么初始值就是 dp[ 0 ][ all j ] = dp[ all i ][ 0 ] = 0,也就是啥也不选的时候。

因为最后我们要求是,非空,也就是至少要选一个,因此最后的答案是枚举,所有 dp[ 1 ...][ 1....] 至少要选择一个的中的最大值即可。

那么这里有一个问题,在转移的时候,dp[ 0 ~ i - 1][0 ~ j - 1] 最大值,如果直接枚举,那就需要时间复杂度是 O(n ^ 4) 这样子会超时。

那么我们分析,当 枚举 i 和 j 的时候,我们是要得到它们之前的最大值。

比如 一开始 i = 3,j = 3的时候,那么j = 4 的时候,原本已经有了 dp[ 0 1 2 ][ 0 1 2] 的最大值 mx 了,那么此时当 j = 4 的时候,需要多 dp[ 0 1 2][3],就需要 计算 mx 和这几个 中的最大值,因此,每一个 计算 j 的时候,需要计算 dp[ 0~ i-1][ j -1],所以其实只需要 多遍历 一次 i 即可,所以总的时间复杂度为 O(n ^ 3),不会超时。

方法二、O(n ^ 2) 的状态设置

此时我们假设 dp[ i ][ j ] 表示,A 的前 i 个,和 B 的前 j 个 中,选出了某一些组成的最大点积和(没要求一定要选 第 i 个 和 第 j 个)

那么此时,我们的转移过来,有四种可能:

1、没有选择 A 的 第 i 个,没有选择 B 的第 j 个,说明此时的最大点积和,应该就是 前 i - 1 和 前 j - 1 组合的,也就是 dp[ i - 1][ j - 1]

2、没有选择 A 的 第 i 个,选择了 B 的 第 j 个,那么此时,最大点积和,应该是 前 i - 1 和 前 j 个的,但是此时我们要注意的一点,因为我们设置的 状态  dp[ i ][ j ] 不需要要求 一定要选 第 i 个 和 第 j 个。那么此时,我们是相当于 前 i - 1 和 前 j 个,同时要求是选择了 第 j 个。

但是注意了,我们知道 dp[ i - 1 ][ j ] 是前 i - 1 和 前 j 个,包括两个状态,一定选择 第 j 个,和不一定要求选择其。那么 dp[ i - 1 ][ j ] 的范围更大,那么我们用其来更新,只是将 考虑范围变大,那么对于找最优解来说,是可以的(只要不是把范围缩小),因此我们可以用 dp[ i - 1 ][ j ] 当作来更新 转移方程。

3、选择 A 的 第 i 个,没有选择 B 的 第 j 个。类似 2,那么就是 dp[ i ][ j -1]

4、选择 A 的 第 i 个,选择 B 的 第 j 个,那么就是,dp[ i - 1 ][ j -1] + A[ i ] * B[ j ]

(注意的是,1 情况,其实被 2 和  3 情况包括在其中的,所以可以看成是,只有 2 3 4 三个情况)

那么就是上面的四种情况的中的最大值 当作 dp[ i ][ j ]。

初始化:也就是根据我们定义的状态来进行判断,也就是 dp[ 0 ][ all j ] = dp[ all i ][ 0 ] = 0。即其中一个没有选的时候,点积和是 0

最后答案:要注意,我们要求,是非空,也就是至少要选择,那么如果直接找 dp[ i ][ j ] 所有中的最大值,由于我们定义的状态,不需要一定选择,可能出现,最大值是 0,即没有选择任何一个。那么这就不符合题意

因此,我们要找的答案,不是直接 dp[ i ][ j ],而应该是,转移 4 情况,也就是,至少保证有选择,那么取 4 情况中的所有最大值才是最后答案。

这样子,我们只需要枚举 i 和 j 即可,时间复杂度是 O(n ^ 2)

AC代码(状态一、O(n ^ 3)  C++)

const int INF = 5e7 + 50;
class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), m = nums2.size();
        vector<vector<int> > dp(n + 1, vector<int> (m + 1, -INF));
        dp[0][0] = 0;

        for(int i = 1;i <= n; ++i)
        {
            int mx = 0;
            for(int j = 1;j <= m; ++j)
            {
                dp[i][j] = max(dp[i][j], mx + nums1[i - 1] * nums2[j - 1]);
                
                for(int ii = 0;ii < i; ++ii)
                    mx = max(mx, dp[ii][j]);
            }
        }
        int ans = -INF;
        for(int i = 1;i <= n; ++i)
        {
            for(int j = 1;j <= m; ++j)
            {
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
};

AC代码(状态二、O(n ^ 2)  C++)

const int INF = 5e7 + 50;
class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), m = nums2.size();
        vector<vector<int> > dp(n + 1, vector<int>(m + 1, -INF));
        // 初始化
        for(int i = 0;i <= n; ++i) dp[i][0] = 0;
        for(int j = 0;j <= m; ++j) dp[0][j] = 0;

        int ans = -INF;
        for(int i = 1;i <= n; ++i)  // 开始转移
        {
            for(int j = 1;j <= m; ++j)
            {
                dp[i][j] = max(dp[i - 1][j - 1], max(dp[i - 1][j], dp[i][j - 1]));
                int t = dp[i - 1][j - 1] + nums1[i - 1] * nums2[j - 1];  // 第 4 中情况
                ans = max(ans, t);  // 答案是第四种情况下的所有最大值
                dp[i][j] = max(dp[i][j], t);
            }
        }
        return ans;
    }
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值