377.组合总和 Ⅳ

目标

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。请注意,顺序不同的序列被视作不同的组合。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

示例 2:

输入:nums = [9], target = 3
输出:0

说明:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 1000
  • nums 中的所有元素 互不相同
  • 1 <= target <= 1000

进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?

思路

这个题要求满足条件的排列数,从给定数组中选择任意数字,同一数字可以重复选择,使所选序列的和等于target。

类似于70.爬楼梯322.零钱兑换

首先初始化给定数组的dp,然后遍历0~target,计算dp。

代码

/**
 * @date 2024-04-22 8:31
 */
public class CombinationSum377 {
    public int combinationSum4(int[] nums, int target) {
        Arrays.sort(nums);
        int[] dp = new int[target + 1];
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] <= target) {
                dp[nums[i]] = 1;
            }
        }
        for (int i = 0; i <= target; i++) {
            for (int num : nums) {
                if (i - num >= 0) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
}

性能

216.组合总和 III

目标

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

说明:

  • 2 <= k <= 9
  • 1 <= n <= 60

思路

从1~9中选k个,使它们的和是n。暴力求解需要 C9k 次遍历,可以使用回溯算法成批地考察具有特定前缀的所有候选解,一旦发现与目标解不合,需要撤销当前选择返回上一层进行下一个可能的尝试。dfs只是回溯算法的一环。关于回溯算法具体可参考《数据结构与算法分析》第314页。

代码

/**
 * @date 2024-04-21 20:44
 */
public class CombinationSum216 {

    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> res = new ArrayList<>();
        Deque<Integer> q = new ArrayDeque<>();
        for (int i = 1; i <= 9; i++) {
            dfs(k - 1, i, n, q, res);
        }
        return res;
    }

    public void dfs(int k, int root, int target, Deque<Integer> q, List<List<Integer>> res) {
        if (k < 0 || target < 0) {
            return;
        }
        if (target == root && k == 0) {
            q.offer(root);
            res.add(new ArrayList<>(q));
            q.pollLast();
            return;
        }
        q.offer(root);
        for (int i = root + 1; i <= 9; i++) {
            dfs(k - 1, i, target - root, q, res);
        }
        q.pollLast();
    }
}

性能

39.组合总和

目标

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

思路

一看到这道题就想到要用动态规划,但是昨天看了回溯算法的视频,所以就试图使用dfs去写。

先从target开始,循环减去可选数字,然后递归。想法是好的,但是这种集合嵌套集合的操作一会就把我搞晕了,向下传递什么,返回什么?有机会再想想吧。

还是用动态规划吧,难点在于去重。刚开始甚至写了hash函数,但是它不能处理2, 5(2 3)4(2 2), 3的情况,dp[2] + dp[5] 与 dp[4] + dp[3] 得到的组合是相同的 [2, 2, 3]

这让我想到了518.零钱兑换II,这两道题本质是一样的。那个只让返回组合数,这个需要返回具体的组合。

去重的精髓就在于不能提前初始化dp,只能在第一次访问到候选值的时候初始化。

代码

/**
 * @date 2024-04-20 10:20
 */
public class CombinationSum39 {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>>[] dp = new List[target + 1];
        for (int i = 0; i <= target; i++) {
            dp[i] = new ArrayList<>();
        }
        for (int candidate : candidates) {
            if (candidate <= target) {
                List<Integer> list = new ArrayList<>();
                list.add(candidate);
                dp[candidate].add(list);
            }
            for (int i = candidate; i <= target; i++) {
                for (List<Integer> lj : dp[i - candidate]) {
                    List<Integer> tmp = new ArrayList<>();
                    tmp.add(candidate);
                    tmp.addAll(lj);
                    dp[i].add(tmp);
                }
            }
        }
        return dp[target];
    }
}

性能

2007.从双倍数组中还原原数组

目标

一个整数数组 original 可以转变成一个 双倍 数组 changed ,转变方式为将 original 中每个元素 值乘以 2 加入数组中,然后将所有元素 随机打乱 。

给你一个数组 changed ,如果 change 是 双倍 数组,那么请你返回 original数组,否则请返回空数组。original 的元素可以以 任意 顺序返回。

示例 1:

输入:changed = [1,3,4,2,6,8]
输出:[1,3,4]
解释:一个可能的 original 数组为 [1,3,4] :
- 将 1 乘以 2 ,得到 1 * 2 = 2 。
- 将 3 乘以 2 ,得到 3 * 2 = 6 。
- 将 4 乘以 2 ,得到 4 * 2 = 8 。
其他可能的原数组方案为 [4,3,1] 或者 [3,1,4] 。

