Skip to content

Bit-by-bit greedy

試填法

可以透過整數的連續性將試填法分成兩種做法:

  • 假設 i-th 為 1/0 則,能否有符合限制的前綴。
for(int i = hi; i >= 0; --i) {
    mask |= 1 << i;
    int nxt = ans | (1 << i);
    for(auto ...) {//traverse element to meet the requirments
        res  [op=] (x & mask);
        if(check ...) {
            ans = nxt
            ...
        }
        ...
    }
}
  1. 421. 数组中两个数的最大异或值

    題目要求找出最大 \(x \oplus y\),透過維護前綴 mask 以及在 i-th 位元試填 nxt,使用 hash table 找出是否有 \(nxt \oplus x_{pre_i}\),若有表示 i-th 可以填 1。

  2. 2935. Maximum Strong Pair XOR II

    題目要求找出符合限制的 \(x \oplus y\),透過維護前綴 mask 以及在 i-th 位元試填 nxt,使用 hash table 找到能否有符合題目要求的 \(nxt \oplus x_{pre_i}\),若有表示 i-th 可以填 1。

  3. 3022. 给定操作次数内使剩余元素的或值最小

    這題乍看之下要使用下面的方法做逐位排除確定,但是從 [7,3,15,14,2,8] 這個例子會發現這樣沒辦法得到解答,可以發現若是題目把可以操作的元素限制在一個集合當中,並且要透過維護一些性質並計算出答案,就要使用類似上面的模板的思考方向。

    這題是要求限制 k 次 AND 操作後集合中的元素進行 OR 所得最小,透過維護前綴 mask 確定那些位元會是 0 得到當前操作 \(\le k\) 次的解。

  4. 3806. 增加操作后最大按位与的结果

    貢獻法 + 試填法。構造出 target 需要多少貢獻,選最小貢獻的 m 做 AND。

  5. 3859. 统计包含 K 个不同整数的子数组

    基本上就是先填看看 0 ,然後看能夠選的選的元素中是否有符合當前bit為0的元素。

    怎樣能夠選? 已經選了前綴 mask ,我們剩下能選的部分是"前綴 + \(2^i - 1\) 的子集。

  6. Codeforces:

    1. XOR-factorization

      直覺的貪心會掉進陷阱,必須逐位元維護那些數是沿著 limit n,那些數是小於 n。

核心想法:

若是題目把可以操作的元素限制在一個有限集合(大小大概是 \(10^5\) )當中。透過維護前綴 mask 每次都只取 x 前綴,在遍歷過程中維護題目要求,重點在於遍歷的過程中要滿足限制。

