Binary Indexed Tree / Fenwick Tree

树状数组(Binary Indexed Tree,也称为Fenwick Tree)作为一种高效的数据结构,主要用于区间查询和动态更新数据。

Fenwick Tree(芬威克树)得名于其发明者 Peter M. Fenwick(彼得·梅隆·芬威克),一位计算机科学家。他在1994年首次提出了这种数据结构,并以论文"A New Data Structure for Cumulative Frequency Tables" 的形式发表了这一成果。因为这种数据结构在处理累积频率表和其他区间查询问题上表现出了高效性,所以后来人们便以他的姓氏 Fenwick 来命名,以表彰他的贡献。

以下是论文摘要

A new method (the ‘binary indexed tree’) is presented for maintaining the cumulative frequencies which are needed to support dynamic arithmetic data compression. It is based on a decomposition of the cumulative frequencies into portions which parallel the binary representation of the index of the table element (or symbol). The perations to traverse the data structure are based on the binary coding of the index. In comparison with previous methods, the binary indexed tree is faster, using more compact data and simpler code. The access time for all operations is either constant or proportional to the logarithm of the table size. In conjunction with the compact data structure, this makes the new method particularly suitable for large symbol alphabets.

3067.在带权树网络中统计可连接服务器对数目

目标

给你一棵无根带权树,树中总共有 n 个节点,分别表示 n 个服务器,服务器从 0 到 n - 1 编号。同时给你一个数组 edges ,其中 edges[i] = [ai, bi, weighti] 表示节点 ai 和 bi 之间有一条双向边,边的权值为 weighti 。再给你一个整数 signalSpeed 。

如果两个服务器 a ,b 和 c 满足以下条件,那么我们称服务器 a 和 b 是通过服务器 c 可连接的 :

  • a < b ,a != c 且 b != c 。
  • 从 c 到 a 的距离是可以被 signalSpeed 整除的。
  • 从 c 到 b 的距离是可以被 signalSpeed 整除的。
  • 从 c 到 b 的路径与从 c 到 a 的路径没有任何公共边。

请你返回一个长度为 n 的整数数组 count ,其中 count[i] 表示通过服务器 i 可连接 的服务器对的 数目 。

说明:

  • 2 <= n <= 1000
  • edges.length == n - 1
  • edges[i].length == 3
  • 0 <= ai, bi < n
  • edges[i] = [ai, bi, weighti]
  • 1 <= weighti <= 10^6
  • 1 <= signalSpeed <= 10^6
  • 输入保证 edges 构成一棵合法的树。

思路

有一颗无根带权树,所有到服务器 c 的路径,如果路径长度能够被 signalSpeed 整除,并且路径没有重合,则这些服务器可以通过 c 连接。即 c 的每个分支上满足条件的节点可以与其它分支满足条件的节点连接。

遍历每一个节点,以其为根,使用dfs分别计算各分支满足条件的节点,然后计算服务器对。

假设根节点R有4个分支,每个分支上满足条件的节点个数为 a、b、c、d,我们可以使用下面两个方法计算服务器对:

for:
    a:ab + ac + ad
    b:bc + bd
    c:cd

或者

for
    a:0 * a
    b:a * b
    c:(a+b) * c
    d:(a+b+c) * d

第二种方法计算的其实是第一种方法斜线上的和
    a:ab + ac + ad
          /    /
    b:bc + bd
          /
    c:cd

最快的解法应该是换根dp,但是换根后节点数如何变化,处理起来比较复杂,考虑的情况也更多,容易出错。

代码

/**
 * @date 2024-06-04 8:41
 */
public class CountPairsOfConnectableServers3067 {

    private List<int[]>[] g;
    private int speed;

    public int[] countPairsOfConnectableServers_v1(int[][] edges, int signalSpeed) {
        int n = edges.length;
        g = new List[n + 1];
        speed = signalSpeed;
        int[] res = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 0; i < n; i++) {
            g[edges[i][0]].add(new int[]{edges[i][1], edges[i][2]});
            g[edges[i][1]].add(new int[]{edges[i][0], edges[i][2]});
        }
        for (int i = 0; i <= n; i++) {
            int pre = 0;
            if (g[i].size() == 1) {
                continue;
            }
            for (int j = 0; j < g[i].size(); j++) {
                int cnt = dfs(g[i].get(j)[0], i, g[i].get(j)[1]);
                res[i] += pre * cnt;
                pre += cnt;
            }
        }
        return res;
    }

    public int dfs(int root, int parent, int path) {
        int cnt = path % speed == 0 ? 1 : 0;
        if (g[root].size() == 1 && parent != -1) {
            return cnt;
        }
        for (int[] child : g[root]) {
            if (child[0] == parent) {
                continue;
            }
            cnt += dfs(child[0], root, path + child[1]);
        }
        return cnt;
    }

}

