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;
    }
};

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章