---
# System prepended metadata

title: 【Leetcode C++ 解題筆記】Divide & Conquer - part 2
tags: [資料結構, 程式設計, C++, 競程, 演算法, 程式語言, CP]

---

【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);
    }
};
```