性能

1103.分糖果II

目标

排排坐,分糖果。

我们买了一些糖果 candies,打算把它们分给排好队的 n = num_people 个小朋友。

给第一个小朋友 1 颗糖果,第二个小朋友 2 颗,依此类推,直到给最后一个小朋友 n 颗糖果。

然后,我们再回到队伍的起点,给第一个小朋友 n + 1 颗糖果,第二个小朋友 n + 2 颗,依此类推,直到给最后一个小朋友 2 * n 颗糖果。

重复上述过程(每次都比上一次多给出一颗糖果,当到达队伍终点后再次从队伍起点开始),直到我们分完所有的糖果。注意,就算我们手中的剩下糖果数不够(不比前一次发出的糖果多),这些糖果也会全部发给当前的小朋友。

返回一个长度为 num_people、元素之和为 candies 的数组,以表示糖果的最终分发情况(即 ans[i] 表示第 i 个小朋友分到的糖果数)。

示例 1:

输入:candies = 7, num_people = 4
输出:[1,2,3,1]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0,0]。
第三次,ans[2] += 3,数组变为 [1,2,3,0]。
第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。

示例 2:

输入:candies = 10, num_people = 3
输出:[5,2,3]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0]。
第三次,ans[2] += 3,数组变为 [1,2,3]。
第四次,ans[0] += 4,最终数组变为 [5,2,3]。

说明:

  • 1 <= candies <= 10^9
  • 1 <= num_people <= 1000

思路

现在有一些糖果要分给一排小朋友,第一个小朋友分1个,第二个分2个,依次类推,即分配的糖果总比上一次分的多一个,如果不够就剩多少分多少。如果一轮下来没有分完,就接着从第一个小朋友开始依次分配。问最终每个小朋友能够分得的糖果数量。

我们可以很容易地模拟这个分配过程,记录当前分配的糖果数量,并计算剩余糖果数量直到0。

网友最快的题解使用了数学公式,使时间复杂度从O(num_people+sqrt(candies)) 降为O(num_people)。

代码

/**
 * @date 2024-06-03 0:06
 */
public class DistributeCandies1103 {

    public int[] distributeCandies_v1(int candies, int num_people) {
        int[] res = new int[num_people];
        // 足量发放人次,解不等式:(n+1)n/2 <= candies
        int n = (int) ((Math.sqrt(candies * 8F + 1) - 1) / 2);
        // 足量发放的糖果数量,num_people <= 1000,其实不会溢出
        int distributed = (int) ((1L + n) * n / 2);
        // 剩余糖数量
        int remainder = candies - distributed;
        // 所有小朋友都得到糖果的轮次
        int allGetLoops = n / num_people;
        // 最后一轮获得糖果的人数
        int lastLoopNum = n % num_people;
        for (int i = 0; i < num_people; i++) {
            // 收到糖果的次数 = 所有小朋友都得到糖果的轮次 + 最后一轮是否发放
            int times = (lastLoopNum > i ? 1 : 0) + allGetLoops;
            // 等差数列求和,公差为 num_people,a1 = i + 1,n = times;
            res[i] = ((times - 1) * num_people + i * 2 + 2) * times / 2;
        }
        // 如果是最后一个,将剩余的全部分配
        res[lastLoopNum] += remainder;
        return res;
    }

    public int[] distributeCandies(int candies, int num_people) {
        int[] res = new int[num_people];
        int cnt = 1;
        while (candies > 0) {
            for (int i = 0; i < num_people && candies > 0; i++) {
                int num = Math.min(cnt++, candies);
                candies -= num;
                res[i] += num;
            }
        }
        return res;
    }

}

性能

575.分糖果

目标

Alice 有 n 枚糖,其中第 i 枚糖的类型为 candyType[i] 。Alice 注意到她的体重正在增长,所以前去拜访了一位医生。

医生建议 Alice 要少摄入糖分,只吃掉她所有糖的 n / 2 即可(n 是一个偶数)。Alice 非常喜欢这些糖,她想要在遵循医生建议的情况下,尽可能吃到最多不同种类的糖。

