週日收拾行李,準備返校,就沒時間參加了,聽說是手速題,還是挺簡單的,最後一題兩種假設 DP 狀態的方法,第一種方法筆記簡單,容易想到。第二種方法,如果做過類似的題目,比如 LeetCode 上的第 72 題 “編輯距離”,那就很好做了。
第一題:字符串分割 + 模擬(手動分割 或者 C++ stringstream 分割)。
第二題:滑動窗口。
第三題:DFS。
第四題:動態規劃 DP,有兩種狀態假設方法。
詳細題解如下。
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;
}
};