示例 2:

输入:changed = [6,3,0,1]
输出:[]
解释:changed 不是一个双倍数组。

示例 3:

输入:changed = [1]
输出:[]
解释:changed 不是一个双倍数组。

说明:

  • 1 <= changed.length <= 10^5
  • 0 <= changed[i] <= 10^5

思路

所谓双倍数组指的是由原数组以及其每个元素乘以2之后的元素合并在一起的数组。现在想要从双倍数组还原为原数组,如果不是合法的双倍数组则返回空数组。

首先,如果元素个数为奇数肯定不是双倍数组。注意到数组中的元素如果是奇数那么一定属于原数组。由于返回数组的顺序任意,那么先排序会更好处理。

接着就想到将奇数与偶数分开,然后看奇数*2是否有相应的偶数对应,或者可以将偶数元素/2看是否有奇数对应(不过这样就得处理0的情况,因为0只能与它自己匹配)。匹配完成后可能还剩下原数组中的偶数元素与其对应的双倍元素。

该如何处理呢?看了具体的例子很容易有一些想当然的想法,比如剩余[2,2,4,4]很容易将其平分为两部分,然后同时比较这两部分相应的位置是否满足二倍关系。这种假设是没有根据的,也是不对的,比如剩余[2、2、4、4、4、6、8、12]也是合法的。那我们该怎么比较呢?

这时突然有一个想法出现在大脑中,可以先将当前元素存到队列中,如果后面的元素不是其二倍就添加到队列中,如果是则将队列的第一个元素取出,这样遍历完之后看队列中是否还有数据即可。我们之所以可以这么做是因为前面排过序了,队首元素的二倍元素一定会最先出现。如果不排序的话,比如说[2,1],2先加入队列,后加入的1就无法匹配了。再比如[4,8,2,16],4与8匹配了,剩下的就匹配不了了。

有了这个想法之后,前面区分奇数、偶数,对0做特殊处理就都没有必要了。

网友还介绍了一种消消乐的方法,可以不排序。这个需要先统计各元素出现的次数,然后如果x % 2 == 0 && cnt.containsKey(x / 2)则跳过,即如果 x/2 在 cnt 中,则跳过,直到找到开始的那个x,然后一次性处理之前被跳过的2x、4x、6x...。这里其实也利用了顺序,只不过没有排序而是先找到不能再分的那个初始节点再向后倍增。

还有的使用大数组保存了 0~max 元素出现次数的数组cnt,然后遍历cnt,如果cnt[i]>02*i>max || cnt[2i]==0 直接返回空数组,否则cnt[2i]--这个方法也不用排序,是因为统计个数时变相地将数组元素映射为下标,遍历cnt数组是从小到大有序的。

代码

/**
 * @date 2024-04-18 8:48
 */
public class FindOriginalArray2007 {

    public int[] findOriginalArray(int[] changed) {
        int n = changed.length;
        if (n % 2 == 1) {
            return new int[0];
        }
        Arrays.sort(changed);
        int originLength = n / 2;
        int[] res = new int[originLength];
        Deque<Integer> collect = new ArrayDeque<>();
        int i = 0;
        for (int j = 0; j < changed.length; j++) {
            if (collect.size() == 0 || changed[j] % 2 == 1 || !collect.peek().equals(changed[j] / 2)) {
                collect.add(changed[j]);
            } else {
                res[i++] = collect.poll();
            }
        }
        if (collect.size() > 0) {
            return new int[0];
        }
        return res;
    }

}

性能

238.除自身以外数组的乘积

目标

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

示例 1:

输入: nums = [1,2,3,4]
输出: [24,12,8,6]

示例 2:

输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

说明:

  • 2 <= nums.length <= 10^5
  • -30 <= nums[i] <= 30
  • 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内

进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)

思路

最简单的想法是先计算所有元素的乘积,然后挨个除,但是题目要求不能用除法,略过。

后面又要求不要创建额外的空间,即最好只创建一个用于返回结果的数组。

考虑先保存数组元素的右/左侧的乘积,然后二次遍历计算左/右侧乘积,然后与之前保存的值相乘即可。

网友还有一种一次遍历的写法,初始化结果数组为1,同时从前计算左边乘积,从后计算右边乘积,但是每次循环执行了4次乘法,效率并不高。

代码

/**
 * @date 2024-04-07 11:35
 */
