2673.使二叉树所有路径值相等的最小代价

目标

给你一个整数 n 表示一棵 满二叉树 里面节点的数目,节点编号从 1 到 n 。根节点编号为 1 ,树中每个非叶子节点 i 都有两个孩子,分别是左孩子 2 i 和右孩子 2 i + 1 。

树中每个节点都有一个值,用下标从 0 开始、长度为 n 的整数数组 cost 表示,其中 cost[i] 是第 i + 1 个节点的值。每次操作,你可以将树中 任意 节点的值 增加 1 。你可以执行操作 任意 次。

你的目标是让根到每一个 叶子结点 的路径值相等。请你返回 最少 需要执行增加操作多少次。

注意:

  • 满二叉树 指的是一棵树,它满足树中除了叶子节点外每个节点都恰好有 2 个子节点,且所有叶子节点距离根节点距离相同。
  • 路径值 指的是路径上所有节点的值之和。

说明:

  • 3 <= n <= 105
  • n + 1 是 2 的幂
  • cost.length == n
  • 1 <= cost[i] <= 104

思路

操作首先要知道操作的最终状态:各路径相等。那么哪一个路径值可以使操作总数最小?既然明确不了哪一个路径又如何操作,又怎会知道最小?最开始以为可能是路径的中位数,但是我也没法证明。后来发现想复杂了,操作只能加不能减,那么问题就变成了将所有路径变成最大最少需要几次操作。

首先可以根据节点的父子关系将子路径值层层累加,那么最底下的叶子层就是各条路径的值。用最大路径值分别减去其它路径值得到相应的操作数,但这时操作数不是最小的。注意到,如果左右孩子所需操作均不为零,可以将较小孩子节点的操作数提到父节点上,并将操作数置零,而另一个孩子节点同时减去该操作数,这样就完成了一步最小化。依次向上计算,直到根节点,然后统计各节点操作数即可。

代码

/**
 * @date 2024-02-28 8:52
 */
public class MinIncrements {

    public int minIncrements(int n, int[] cost) {
        for (int i = 1; i < cost.length; i++) {
            if (i % 2 == 1) {
                cost[i] += cost[(i - 1) / 2];
            } else {
                cost[i] += cost[(i - 2) / 2];
            }
        }
        int max = 0;
        double l = Math.floor(Math.log(Double.parseDouble(String.valueOf(n))) / Math.log(2.0));
        for (int i = (int) Math.pow(2.0, l) - 1; i < cost.length; i++) {
            if (cost[i] > max) {
                max = cost[i];
            }
        }
        for (int i = (int) Math.pow(2.0, l) - 1; i < cost.length; i++) {
            cost[i] = max - cost[i];
        }
        for (int i = cost.length - 1; i > 2; i -= 2) {
            if (cost[i] == 0 || cost[i - 1] == 0) {
                cost[(i - 2) / 2] = 0;
                continue;
            }
            if (cost[i] <= cost[i - 1]) {
                cost[(i - 2) / 2] = cost[i];
                cost[i - 1] = cost[i - 1] - cost[i];
                cost[i] = 0;
            } else if (cost[i] >= cost[i - 1]) {
                cost[(i - 2) / 2] = cost[i - 1];
                cost[i] = cost[i] - cost[i - 1];
                cost[i - 1] = 0;
            }
        }
        int res = 0;
        for (int i = 1; i < cost.length; i++) {
            res += cost[i];
        }
        return res;
    }

    public static void main(String[] args) {

        MinIncrements main = new MinIncrements();
//        int[] cost = new int[]{1, 5, 2, 2, 3, 3, 1};
        int[] cost = new int[]{764, 1460, 2664, 764, 2725, 4556, 5305, 8829, 5064, 5929, 7660, 6321, 4830, 7055, 3761};
        System.out.println(main.minIncrements(cost.length, cost));
    }
}

性能

又是勉强过关。看看官网给的答案:

class Solution {
    public int minIncrements(int n, int[] cost) {
        int ans = 0;
        for (int i = n - 2; i > 0; i -= 2) {
            ans += Math.abs(cost[i] - cost[i + 1]);
            // 叶节点 i 和 i+1 的双亲节点下标为 i/2(整数除法)
            cost[i / 2] += Math.max(cost[i], cost[i + 1]);
        }
        return ans;
    }
}

称之为自底向上的贪心算法。所谓贪心算法,它在每一步都做出当时看起来最佳的选择,也就是说,它总是做出局部最优的选择。贪心算法并不保证得到最优解,但对很多问题确实可以求得最优解。