2305. Fair Distribution of Cookies

https://leetcode.com/problems/fair-distribution-of-cookies/

由於這一題數據量不大,因此其中一個方法就是暴力解,直接遍歷所有的可能性。

用到方法也是非常經典的 backtracking。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int distributeCookies(vector<int>& cookies, int k) {
    int n = cookies.size();
    if (n == k){
        return *max_element(begin(cookies), end(cookies));
    }
    vector<int> distribution(k, 0);
    int ans = reduce(begin(cookies), end(cookies));
    back_track(cookies, distribution, 0, ans);
    return ans;
}

void back_track(vector<int>& cookies, vector<int> &distribution, int cur, int &ans){
    int n = cookies.size();
    if (cur >= n){
        ans = min(ans, *max_element(begin(distribution), end(distribution)));
        return;
    }
    int k = distribution.size();
    for (int i = 0; i < k; i++){
        distribution[i] += cookies[cur];
        back_track(cookies, distribution, cur + 1, ans);
        distribution[i] -= cookies[cur];
    }
    return;
}

time complexity: $O(k^n)$
space complextiy: $O(k)$
where n = cookies.length

另一個方法是 dynamic programming。
由於小朋友是同質的,也就是全部餅乾分給 1 號小朋友或是全部餅乾分給 2 號小朋友,其實並沒有差異,因此可以用 dynamic programming 來記錄這種重複的問題。

用二進位的數字 mask 來記錄餅乾的使用情況,i 來記錄還剩下幾位小朋友可以分餅乾。
例如:dp[i = 3][mask = 5] = dp[3][$101_2$] 記錄的就是「在剩下 3 個小朋友,並且第 0,2 塊餅乾可以被分的情況下,拿最多餅乾的小朋友分了是多少餅乾(unfairness)?」。
此時有 $2^2$ 種分法,也就是

  1. 第 0,2 號餅乾給其中一位小朋友,此時子問題是 dp[2][$000_2$]
  2. 第 0 號餅乾給其中一位小朋友,此時子問題是 dp[2][$100_2$]
  3. 第 2 號餅乾給其中一位小朋友,此時子問題是 dp[2][$001_2$]
  4. 沒有餅乾給其中一位小朋友,此時子問題是 dp[2][$000_2$]

只要在這些情況裡面找最小的 unfairness 就好。至於這個小朋友是誰並不重要,因為小朋友是同質的。

找 unfairness 的方法也很簡單。
例如 2 號分法中,unfairness 會是 max(第 0 號餅乾的和, dp[2][$100_2$]),再跟現有的 unfairness 取較小的即可。

同時使用 sum[mask] 來記錄的就是 mask 中所有餅乾的和,這樣計算子問題的時候也會比較快。
例如: sum[$101_2$] = 第 0,2 號餅乾的和。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int distributeCookies(vector<int>& cookies, int k) {
    int n = cookies.size();
    vector<int> sum((1 << n), 0);
    for (int i = 1; i < (1 << n); i++){
        for (int j = 0; j < n; j++){
            if ((i & (1 << j)) != 0){
                sum[i] += cookies[j];
            }
        }
    }
    
    vector<vector<int>> dp(k + 1, vector<int> ((1 << n), INT_MAX));
    dp[0][0] = 0;
    for (int i = 1; i <= k; i++){
        for (int mask = 1; mask < (1 << n); mask++){
            // submask means one child get "submask" this much of cookies, which is sum[submask]
            for (int submask = 0; submask <= mask; submask++){
                if ((submask | mask) == mask){ // check submask is valid or not
                    dp[i][mask] = min(dp[i][mask], max(sum[submask], dp[i - 1][mask ^ submask]));
                }
            }
        }
    }
    return dp[k][(1 << n) - 1];
}

time complexity: $O(2^n \times n + k \times 2^n \times 2^n)$
space complextiy: $O(k \times 2^n)$
where n = cookies.length

但是其實還可以再優化一下,因為在遍歷 submask 的時候,其實遍歷了很多不符合條件的 submask。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int distributeCookies(vector<int>& cookies, int k) {
    int n = cookies.size();
    vector<int> sum((1 << n), 0);
    for (int i = 1; i < (1 << n); i++){
        for (int j = 0; j < n; j++){
            if ((i & (1 << j)) != 0){
                sum[i] += cookies[j];
            }
        }
    }
    
    vector<vector<int>> dp(k + 1, vector<int> ((1 << n), INT_MAX));
    dp[0][0] = 0;
    for (int i = 1; i <= k; i++){
        for (int mask = 1; mask < (1 << n); mask++){
            for (int submask = mask; submask != 0; submask = (submask - 1) & mask){ // 改了這裡
                dp[i][mask] = min(dp[i][mask], max(sum[submask], dp[i - 1][mask ^ submask]));
            }
        }
    }
    return dp[k][(1 << n) - 1];
}

上面這段 code 我覺得最精華的部分在於 submask = (submask - 1) & mask。這樣只會遍歷所有的子問題,而不會遍歷到其他不相干的狀況。

接下來研究一下下面這段 code 的複雜度。

1
2
3
4
5
for (int mask = 1; mask < (1 << n); mask++){
    for (int submask = mask; submask != 0; submask = (submask - 1) & mask){
        dp[i][mask] = min(dp[i][mask], max(sum[submask], dp[i - 1][mask ^ submask]));
    }
}

mask 如果有 k 個 1 bit,那麼會有 $2^k$ 個 submask。
而 mask 從 1 遍歷到 (1 « n),總共會有
$\binom{n}{1}$ 個 mask 有 1 個 1 bit
$\binom{n}{2}$ 個 mask 有 2 個 1 bit

$\binom{n}{n}$ 個 mask 有 n 個 1 bit

所以複雜度會是 $\sum_{k = 1}^n \binom{n}{k} \times 2^k$,而這個剛剛好就是 $(1+2)^n$ 的展開式。也就是說,這段 code 的複雜度就是 $O(3^n)$。

最後整體的複雜度就是
time complexity: $O(2^n \times n + k \times 3^n)$
space complextiy: $O(k \times 2^n)$
where n = cookies.length