给你一个长度为 n 的整数数组 candyType ,返回: Alice 在仅吃掉 n / 2 枚糖的情况下,可以吃到糖的 最多 种类数。

示例 1:

输入:candyType = [1,1,2,2,3,3]
输出:3
解释:Alice 只能吃 6 / 2 = 3 枚糖,由于只有 3 种糖,她可以每种吃一枚。

示例 2:

输入:candyType = [1,1,2,3]
输出:2
解释:Alice 只能吃 4 / 2 = 2 枚糖,不管她选择吃的种类是 [1,2]、[1,3] 还是 [2,3],她只能吃到两种不同类的糖。

示例 3:

输入:candyType = [6,6,6,6]
输出:1
解释:Alice 只能吃 4 / 2 = 2 枚糖,尽管她能吃 2 枚,但只能吃到 1 种糖。

说明:

  • n == candyType.length
  • 2 <= n <= 10^4
  • n 是一个偶数
  • -10^5 <= candyType[i] <= 10^5

思路

统计糖果种类,然后与n/2比较,取其中的最小值。

可以使用 HashSet 来计算种类数量,题解中最快的解法是自定义了hash函数,将种类映射到数组下标,通过判断相应位置上的值来计数。

代码

/**
 * @date 2024-06-02 9:26
 */
public class DistributeCandies575 {

    public int distributeCandies(int[] candyType) {
        int n = candyType.length;
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < n; i++) {
            set.add(candyType[i]);
        }
        return Math.min(set.size(), n / 2);
    }

    public int distributeCandies_v1(int[] candyType) {
        int res = 0;
        int n = candyType.length;
        byte[] mask = new byte[200001];
        for (int i = 0; i < n; i++) {
            int c = candyType[i];
            // 由于类型可能为负数,所以使用c+100000
            if (mask[c + 100000] == 0) {
                mask[c + 100000] = 1;
                res++;
                if (res > n / 2) return n / 2;
            }
        }
        return res;
    }
}

性能

2928.给小朋友们分糖果I

目标

给你两个正整数 n 和 limit 。

请你将 n 颗糖果分给 3 位小朋友,确保没有任何小朋友得到超过 limit 颗糖果,请你返回满足此条件下的 总方案数 。

示例 1:

输入:n = 5, limit = 2
输出:3
解释:总共有 3 种方法分配 5 颗糖果,且每位小朋友的糖果数不超过 2 :(1, 2, 2) ,(2, 1, 2) 和 (2, 2, 1) 。

示例 2:

输入:n = 3, limit = 3
输出:10
解释:总共有 10 种方法分配 3 颗糖果,且每位小朋友的糖果数不超过 3 :(0, 0, 3) ,(0, 1, 2) ,(0, 2, 1) ,(0, 3, 0) ,(1, 0, 2) ,(1, 1, 1) ,(1, 2, 0) ,(2, 0, 1) ,(2, 1, 0) 和 (3, 0, 0) 。

说明:

  • 1 <= n <= 50
  • 1 <= limit <= 50

思路

n 颗糖果分给 3 位小朋友,每个小朋友分到的糖果数量不超过 limit,求分配的方案数。注意,糖果必须分完,比如示例 1,不存在分得糖果数量为 0 的情况。

  • 第一个小朋友分到的糖果数为 a ∈ [0, Math.min(n, limit)]
  • 第二个小朋友分到的糖果数为 b ∈ [Math.max(0, n - a - limit), Math.min(n - a, limit)]
  • 第三个小朋友分到的糖果数为 n - a - b

代码

/**
 * @date 2024-06-01 15:59
 */
public class DistributeCandies2928 {
    public int distributeCandies(int n, int limit) {
        int res = 0;
        // 为第一个小朋友分配有0~limit种可能
        for (int i = 0; i <= limit; i++) {
            // 这时为第二个小朋友可以有n - i种可能,这里不要忘了不能超过limit
            for (int j = 0; j <= Math.min(limit, n - i); j++) {
                // 第三个小朋友,获取剩下的糖果,但不能超过limit
                if(n - i - j <= limit){
                    res++;
                }
            }
        }
        return res;
    }
}

性能

2965.找出缺失和重复的数字

目标

给你一个下标从 0 开始的二维整数矩阵 grid,大小为 n * n ,其中的值在 [1, n2] 范围内。除了 a 出现 两次,b 缺失 之外,每个整数都 恰好出现一次 。