有些題目可以使用 0-1 Trie 完成。


  • k-th lexicographical binary string

    • 對於每一位,假設這一位是 0/1,計算後面還能構成多少合法字串,如果 \(\ge k\),就選 0,否則選 1 並扣掉數量。

    • 或者假設這一位是 1,計算左邊和右邊可以產生多少貢獻 x x x 1 o o .. ,如果 \(\le k\) 那就把這位設為 1 並扣掉貢獻。

    • 核心想法: 利用整數的連續性,不斷縮小規模從而確定答案。

  • 3007. 价值和小于等于 K 的最大数字

    計算 i-th 設 1 會產生出的貢獻,若是小於 k ,減去貢獻並縮小規模,貢獻的公式 \(cnt = pre_i \times 2^i + \lfloor \frac{i}{x} \rfloor \times 2^{i - 1}\),直觀理解為右邊有 \(2^i\) 的方案數可以使左邊 \(pre_i\) 產生貢獻。對於右邊的情況則可以用組合數學的方式思考,在 [0, i - 1] 位元上共有 \(\lfloor \frac{i}{x} \rfloor\) 個是可以產生貢獻的位元,對於每個位元其可以產生的貢獻為 \(2^{i - 1}\)\(2^{i - 1}\) 代表著除了自己之外的方案數,兩個相乘 \(\lfloor \frac{i}{x} \rfloor \times 2^{i - 1}\) 表示在 x 位置上可以產生的貢獻。

  • 3145. 大数组元素的乘积

    這題跟上面那題的思路是一樣的,只有在細節上不一樣,因為題目限制只有 2 的冪次相乘,所以可以將相乘的計算改為冪次和的計算,避免數值溢出。另外對於右邊的冪次和 \(\sum_{0}^{x = i - 1} x * 2^{i - 1}\),所以可以利用等差級數和公式得到 \(\frac{i(i - 1)}{2} \times 2^{i - 1}\)

  • 3821. 二进制中恰好K个1的第N小整数

    由高位至低位透過假設 i-th bit 填 0 的方案數決定這個位元是否填 1

  • 3344. 最大尺寸数组

    這題要計算的是 $\(\sum_{j, k}(j \text{ OR } k) = \sum_{j, k} \sum_m(j \text{ OR } k) \text{ AND } 2^m = \sum_m \sum_{j, k} (j \text{ AND } 2^m) \text{ OR } (k \text{ AND } 2^m)\)$

    由於 \(j \text{ AND } 2^m\) 只會是 \(2^m\) 或是 0,所以可以轉化為 \(n^2 - cnt^2(j \text{ AND } 2^m = 0)\),用全部可能 \(n^2\) 減去兩側皆為 0 的情況:

    \[\sum_{j, k} \sum_m(j \text{ AND } 2^m) \text{ OR } (k \text{ AND } 2^m) = \sum_{j, k} \sum_m 2^m(n^2 - Card^2\{j | j \text{AND} 2^m = 0\})\]

    設函數 \(Z(n, m) = Card\{j | 0 \le j < n, j \text{AND} 2^m = 0\}\)\([0, n - 1]\) 的數中第 m 位為 0 的個數。

    \[\sum_{j, k} \sum_m(j \text{ AND } 2^m) \text{ OR } (k \text{ AND } 2^m) = \sum_m 2^m(n^2 - Z^2(n, m))\]

    可以發現 0, 1 在 [0, n - 1] 中是週期性出現的,所以 Z(n, m) 可以利用這個性質很容易的求出,我們可以利用二分的方式得到 \(O(\log^2{n})\) 的方法。

    可以發現我們要計算的整數範圍是 [0, n - 1],由整數的連續性可以知道可以透過逐位確定不斷縮小問題規模。


    增量計算

    講解

    要使用逐位確定的話必須求出在第 i 位產生的貢獻,假設 \(n = a \cdot 2^b\)\(n\) 是二進制右邊皆為 0 的形式。

    \(G(n) = \sum_{0\le j, k < n}(j \text { OR } k) = \sum_m 2^m(n^2 - Z^2(n, m))\)

    我們想要計算的是對於 \(2^{b-1}\) 產生的增量,也就是 \(G(n + 2^{b-1}) - G(n)\),當我們由高至低確定第 b - 1 位是否會超過限制 s ,若是不超過則 \(n = n + 2^{b-1}\) 不斷逼近 s。

    由上面的 \(G(n)\) 可以知道,\(G(n + 2^{b-1}) = \sum_{m}2^m((n+2^{b-1})^2 - Z^2(n+2^{b-1},m))\),此時需要關注的點是 \(Z(n+2^{b-1},m)\),要考慮的是 \(Z(n+2^{b-1},m)\)\(Z(n+2,m)\) 的關係變化。

    我們要考慮的是四種情況:

    1) \(0 \le m < b - 1\)

    由二進制的週期性可以知道 \(Z(n + 2^{b-1}, m) = \frac{n + 2^{b-1}}{2}\)

    此時產生的增量為

    \[ \begin{aligned} G(n + 2^{b-1}) - G(n) &= \sum_m 2^m((n+2^{b-1})^2 - Z^2(n+2^{b-1}, m)) - \sum_m 2^m(n^2 - Z^2(n, m)) \\ &= \sum_m 2^m((n+2^{b-1})^2 - Z^2(n+2^{b-1}, m) - n^2 + Z^2(n,m)) \\ &= \sum_m 2^m((n+2^{b-1})^2 - (\frac{n + 2^{b-1}}{2})^2 - n^2 + (\frac{n}{2})^2) \\ &= \frac{3}{4} \sum_m 2^m((n+2^{b-1})^2 - n^2) \\ &= \frac{3}{4} \cdot (2^{b-1} - 1) \cdot ((n+2^{b-1})^2 - n^2) \\ &= \frac{3}{4} \cdot (2^{b-1} - 1) \cdot 2^{b-1}(2 \cdot n + 2^{b-1}) \\ \end{aligned} \]

    可以利用等比級數和公式將式子化簡 \(\sum_{m = 0}^{b - 2} 2^m = (2^{b-1} - 1)\)

    2) \(m = b - 1\)

    \([n, n + 2^b-1)\) 上第 b - 1 位都是 0,這是 \(Z(n + 2^{b-1}, b - 1)\) 產生的增量,也就是 \(Z(n + 2^{b-1}, b - 1) = \frac{n}{2} + 2^{b-1}\)

    因此增量為:

    \[ \begin{aligned} G(n + 2^{b-1}) - G(n) &= \sum_m 2^m((n+2^{b-1})^2 - Z^2(n+2^{b-1}, b-1)) - \sum_m 2^m(n^2 - Z^2(n, b-1)) \\ &= \sum_m 2^m((n+2^{b-1})^2 - (\frac{n}{2} + 2^{b-1})^2) - \sum_m 2^m(n^2 - (\frac{n}{2})^2) \\ &= \sum_m 2^m((n+2^{b-1})^2 - (\frac{n}{2} + 2^{b-1})^2 - n^2 + (\frac{n}{2})^2) \\ &= \sum_m 2^m(2 \cdot n \cdot 2^{b-1} - n \cdot 2^{b-1}) \\ &= \sum_m 2^m(n \cdot 2^{b-1}) \\ &= 2^{b-1}\cdot n \cdot 2^{b-1} \\ \end{aligned} \]

    3) \(m \ge b, (n \text{ AND } 2^m) = 0\)

    在比 b - 1 更高位時為 0 的情況,也就是在 \([n, n + 2^{b-1})\) 中為 0 的情況。對於每一位有 \(Z(n + 2^{b-1}, m) = Z(n, m) + 2^{b-1}\),代表在 \(Z(n, m)\) 上再增加 \(2^{b-1}\)

    有增量:

    \[ \begin{aligned} G(n + 2^{b-1}) - G(n) &= \sum_m 2^m((n+2^{b-1})^2 - (Z(n, m) + 2^{b-1})^2) - \sum_m 2^m(n^2 - Z^2(n, m)) \\ &= \sum_m 2^m(n+2^{b-1})^2 - (Z(n, m) + 2^{b-1})^2 - n^2 + Z^2(n, m) \\ &= \sum_m 2^m(2 \cdot n \cdot 2^{b-1} + 2 \cdot Z(n, m) \cdot 2^{b-1}) \\ &= \sum_m 2^m \cdot 2^{b-1}(2n + 2Z(n, m)) \\ &= 2^{b-1} \sum_m 2^m (2n + 2Z(n, m)) \\ \end{aligned} \]

    \(ZC(n, b) = \sum_{m \ge b, n \text{ AND } 2^m = 0} 2^m (2n + 2Z(n, m))\)

    最後得到:

    \[G(n + 2^{b-1}) - G(n) = ZC(n, b)2^{b-1}\]

    4) \(m \ge b, (n \text{ AND } 2^m) = 2^m\)

    在比 b - 1 更高位時為 1 的情況,也就是在 \([n, n + 2^{b-1})\) 中為 1 的情況。對於每一位有 \(Z(n + 2^{b-1}, m) = Z(n, m)\)。這個意義是對於 \(n \text{ AND } 2^m = 2^m\) 的情況 \(Z(n + 2^{b-1}, m)\) 不會有變化(\(Z\) 的定義是 0 的個數)。

    此時的增量

    \[ \begin{aligned} G(n + 2^{b-1}) - G(n) &= \sum_m 2^m((n+2^{b-1})^2 - Z^2(n, m)) - \sum_m 2^m(n^2 - Z^2(n, m)) \\ &= \sum_m 2^m((n+2^{b-1})^2 - n^2)\\ &= \sum_m 2^m(2 \cdot n \cdot 2^{b-1} + 2^{b-1} \cdot 2^{b-1}) \\ &= \sum_m 2^m \cdot 2^{b-1}(2 n + 2^{b-1} ) \end{aligned} \]

    由於我們考慮的是 \((n \text{ AND } 2^m) = 2^m\),且此時的 n 為 \(n = a \cdot 2^b\) 的形式,所以可以得到在 \(m \ge b, (n \text{ AND } 2^m) = 2^m\) 的情況下 \(\sum_m 2^m = n\)

    因此增量為:

    \[G(n + 2^{b-1}) - G(n) = n \cdot 2^{b-1}(2 n + 2^{b-1} )\]

    最後全部求和可以得到對於 \(n + 2^{b-1}\) 產生的增量:

    \[ \begin{aligned} G(n + 2^{b-1}) - G(n) &= ZC(n, b)2^{b-1} + n \cdot 2^{b-1}(2 n + 2^{b-1} ) + 2^{b-1}\cdot n \cdot 2^{b-1} \\ &+ \frac{3}{4} \cdot (2^{b-1} - 1) \cdot 2^{b-1}(2 \cdot n + 2^{b-1}) \end{aligned} \]

    \(S = 2^{b-1}\) 簡化為:

    \[ \begin{aligned} G(n + 2^{b-1}) - G(n) &= ZC(n, b)\cdot S + n \cdot S(2 n + S) + S\cdot n \cdot S \\ &+ \frac{3}{4} \cdot (S - 1) \cdot S(2 \cdot n + S) \end{aligned} \]

    接著要考慮的是如何維護 \(ZC(n, b)\),因為對於 b - 1 位,有兩種可能 0 or 1,所以對於更新 \(ZC(n, b)\) 必須考慮兩種情況:

    • b - 1 位為 0 時,此時 n 不變,需要計算的是 \(ZC(n, b - 1)\),由定義可以得到 \(ZC(n, b-1) = ZC(n, b) + 2^{b-1} (2n + 2Z(n, b-1))\),而 \(Z(n, b-1)\) 時 b - 1 位的變化為週期性的,所以 \(Z(n, b-1) = \frac{n}{2}\)

      得到 \(ZC(n, b-1) = Z(n, b) + 2^{b-1} \cdot n\)

    • b - 1 位為 1 時,n 變成 \(n + 2^{b-1}\),需計算 \(ZC(n + 2^{b-1}, b-1)\),可以知道對於 \(Z(n + 2^{b-1}, m)\) 此時會在 \(Z(n, m)\) 上產生 \(2^{b-1}\) 的增量

      \[Z(n + 2^{b-1}, m) = Z(n, m) + 2^{b-1}\]

      兩邊同時取負號加上 \(n + 2^{b-1}\) 後乘 2 可以得到

      \[2(n + 2^{b-1}) - Z(n + 2^{b-1}, m) = 2n + 2Z(n, m)\]

      也就是

      \[ZC(n + 2^{b-1}, b - 1) = ZC(n, b)\]

    結合上面兩種情況可以維護 \(ZC\)

    code