public class ProductExceptSelf238 {

    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] right = new int[n];
        right[n- 1] = 1;
        for (int i = n - 2; i >= 0; i--) {
            right[i] = nums[i + 1] * right[i + 1];
        }
        int left = 1;
        for (int i = 1; i < n; i++) {
            left *= nums[i - 1];
            right[i] *= left;
        }
        return right;
    }

    /**  一次遍历的版本 */
    public int[] productExceptSelf_v1(int[] nums) {
        int[] res = new int[nums.length];
        Arrays.fill(res, 1);
        int j = nums.length - 2;
        int left = 1, right = 1;
        for (int i = 1; i < nums.length; i++) {
            left *= nums[i - 1];
            right *= nums[j + 1];
            res[i] = left * res[i];
            res[j] = right * res[j];
            j--;
        }
        return res;
    }
}

性能

2924.找到冠军II

目标

一场比赛中共有 n 支队伍,按从 0 到 n - 1 编号。每支队伍也是 有向无环图(DAG, Directed Acyclic Graph) 上的一个节点。

给你一个整数 n 和一个下标从 0 开始、长度为 m 的二维整数数组 edges 表示这个有向无环图,其中 edges[i] = [ui, vi] 表示图中存在一条从 ui 队到 vi 队的有向边。

从 a 队到 b 队的有向边意味着 a 队比 b 队 强 ,也就是 b 队比 a 队 弱 。

在这场比赛中,如果不存在某支强于 a 队的队伍,则认为 a 队将会是 冠军 。

如果这场比赛存在 唯一 一个冠军,则返回将会成为冠军的队伍。否则,返回 -1 。

注意:

是形如 a1, a2, ..., an, an+1 的一个序列,且满足:节点 a1 与节点 an+1 是同一个节点;节点 a1, a2, ..., an 互不相同;对于范围 [1, n] 中的每个 i ,均存在一条从节点 ai 到节点 ai+1 的有向边。

有向无环图 是不存在任何环的有向图。

说明:

  • 1 <= n <= 100
  • m == edges.length
  • 0 <= m <= n * (n - 1) / 2
  • edges[i].length == 2
  • 0 <= edge[i][j] <= n - 1
  • edges[i][0] != edges[i][1]
  • 生成的输入满足:如果 a 队比 b 队强,就不存在 b 队比 a 队强
  • 生成的输入满足:如果 a 队比 b 队强,b 队比 c 队强,那么 a 队比 c 队强

思路

如果只有一个节点,那它就是冠军。入度非0的不是冠军。多个没有强弱关系的的节点,返回-1,例如,n=2,边为空。

只需计算没有被标记为weaker节点的index,如果多于1个返回-1。

代码

/**
 * @date 2024-04-13 19:22
 */
public class FindChampion2924 {
    public int findChampion(int n, int[][] edges) {
        boolean[] weaker = new boolean[n];
        for (int[] edge : edges) {
            weaker[edge[1]] = true;
        }
        int championIndex = -1;
        for (int i = 0; i < weaker.length; i++) {
            if (weaker[i]) {
                continue;
            }
            if (championIndex != -1) {
                return -1;
            }
            championIndex = i;
        }
        return championIndex;
    }
}

性能

1702.修改后的最大二进制字符串

目标

给你一个二进制字符串 binary ,它仅有 0 或者 1 组成。你可以使用下面的操作任意次对它进行修改:

  • 操作 1 :如果二进制串包含子字符串 "00" ,你可以用 "10" 将其替换。

    比方说, "00010" -> "10010"

  • 操作 2 :如果二进制串包含子字符串 "10" ,你可以用 "01" 将其替换。

    比方说, "00010" -> "00001"

请你返回执行上述操作任意次以后能得到的 最大二进制字符串 。如果二进制字符串 x 对应的十进制数字大于二进制字符串 y 对应的十进制数字,那么我们称二进制字符串 x 大于二进制字符串 y 。

示例 1:

输入:binary = "000110"
输出:"111011"
解释:一个可行的转换为:
"000110" -> "000101" 
"000101" -> "100101" 
"100101" -> "110101" 
"110101" -> "110011" 
"110011" -> "111011"

示例 2:

输入:binary = "01"
输出:"01"
解释:"01" 没办法进行任何转换。

说明:

  • 1 <= binary.length <= 10^5
  • binary 仅包含 '0' 和 '1'

思路

看到这道题我最先想到的是使用字符串替换,先把00的都替换为10,直到不能替换为止。然后再替换10为01,直到不能替换为止。然后再从头替换,相当于是一个while里面套两个while。