任务是找出重复的数字a 和缺失的数字 b 。

返回一个下标从 0 开始、长度为 2 的整数数组 ans ,其中 ans[0] 等于 a ,ans[1] 等于 b 。

示例 1:

输入:grid = [[1,3],[2,2]]
输出:[2,4]
解释:数字 2 重复,数字 4 缺失,所以答案是 [2,4] 。

示例 2:

输入:grid = [[9,1,7],[8,9,2],[3,4,6]]
输出:[9,5]
解释:数字 9 重复,数字 5 缺失,所以答案是 [9,5] 。

说明:

  • 2 <= n == grid.length == grid[i].length <= 50
  • 1 <= grid[i][j] <= n * n
  • 对于所有满足 1 <= x <= n * n 的 x ,恰好存在一个 x 与矩阵中的任何成员都不相等。
  • 对于所有满足 1 <= x <= n * n 的 x ,恰好存在一个 x 与矩阵中的两个成员相等。
  • 除上述的两个之外,对于所有满足 1 <= x <= n * n 的 x ,都恰好存在一对 i, j 满足 0 <= i, j <= n - 1grid[i][j] == x

思路

记录元素的出现次数,选出其中出现次数为2和0的值即可。

网友还给出了一些位运算与数学公式求解的方法,可以降低空间复杂度,不用对每个元素计次,而是使用异或和与数学公式求解。

异或和求解的关键是如何区分得到的 a ^ b,由于a 与 b 不同,异或的结果至少有一个非0位,可以根据该bit位分组,这样每一组中就只含有a或b。

至于数学公式求解的关键是,通过各项和的差可以得到 b - a,各项平方和的差可以得到 b^2 - a^2,进而求得 b + a,解方程组可得a、b。

代码

/**
 * @date 2024-05-31 0:12
 */
public class FindMissingAndRepeatedValues2965 {

/**
     * 公式求和 1,2,……,n^2 以及 1^2,2^2,……,n^2
     * 然后遍历求和,二者之差为b - a 和 b^2 - a^2
     * 进而可以求得b + a,解方程可得a、b
     */
    public int[] findMissingAndRepeatedValues_v2(int[][] grid) {
        int n = grid.length;
        int n2 = n * n;
        long sum = 0;
        long sum2 = 0;
        for (int[] row : grid) {
            for (int i : row) {
                sum += i;
                sum2 += i * i;
            }
        }
        long bsuba = n2 * (n2 + 1L) / 2 - sum;
        long b2suba2 = n2 * (n2 + 1L) * (2 * n2 + 1) / 6 - sum2;
        long badda = b2suba2 / bsuba;
        long b = (badda + bsuba) / 2L;
        long a = badda - b;
        return new int[]{(int) a, (int) b};
    }

    /**
     * 异或和
     */
    public int[] findMissingAndRepeatedValues_v1(int[][] grid) {
        int n = grid.length;
        int n2 = n * n;
        int prefix = 0;
        for (int i = 1; i <= n2; i++) {
            prefix ^= i;
        }
        int tmp = 0;
        for (int[] row : grid) {
            for (int element : row) {
                tmp ^= element;
            }
        }
        int axorb = prefix ^ tmp;
        int shift = Integer.numberOfTrailingZeros(axorb);
        int[] res = new int[2];
        // 由于axorb至少有1bit是不同的,那么根据该bit分组,这样每一组里面a b是分开的
        for (int i = 1; i <= n2; i++) {
            res[i >> shift & 1] ^= i;
        }
        for (int[] row : grid) {
            for (int element : row) {
                res[element >> shift & 1] ^= element;
            }
        }
        // 由于顺序是不确定的,判断出现的为a
        for (int[] row : grid) {
            for (int element : row) {
                if (element == res[0]){
                    return res;
                }
            }
        }
        // 调换位置
        return new int[]{res[1], res[0]};
    }

    public int[] findMissingAndRepeatedValues(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int l = m * n + 1;
        int[] occurrence = new int[l];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                occurrence[grid[i][j]]++;
            }
        }
        int[] res = new int[2];
        for (int i = 1; i < l; i++) {
            if (occurrence[i] == 0) {
                res[1] = i;
            } else if (occurrence[i] == 2) {
                res[0] = i;
            }
        }
        return res;
    }
}

性能

2981.找出出现至少三次的最长特殊子字符串I

目标

给你一个仅由小写英文字母组成的字符串 s 。