Logtrick

  • 單調性

對於位元運算的時候,不妨將每個元素的位元運算看成對不同集合做運算,也就是:

\[ \begin{aligned} 001 = \{b_1\}, 010 &= \{b_2\}, 011 = \{b_2, b_3\} \\ 001 | 010 &= 011 = \{b_2, b_3\}\\ \end{aligned} \]

由集合的角度出發,當遍歷陣列時,一旦固定右端點,不斷的往左進行 OR 運算,集合中的元素只會單調的增加,由陣列的元素值域 \([1, x]\) 可以知道集合的大小最大只會有 \(\lfloor \log{x}\rfloor\)

若是做 AND 運算則集合的大小會單調遞減。

利用上面這個性質,我們可以將每次變化的位置記錄下來計算答案所需的部分。

由下面的圖片可以發現若是可以記錄下集合大小變化的位置,那就可以快速處理這些區間的資訊。

or

AND 運算會使集合大小單調遞減。

and

  • 可以透過維護一個集合 set 由左往右遍歷陣列,為了維護陣列單調性,每次元素都要與陣列中的元素進行 OR/AND。

  • 模板題:

  • Leetcode #3878. 统计好子数组

    這題需要統計子陣列的個數,合法的子陣列為子陣列元素作或運算後得到的值 \(v_{or}\) 出現在子陣列中。

    透過上圖可以觀察到,設右端點在 \(i\),左端點在 \([l,r]\) 中的子陣列的 OR 都是 \(v_{or}\)。設 \(j=pre[v_{or}]\)。當 \(j \ge l\) 時,左端點為 \(l,l+1,...,min(r,j)\),右端點為 \(i\) 的子陣列都包含 \(v_{or}\),這一共有

    \[min(r,j)−l+1\]

    ref.

    • 原地去重要看一下。