通过对具体例子的观察可以发现将10替换为01不是无条件的,我甚至还写了比较字符串大小的方法,如果字符串变小了就不变了。但其实是不行的,因为中间过程确实存在变小的情况。

最后经过观察分析发现必须要前面有0才可以替换,因为这样可以将高位的0置为1。以01110为例,最后能够转换为10111。

于是就想通过replace方法替换捕获组来实现,例如匹配 0(1*)0,替换为 10(匹配到的1),试了一下发现replacement不支持。Pattern 类也是无法使用的。

基于上面的分析,我们可以通过算法模拟出替换过程,这里面需要用到双指针 starti

  1. 如果 starti 相同且都指向1,那么直接跳过
  2. 如果 starti 相差1,且 i 指向0,即00的情况,那么将 start 指向置1,start++
  3. 否则,如果 starti 相差大于1,且 i 指向0,即0(1+)0的情况,那么需要将start 指向置1,start 后面的置0,i 指向的置1 即可

需要注意的是,如果 starti 不同,那么 start 指向的一定是0。其实步骤2与3可以合并,只需先将 i 置1,然后再将 start 后面的置0即可。

看了官网的题解,还提供了一种直接构建的算法。

如果字符串中有多个0,总可以将它们通过10->01将其前移至第一个0的位置,然后通过00->10,使高位的0变为1。最终的结果中至多包含1个0。

因此,直接构建的方法是:从第一个0开始,后面的0全置为1,然后将第一个0后移 0的个数减1 个位置。

代码

/**
 * @date 2024-04-10 0:53
 */
public class MaximumBinaryString1702 {

    /** 直接构造 */
    public String maximumBinaryString_v2(String binary) {
        char[] b = binary.toCharArray();
        int firstZero = binary.indexOf('0');
        if (firstZero == -1) {
            return binary;
        }
        int cnt = 0;
        for (int i = firstZero; i < b.length; i++) {
            cnt += '1' - b[i];
            b[i] = '1';
        }
        b[firstZero + cnt - 1] = '0';
        return new String(b);
    }

    public String maximumBinaryString_v1(String binary) {
        char[] b = binary.toCharArray();
        int start = 0;
        for (int i = 0; i < b.length; i++) {
            if (start == i && '1' == b[i]) {
                start++;
            } else if (start <= i - 1 && '0' == b[i]) {
                b[start++] = '1';
                b[i] = '1';
                b[start] = '0';
            }
        }
        return new String(b);
    }

    public String maximumBinaryString(String binary) {
        char[] b = binary.toCharArray();
        int start = 0;
        for (int i = 0; i < b.length; i++) {
            if (start == i && '1' == b[i]) {
                start++;
            } else if (start == i - 1 && '0' == b[i]) {
                b[start++] = '1';
            } else if (start < i - 1 && '0' == b[i]) {
                b[start++] = '1';
                b[start] = '0';
                b[i] = '1';
            }
        }
        return new String(b);
    }

}

性能

1600.王位继承顺序

目标

一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点,这个家庭里有人出生也有人死亡。

这个王国有一个明确规定的王位继承顺序,第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) ,给定一个人 x 和当前的继承顺序,该函数返回 x 的下一继承人。

Successor(x, curOrder):
    如果 x 没有孩子或者所有 x 的孩子都在 curOrder 中:
        如果 x 是国王,那么返回 null
        否则,返回 Successor(x 的父亲, curOrder)
    否则,返回 x 不在 curOrder 中最年长的孩子

比方说,假设王国由国王,他的孩子 Alice 和 Bob (Alice 比 Bob 年长)和 Alice 的孩子 Jack 组成。

  1. 一开始, curOrder 为 ["king"].
  2. 调用 Successor(king, curOrder) ,返回 Alice ,所以我们将 Alice 放入 curOrder 中,得到 ["king", "Alice"] 。
  3. 调用 Successor(Alice, curOrder) ,返回 Jack ,所以我们将 Jack 放入 curOrder 中,得到 ["king", "Alice", "Jack"] 。
  4. 调用 Successor(Jack, curOrder) ,返回 Bob ,所以我们将 Bob 放入 curOrder 中,得到 ["king", "Alice", "Jack", "Bob"] 。
  5. 调用 Successor(Bob, curOrder) ,返回 null 。最终得到继承顺序为 ["king", "Alice", "Jack", "Bob"] 。