如果一个字符串仅由单一字符组成,那么它被称为 特殊 字符串。例如,字符串 "abc" 不是特殊字符串,而字符串 "ddd"、"zz" 和 "f" 是特殊字符串。

返回在 s 中出现 至少三次 的 最长特殊子字符串 的长度,如果不存在出现至少三次的特殊子字符串,则返回 -1 。

子字符串 是字符串中的一个连续 非空 字符序列。

示例 1:

输入:s = "aaaa"
输出:2
解释:出现三次的最长特殊子字符串是 "aa" :子字符串 "aaaa"、"aaaa" 和 "aaaa"。
可以证明最大长度是 2 。

示例 2:

输入:s = "abcdef"
输出:-1
解释:不存在出现至少三次的特殊子字符串。因此返回 -1 。

示例 3:

输入:s = "abcaba"
输出:1
解释:出现三次的最长特殊子字符串是 "a" :子字符串 "abcaba"、"abcaba" 和 "abcaba"。
可以证明最大长度是 1 。

说明:

  • 3 <= s.length <= 50
  • s 仅由小写英文字母组成。

思路

这道题要我们求给定字符串中至少出现三次的由相同字符组成的子串的最大长度。下面分情况讨论:

  • 如果特殊子串是连续的,那么取最大子串长度-2。例如:aaaa 有以下符合条件的特殊子串 (aa)aaa(aa)aaa(aa),至少出现三次的最长特殊子字符串长度为2。
  • 如果特殊子串个数为2:
    • 如果这两个子串长度相同,取长度-1。例如:aaa aaa 有以下符合条件的特殊子串 (aa)a a(aa) (aa)a a(aa),出现了4次,最长特殊子串长度为2。
    • 如果这两个子串长度不同,取 max(last -2 , secondTolast)。例如:aa aaa aaaa 有以下符合条件的特殊子串 (aaa) (aaa)a a(aaa),出现了3次,最长特殊子串长度为3。
  • 如果特殊子串个数大于2,取max(last -2 , thirdTolast)。例如:aa aaa aaa 有以下符合条件的特殊子串 (aa) (aa)a a(aa) (aa)a a(aa),出现了5次,最长特殊子串长度为3。

代码

/**
 * @date 2024-05-29 8:42
 */
public class MaximumLength2981 {
    public int maximumLength(String s) {
        int res = -1;
        Map<Character, List<Integer>> map = new HashMap<>(26);
        for (int i = 'a'; i <= 'z'; i++) {
            map.put((char) i, new ArrayList<>());
        }
        int n = s.length();
        char last = s.charAt(0);
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c == last) {
                cnt++;
            } else {
                map.get(last).add(cnt);
                cnt = 1;
                last = c;
            }
            if (i == n - 1) {
                map.get(c).add(cnt);
            }
        }
        for (Map.Entry<Character, List<Integer>> entry : map.entrySet()) {
            List<Integer> occurrence = entry.getValue();
            int size = occurrence.size();
            Collections.sort(occurrence);
            if (size >= 2) {
                Integer secondToLastOccurrence = occurrence.get(size - 2);
                Integer lastOccurrence = occurrence.get(size - 1);
                if (lastOccurrence - secondToLastOccurrence >= 1) {
                    res = Math.max(res, Math.max(secondToLastOccurrence, lastOccurrence - 2));
                } else {
                    res = Math.max(res, lastOccurrence - 1);
                }
                if (size >= 3) {
                    res = Math.max(res, Math.max(occurrence.get(size - 3), occurrence.get(size - 1) - 2));
                }
            } else if (size == 1) {
                res = Math.max(res, occurrence.get(0) - 2);
            }
        }
        return res == 0 ? -1 : res;
    }
}

性能

// todo 性能优化

2951.找出峰值

目标

给你一个下标从 0 开始的数组 mountain 。你的任务是找出数组 mountain 中的所有 峰值。

以数组形式返回给定数组中 峰值 的下标,顺序不限 。

注意:

  • 峰值 是指一个严格大于其相邻元素的元素。
  • 数组的第一个和最后一个元素 不 是峰值。

示例 1:

输入:mountain = [2,4,4]
输出:[]
解释:mountain[0] 和 mountain[2] 不可能是峰值,因为它们是数组的第一个和最后一个元素。
mountain[1] 也不可能是峰值,因为它不严格大于 mountain[2] 。
因此,答案为 [] 。

示例 2:

输入:mountain = [1,4,3,8,5]
输出:[1,3]
解释:mountain[0] 和 mountain[4] 不可能是峰值,因为它们是数组的第一个和最后一个元素。
mountain[2] 也不可能是峰值,因为它不严格大于 mountain[3] 和 mountain[1] 。
但是 mountain[1] 和 mountain[3] 严格大于它们的相邻元素。
因此,答案是 [1,3] 。

说明:

  • 3 <= mountain.length <= 100
  • 1 <= mountain[i] <= 100

思路

找到大于左右两边元素值的下标。

代码

/**
 * @date 2024-05-28 8:39
 */
public class FindPeaks2951 {
    public List<Integer> findPeaks(int[] mountain) {
        List<Integer> res = new ArrayList<>();
        int n = mountain.length;
        int l = 0;
        int r = 2;
        while (r < n) {
            int i = l + 1;
            if (mountain[i] > mountain[l] && mountain[i] > mountain[r]) {
                res.add(i);
                l = r;
                r += 2;
            } else {
                l++;
                r++;
            }
        }
        return res;
    }
}

性能

2028.找出缺失的观测数据

目标

现有一份 n + m 次投掷单个 六面 骰子的观测数据,骰子的每个面从 1 到 6 编号。观测数据中缺失了 n 份,你手上只拿到剩余 m 次投掷的数据。幸好你有之前计算过的这 n + m 次投掷数据的 平均值 。

给你一个长度为 m 的整数数组 rolls ,其中 rolls[i] 是第 i 次观测的值。同时给你两个整数 mean 和 n 。

返回一个长度为 n 的数组,包含所有缺失的观测数据,且满足这 n + m 次投掷的 平均值 是 mean 。如果存在多组符合要求的答案,只需要返回其中任意一组即可。如果不存在答案,返回一个空数组。

k 个数字的 平均值 为这些数字求和后再除以 k 。

注意 mean 是一个整数,所以 n + m 次投掷的总和需要被 n + m 整除。

示例 1:

输入:rolls = [3,2,4,3], mean = 4, n = 2
输出:[6,6]
解释:所有 n + m 次投掷的平均值是 (3 + 2 + 4 + 3 + 6 + 6) / 6 = 4 。

示例 2:

输入:rolls = [1,5,6], mean = 3, n = 4
输出:[2,3,2,2]
解释:所有 n + m 次投掷的平均值是 (1 + 5 + 6 + 2 + 3 + 2 + 2) / 7 = 3 。

示例 3:

输入:rolls = [1,2,3,4], mean = 6, n = 4
输出:[]
解释:无论丢失的 4 次数据是什么,平均值都不可能是 6 。

示例 4:

输入:rolls = [1], mean = 3, n = 1
输出:[5]
解释:所有 n + m 次投掷的平均值是 (1 + 5) / 2 = 3 。

说明:

  • m == rolls.length
  • 1 <= n, m <= 10^5
  • 1 <= rolls[i], mean <= 6

思路

已知 m+n 个投骰子的观测数据的均值 mean,以及其中 m 个观察数据 rolls,返回缺失的观测数据,如果存在多个只返回其中一组,如果不存在答案返回空数组。

我们可以很容易计算出观测数据的总和 mean * (m + n),用它减去已知的观测数据和 sum,得到 diff

  • 如果 diff > n * 6 说明 剩余的 n 次都得到 6 点也不够,返回空数组。
  • 如果 diff < n 说明 剩余的 n 次都得到 1 点也多余,返回空数组。
  • 否则,问题变成选 n 个数字使其和等于 diff,每个数的取值范围是 1 ~ 6

这让我想起了背包问题还有之前做过的硬币找零,组合总和等问题。这里只需要返回一种可能就行了,不需要动态规划。

可以先计算 val = diff / n,如果有剩余 r,就为 r 个值加1。

代码

/**
 * @date 2024-05-27 9:20
 */
public class MissingRolls2028 {

    public int[] missingRolls(int[] rolls, int mean, int n) {
        int m = rolls.length;
        int sum = 0;
        for (int roll : rolls) {
            sum += roll;
        }
        int diff = mean * (m + n) - sum;
        if (diff > n * 6 || diff < n) {
            return new int[0];
        }
        int[] res = new int[n];
        int val = diff / n;
        diff = diff - val * n;
        Arrays.fill(res, val);
        for (int i = 0; i < diff; i++) {
            res[i]++;
        }
        return res;
    }
}

性能