【Leetcode C++ 解題筆記】Divide & Conquer - part 2
===
[TOC]
本筆記僅供個人學習用途,內容斟酌參考。
## 1. Convert Sorted Array to Binary Search Tree
Problem Source:https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/?envType=problem-list-v2&envId=divide-and-conquer
難度:Easy
題意大致上就是要你把一般常見的陣列轉換成 BST。
補充一下 BST 特性:
- 左子樹的 Nodes 都比 Root 還小;反之,右子樹都比 Root 還大。
- 子樹必須都是 BST。
解題思路:
用到 Divide & Conquer 的概念,每次把「目前」陣列片段選中間元素作為 root。
對 root 左側遞迴呼叫產生左子樹,右側也一樣,直到子陣列長度為 0 就結束。
範例程式碼:
```cpp=
/**
* 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:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return buildBST(nums, 0, nums.size() - 1);
}
private:
TreeNode* buildBST(vector <int>& nums, int left, int right){
if (left > right) return nullptr;
int mid = left + (right - left) / 2;
TreeNode* node = new TreeNode(nums[mid]); // root
node -> left = buildBST(nums, left, mid - 1);
node -> right = buildBST(nums, mid + 1, right);
return node;
}
};
```
## 2. Majority Element
Problem Source:https://leetcode.com/problems/majority-element/description/?envType=problem-list-v2&envId=divide-and-conquer
難度:Easy
題目敘述:
給定長度為 n 的陣列 nums,回傳多數元素(the majority element)。
所謂多數元素就是元素出現超過 `[n / 2]` 次。你可以假設它都會出現在陣列中。
解題思路:
用 Divide & Conquer 做這題,一樣是二分法去做,主要處理 `[left, mid]` 和 `[mid + 1, right]` 這兩段區間。
終止條件是 `if (left == right) return nums[left];` 表示元素個數剩一個,該元素必為多數元素,故回傳此值。
遞迴處理 lm 和 rm 的部分(左右區間)。
之後做合併動作,也直接做判斷:`if (lm == rm) return lm;` 表示左右多數元素相同。
如果不同,就需要去數 lm 跟 rm 共出現幾次:
```cpp=
int c1 = count(nums.begin() + left, nums.begin() + right + 1, lm);
int c2 = count(nums.begin() + left, nums.begin() + right + 1, rm);
return c1 > c2 ? lm : rm;
```
範例程式碼:
```cpp=
class Solution {
public:
int majorityElement(vector<int>& nums) {
return majority(nums, 0, nums.size() - 1);
}
int majority(vector <int>& nums, int left, int right){
if (left == right) return nums[left];
int mid = left + (right - left) / 2;
int lm = majority(nums, left, mid);
int rm = majority(nums, mid + 1, right);
if (lm == rm) return lm;
int c1 = count(nums.begin() + left, nums.begin() + right + 1, lm);
int c2 = count(nums.begin() + left, nums.begin() + right + 1, rm);
return c1 > c2 ? lm : rm;
}
};
```
## 3. Reverse Bits
Problem Source:https://leetcode.com/problems/reverse-bits/description/?envType=problem-list-v2&envId=divide-and-conquer
難度:Easy
題目敘述:
反轉 32 位元的偶數有號整數(uint)。
先以 8 位元舉例比較好懂,32 位元太長了。
假設原本數字是 0 0 0 1 0 1 1 0,二進位是從右開始為起始索引,最左邊是最終索引 7。
```
Index :7 6 5 4 3 2 1 0
0 0 0 1 0 1 1 0
```
所謂反轉位元的意思就是把索引顛倒:
```
Index :0 1 2 3 4 5 6 7
0 1 1 0 1 0 0 0
```
就這樣。
範例程式碼:
```cpp=
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
n = (n >> 16) | (n << 16);
n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);
n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);
n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);
n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);
return n;
}
};
```
基本上這段程式碼就是不斷將 n 切割成兩部分,並且作互換(用 `|` OR 運算子互換)的動作,以此來達到反轉位元的效果。
## 4. Number of 1 Bits
Problem Source:https://leetcode.com/problems/number-of-1-bits/description/?envType=problem-list-v2&envId=divide-and-conquer
難度:Easy
題目敘述:
給定一個正整數 n,寫一個函數回傳其二進位表示法中的設定位數(也稱為漢明權重)。
set bits 指的是有 1 的位元數量,如 1011 就有 3 個 set bits,這個也稱為 Hamming weight。
範例程式碼講解:
雖然有 `std::popcount`(C++ 20) 或 `__builtin_popcount()`(看編譯器支不支援)可以用,但上述兩種方法若在某些競賽上使用可能不切實際。
所以我們來手刻一下 popcount。
在細講這個演算法之前,我們要先知道幾個 bitmasks:
```
0x55 = 01010101
0x33 = 00110011
0x0f = 00001111
0xff = 11111111
```
第一行(`n = (n & 0x55555555) + ((n >> 1) & 0x55555555);`)用到 bitmasks `0x55`,挑出 n 中所有在奇數位(bit 0, 2, 4, ...)的 bit,再把 n 右移 1 位後(偶數位),也用相同遮罩挑出偶數位 bit,最後兩者相加。
這一層分治主要將 32 個 bit 分成 16 組,每組 2bit。
第二行(`n = (n & 0x33333333) + ((n >> 2) & 0x33333333);`)用到 bitmasks `0x33`,將剛才算好的每 2bit 組做累加操作,把相鄰的兩個 2bit 組合成一個 4bit 組並累加 1 的數量。
然後這層分治也將 32bit 分成 8 組,每組 4bit,計算每組內 1 的個數。
第三行(`n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);`)用到 bitmasks `0x0f`,將相鄰的兩個 4bit 組合成一個 8bit 組,同樣使用 bitmasks 分低 4bit 及高 4bit 相加。
該層分治再把 32bit 分成 4 組,每組 8bit,計算每組內 1 的個數。
第四行(`n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);`)也用到一樣的 bitmasks,只不過多加個 f,在二進位上就多了四個 f。
這層分治把將相鄰兩個 8bit 組合成 1 個 16bit 組,再累加各 8bit 中 1 的個數。
第五行(`n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);`)用到最後一個 bitmasks `0xff`,將兩個 16bit 組合成整個 32bit 的組合,再將前後 16bit 中 1 的個數合併。
最終我們就從 32 bit 裡面得到所有 1 的數量了,為什麼是 32 bit 呢?因為題目要求 int,int 就剛好 32 bit。
完整範例程式碼:
```cpp=
class Solution {
public:
int hammingWeight(int n) {
n = (n & 0x55555555) + ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);
n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);
n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
return n;
}
};
```
## 5. Longest Nice Substring
Problem Source:https://leetcode.com/problems/longest-nice-substring/description/?envType=problem-list-v2&envId=divide-and-conquer
難度:Easy
題目敘述:
一字串 s 如果是 nice,那對於每個字母表中的每個字母都包含 s,它以大寫和小寫形式出現。
舉例來說,`"abABB"` 是 nice,因為 `'A'` 和 `'a'` 出現,並且 `'B'` 和 `'b'` 出現。然而,`"abA"` 不是 nice,因為 `'b'` 出現,而 `'B'` 沒有出現。
給定一個字串 s,回傳 s 中最長的、滿足 nice 條件的子字串。如果有多個,則回傳最早出現的子字串。如果沒有,則傳回空字串。
解題思路:
我們用資料結構 `unordered_set <int> seen(s.begin(), s.end());` 來實作這題。
首先跑迴圈,內部判斷一個字串中的字元 c 有沒有其他相反的大小寫,如 A 要找到對應的小寫 a 才是一個 nice substring。(這邊要找他對應相反的大小寫,我們用 `seen.find()` 去找)
大小寫判斷可以額外寫一個 `toggleCase()` 函數去判斷,如:
```cpp=
char toggleCase(char c){
if (islower(c)) return toupper(c);
else return tolower(c);
}
```
如果 `seen.find()` 沒有找到,則回傳 `end()` 這個 iterator。
沒找到的話我們就要做遞迴去找了,這邊用分治法的概念將字串切兩半去找:
```cpp=
string left = longestNiceSubstring(s.substr(0, i));
string right = longestNiceSubstring(s.substr(i + 1));
```
`substr(0, i)` 為從索引 0 開始,擷取長度 i 的子字串(不含索引 i 的字元),形成左半邊的子字串。
`substr(i + 1)` 從索引 i + 1 開始,擷取到字串尾的子字串,形成右半邊子字串。
最後回傳較長的子字串:`return left.size() >= right.size() ? left : right`。
最後的最後,如果我們前面判斷都沒有回傳 `end()` 的話,那就表示說整串字串完美符合 nice substring 的條件,就直接 `return s`。
完整範例程式碼:
```cpp=
class Solution {
public:
string longestNiceSubstring(string s) {
if (s.length() < 2) return "";
unordered_set <char> seen(s.begin(), s.end());
for (int i = 0; i < (int)s.length(); ++i){
if (seen.find(toggleCase(s[i])) == seen.end()){
string left = longestNiceSubstring(s.substr(0, i));
string right = longestNiceSubstring(s.substr(i + 1));
return left.size() >= right.size() ? left : right;
}
}
return s;
}
private:
char toggleCase(char c){
if (islower(c)) return toupper(c);
else return tolower(c);
}
};
```