通过以上的函数,我们总是能得到一个唯一的继承顺序。

请你实现 ThroneInheritance 类:

  • ThroneInheritance(string kingName) 初始化一个 ThroneInheritance 类的对象。国王的名字作为构造函数的参数传入。
  • void birth(string parentName, string childName) 表示 parentName 新拥有了一个名为 childName 的孩子。
  • void death(string name) 表示名为 name 的人死亡。一个人的死亡不会影响 Successor 函数,也不会影响当前的继承顺序。你可以只将这个人标记为死亡状态。
  • string[] getInheritanceOrder() 返回 除去 死亡人员的当前继承顺序列表。

示例:

输入:
["ThroneInheritance", "birth", "birth", "birth", "birth", "birth", "birth", "getInheritanceOrder", "death", "getInheritanceOrder"]
[["king"], ["king", "andy"], ["king", "bob"], ["king", "catherine"], ["andy", "matthew"], ["bob", "alex"], ["bob", "asha"], [null], ["bob"], [null]]
输出:
[null, null, null, null, null, null, null, ["king", "andy", "matthew", "bob", "alex", "asha", "catherine"], null, ["king", "andy", "matthew", "alex", "asha", "catherine"]]

解释:
ThroneInheritance t= new ThroneInheritance("king"); // 继承顺序:king
t.birth("king", "andy"); // 继承顺序:king > andy
t.birth("king", "bob"); // 继承顺序:king > andy > bob
t.birth("king", "catherine"); // 继承顺序:king > andy > bob > catherine
t.birth("andy", "matthew"); // 继承顺序:king > andy > matthew > bob > catherine
t.birth("bob", "alex"); // 继承顺序:king > andy > matthew > bob > alex > catherine
t.birth("bob", "asha"); // 继承顺序:king > andy > matthew > bob > alex > asha > catherine
t.getInheritanceOrder(); // 返回 ["king", "andy", "matthew", "bob", "alex", "asha", "catherine"]
t.death("bob"); // 继承顺序:king > andy > matthew > bob(已经去世)> alex > asha > catherine
t.getInheritanceOrder(); // 返回 ["king", "andy", "matthew", "alex", "asha", "catherine"]

说明:

  • 1 <= kingName.length, parentName.length, childName.length, name.length <= 15
  • kingName,parentName, childName 和 name 仅包含小写英文字母。
  • 所有的参数 childName 和 kingName 互不相同。
  • 所有 death 函数中的死亡名字 name 要么是国王,要么是已经出生了的人员名字。
  • 每次调用 birth(parentName, childName) 时,测试用例都保证 parentName 对应的人员是活着的。
  • 最多调用 10^5 次birth 和 death 。
  • 最多调用 10 次 getInheritanceOrder 。

思路

首先要弄清皇位继承顺序,国王孩子中最大的先继承,如果他也有后代同样按照长幼继承,然后才轮到国王的次子继承。刚开始以为是先在老一辈里面按顺序继承,然后才轮到孩子辈的。国王生的孩子直接按顺序加入,如果国王死了就将继承国王的后代加入继承序列。

继承顺序可以有两种处理方式,一个是在出生与死亡的时候维护,另一个是在调用继承顺序方法的时候根据现有的状态生成。

动态维护的算法不容易实现,死亡的时候需要从继承序列中删除,出生时需要在特定位置插入,在哪插?

根据状态生成更容易实现,每次都是从开国的国王开始,按照继承规则遍历即可。写的时候也没有意识到这其实就是多叉树的前序遍历。

代码

/**
 * @date 2024-04-07 8:42
 */
public class ThroneInheritance1600 {

    private final Map<String, List<String>> children;

    private final Set<String> dead;

    private final String originator;

    public ThroneInheritance1600(String kingName) {
        originator = kingName;
        children = new HashMap<>();
        dead = new HashSet<>();
    }

    public void birth(String parentName, String childName) {
        children.computeIfAbsent(parentName, k -> new ArrayList<>()).add(childName);
    }

    public void death(String name) {
        dead.add(name);
    }

    public List<String> getInheritanceOrder() {
        List<String> curOrder = new ArrayList<>();
        if (!dead.contains(originator)) {
            curOrder.add(originator);
        }
        successor(originator, curOrder);
        return curOrder;
    }

    public void successor(String name, List<String> curOrder) {
        if (children.get(name) != null && children.get(name).size() != 0) {
            for (String child : children.get(name)) {
                if (!dead.contains(child)) {
                    curOrder.add(child);
                }
                successor(child, curOrder);
            }
        }
    }