class Solution {
public:
    long long countGoodSubarrays(vector<int>& nums) {
        vector<pair<int, int>> or_left; // (子数组或值,最小左端点)
        unordered_map<int, int> last;
        long long ans = 0;

        for (int i = 0; i < nums.size(); i++) {
            int x = nums[i];
            last[x] = i;

            // 计算以 i 为右端点的子数组或值
            for (auto& [or_val, _] : or_left) {
                or_val |= x; // **根据题目修改**
            }
            // x 单独一个数作为子数组
            or_left.emplace_back(x, i);

            // 原地去重(相同或值只保留最左边的)
            // 原理见力扣 26. 删除有序数组中的重复项
            int m = 1;
            for (int j = 1; j < or_left.size(); j++) {
                if (or_left[j].first != or_left[j - 1].first) {
                    or_left[m++] = or_left[j];
                }
            }
            or_left.resize(m);

            for (int k = 0; k < m; k++) {
                auto [or_val, left] = or_left[k];
                int right = k + 1 < m ? or_left[k + 1].second - 1 : i;
                // 对于左端点在 [left, right],右端点为 i 的子数组,OR 值都是 or_val
                auto it = last.find(or_val);
                if (it != last.end() && it->second >= left) {
                    ans += min(right, it->second) - left + 1;
                }
            }
        }
        return ans;
    }
};

Other

need to practice:

leetcode 3106. 满足距离约束且字典序最小的字符串 leetcode 3720. 大于目标字符串的最小字典序排列 leetcode 3734. 大于目标字符串的最小字典序回文排列 leetcode 3470. 全排列 IV leetcode 3022. 给定操作次数内使剩余元素的或值最小 leetcode 3518. 最小回文排列 II