    public static void main(String[] args) {
        ThroneInheritance1600 main = new ThroneInheritance1600("king");
        System.out.println(main.getInheritanceOrder());
        main.birth("king", "logan");
        main.birth("logan", "hosea");
        main.birth("king", "leonard");
        main.death("king");
        main.birth("logan", "carl");
        main.death("hosea");
        main.birth("leonard", "ronda");
        main.birth("logan", "betty");
        System.out.println(main.getInheritanceOrder());
    }
}

性能

1026.节点与其祖先之间的最大差值

目标

给定二叉树的根节点 root,找出存在于 不同 节点 A 和 B 之间的最大值 V,其中 V = |A.val - B.val|,且 A 是 B 的祖先。

(如果 A 的任何子节点之一为 B,或者 A 的任何子节点是 B 的祖先,那么我们认为 A 是 B 的祖先)

说明:

  • 树中的节点数在 2 到 5000 之间。
  • 0 <= Node.val <= 10^5

思路

这道题还是挺直观的,求节点与其祖先之间的最大差值。直接深度优先遍历,记录路径上的最大与最小值,同时计算最大差值即可。

代码

/**
 * @date 2024-04-05 0:13
 */
public class MaxAncestorDiff1026 {

    int res = 0;

    public int maxAncestorDiff(TreeNode root) {
        dfs(root, root.val, root.val);
        return res;
    }

    public void dfs(TreeNode node, int max, int min) {
        if (node == null) {
            return;
        }
        max = Math.max(node.val, max);
        min = Math.min(node.val, min);
        res = Math.max(res, max - min);
        dfs(node.left, max, min);
        dfs(node.right, max, min);
    }
}

性能

2192.有向无环图中一个节点的所有祖先

目标

给你一个正整数 n ,它表示一个 有向无环图 中节点的数目,节点编号为 0 到 n - 1 (包括两者)。

给你一个二维整数数组 edges ,其中 edges[i] = [fromi, toi] 表示图中一条从 fromi 到 toi 的单向边。

请你返回一个数组 answer,其中 answer[i]是第 i 个节点的所有 祖先 ,这些祖先节点 升序 排序。

如果 u 通过一系列边,能够到达 v ,那么我们称节点 u 是节点 v 的 祖先 节点。

说明:

  • 1 <= n <= 1000
  • 0 <= edges.length <= min(2000, n * (n - 1) / 2)
  • edges[i].length == 2
  • 0 <= fromi, toi <= n - 1
  • fromi != toi
  • 图中不会有重边。
  • 图是 有向 且 无环 的。

思路

这个题要求所有节点的祖先节点集合,最直接的想法就是广度遍历然后记录父节点,然后下一个节点的祖先节点就是其父节点加上父节点的祖先节点。

需要注意的点是图不一定连通,所以选定一个起点不一定能遍历到所有节点。如果直接将所有节点加入队列容易超时。解决方法是先找到没有父节点的根节点,然后再广度遍历。

如果节点已经在队列中就不需要重复放入队列了,因为该节点的祖先集合可以由队列中的节点一起更新。

代码

/**
 * @date 2024-04-04 21:49
 */
public class GetAncestors2192 {

    /** 先找出没有parent的节点放入队列,然后广度优先遍历即可*/
    public List<List<Integer>> getAncestors(int n, int[][] edges) {
        List<Integer>[] g = new ArrayList[n];
        Set<Integer>[] dp = new TreeSet[n];
        List<List<Integer>> res = new ArrayList<>(n);
        Deque<Integer> q = new ArrayDeque<>();
        boolean[] hasParent = new boolean[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            dp[i] = new TreeSet<>();
        }
        for (int[] edge : edges) {
            g[edge[0]].add(edge[1]);
            hasParent[edge[1]] = true;
        }
        for (int i = 0; i < hasParent.length; i++) {
            if (!hasParent[i]) {
                q.offer(i);
            }
        }
        while (!q.isEmpty()) {
            Integer from = q.poll();
            for (int i = 0; i < g[from].size(); i++) {
                dp[g[from].get(i)].addAll(dp[from]);
                dp[g[from].get(i)].add(from);
                if (!q.contains(g[from].get(i))) {
                    q.offer(g[from].get(i));
                }
            }
        }
        for (Set<Integer> integers : dp) {
            res.add(new ArrayList<>(integers));
        }
        return res;
    }
}

性能

勉强过关,官网还介绍了拓扑排序的方法,有机会再更新吧。