1483.树节点的第k个祖先[倍增写法]

今天将1483-树节点的第k个祖先留的那个倍增代码写了一下。

有以下几点需要借鉴的:

  1. 向上取整(log2(x))的计算方法 ceil(log2(x)) = 32 - Integer.numberOfLeadingZeros(x - 1)
  2. 判断第i位是否为1的方式,我原先是判断k%2是否为1,将k减半,循环。其实可以使用位运算((k >> i) & 1) == 1 i从0自增,将k右移i位与1相与,得到i位上的值

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;
    }
}

性能

2923.找到冠军I

目标

一场比赛中共有 n 支队伍,按从 0 到 n - 1 编号。

给你一个下标从 0 开始、大小为 n * n 的二维布尔矩阵 grid 。对于满足 0 <= i, j <= n - 1i != j 的所有 i, j :如果 grid[i][j] == 1,那么 i 队比 j 队 强 ;否则,j 队比 i 队 强 。

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

返回这场比赛中将会成为冠军的队伍。

示例 1:

输入:grid = [[0,1],[0,0]]
输出:0
解释:比赛中有两支队伍。
grid[0][1] == 1 表示 0 队比 1 队强。所以 0 队是冠军。

示例 2:

输入:grid = [[0,0,1],[1,0,1],[0,0,0]]
输出:1
解释:比赛中有三支队伍。
grid[1][0] == 1 表示 1 队比 0 队强。
grid[1][2] == 1 表示 1 队比 2 队强。
所以 1 队是冠军。

说明:

  • n == grid.length
  • n == grid[i].length
  • 2 <= n <= 100
  • grid[i][j] 的值为 0 或 1
  • 对于所有 i, grid[i][i] 等于 0.
  • 对于满足 i != j 的所有 i, j ,grid[i][j] != grid[j][i] 均成立
  • 生成的输入满足:如果 a 队比 b 队强,b 队比 c 队强,那么 a 队比 c 队强

思路

行和为n-1或者列全为0的为冠军。

代码

/**
 * @date 2024-04-12 0:13
 */
public class FindChampion2923 {
    public int findChampion(int[][] grid) {
        int n = grid.length;
        for (int i = 0; i < n; i++) {
            int sum = 0;
            for (int j = 0; j < n; j++) {
                sum += grid[i][j];
            }
            if (sum == n - 1) {
                return i;
            }
        }
        return -1;
    }
}

性能

1766.互质树

目标

给你一个 n 个节点的树(也就是一个无环连通无向图),节点编号从 0 到 n - 1 ,且恰好有 n - 1 条边,每个节点有一个值。树的 根节点 为 0 号点。

给你一个整数数组 nums 和一个二维数组 edges 来表示这棵树。nums[i] 表示第 i 个点的值,edges[j] = [uj, vj] 表示节点 uj 和节点 vj 在树中有一条边。

当 gcd(x, y) == 1 ,我们称两个数 x 和 y 是 互质的 ,其中 gcd(x, y) 是 x 和 y 的 最大公约数 。

从节点 i 到 根 最短路径上的点都是节点 i 的祖先节点。一个节点 不是 它自己的祖先节点。

请你返回一个大小为 n 的数组 ans ,其中 ans[i]是离节点 i 最近的祖先节点且满足 nums[i] 和 nums[ans[i]] 是 互质的 ,如果不存在这样的祖先节点,ans[i] 为 -1 。

说明:

  • nums.length == n
  • 1 <= nums[i] <= 50
  • 1 <= n <= 10^5
  • edges.length == n - 1
  • edges[j].length == 2
  • 0 <= uj, vj < n
  • uj != vj

思路

今天这道题超时了,看了答案才发现节点值不超过50。没有注意到这个点,答案是先计算1到50内每个数字互质的数字列表。然后在dfs的时候记录节点值的最大深度,以及最近的编号。

我是直接记录了parent数组,一步一步向上找,在第35/37个案例超时了,这棵树是单链,并且除了根节点,向上找都不互质,只能从叶子找到根。

这样在递归中套递归直接堆栈溢出了。后来又将这两个递归分开,不溢出了,但还是超时。

后来又试图利用已求得的结果,记录了value -> 最近互质父节点编号的映射,错误地认为如果值相等就可以直接返回这个编号。其实是不对的,因为这二者之间的父节点也可能与当前节点互质。

其实我想到了应该维护一个去重的父节点序列,但是今天没时间了,只能去看答案了。预处理这个点没有想到,记录值的最大深度与最近编号这个也不好想,也许时间充裕可能会想到吧。

好多经过深度思考得到的复杂的算法,时间久了就会忘记许多细节。没必要非得自己想出来,有这时间多看看算法书进步的更快吧。

代码

// todo

性能

// todo

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);
    }

}

性能

2529.正整数和负整数的最大计数

目标

给你一个按 非递减顺序 排列的数组 nums ,返回正整数数目和负整数数目中的最大值。

  • 换句话讲,如果 nums 中正整数的数目是 pos ,而负整数的数目是 neg ,返回 pos 和 neg二者中的最大值。

注意:0 既不是正整数也不是负整数。

示例 1:

输入:nums = [-2,-1,-1,1,2,3]
输出:3
解释:共有 3 个正整数和 3 个负整数。计数得到的最大值是 3 。

示例 2:

输入:nums = [-3,-2,-1,0,0,1,2]
输出:3
解释:共有 2 个正整数和 3 个负整数。计数得到的最大值是 3 。

示例 3:

输入:nums = [5,20,66,1314]
输出:4
解释:共有 4 个正整数和 0 个负整数。计数得到的最大值是 4 。

说明:

  • 1 <= nums.length <= 2000
  • -2000 <= nums[i] <= 2000
  • nums 按 非递减顺序 排列。

进阶:你可以设计并实现时间复杂度为 O(log(n)) 的算法解决此问题吗?

思路

这个题简单做法就是循环计数,O(n)的时间复杂度。O(log(n))需要使用二分查找。Arrays.binarySearch() 是处理不了非严格递增的情况的,如果查找的key有多个,无法保证返回的是哪一个,通常就是中间的那一个。

这里的难点是弄清楚当有多个相同值的时候如何找到第一个。具体来说就是 low 与 high 的更新以及结束条件,自己可以用一个具体的例子来模拟查找的过程。

  • 结束条件是 low == high
  • 如果寻找下界,那么 nums[middle] >= key 更新 high = middlenums[middle] < key 更新 low = middle + 1。返回第一个大于等于key的index。
  • 如果寻找上界,那么 nums[middle] > key 更新 high = middle - 1nums[middle] <= key 更新 low = middle 寻找上界的话只需将等号去掉即可,得到的是第一个小于等于key的index+1。

为什么要 +1 或者 -1? 因为middle指的位置不等于key,不是我们要找的值,不应该再出现在下一次的查找范围内,这是能说的通的。其实最核心的目的是保证最终low、high指向同一个位置,防止出现low与high相差1,但是middle指向的位置也无法触发更新的情况。以[-3,0,0,3,4]为例,我们要找0的下界,如果low的更新不 +1,那么最终就是 low,middle 指向 -3,high 指向第一个0,这不是我们想要的。

代码


/**
 * @date 2024-04-09 1:33
 */
public class MaximumCount2529 {

    public int maximumCount(int[] nums) {
        int pos = 0;
        int neg = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < 0) {
                neg++;
            } else if (nums[i] > 0) {
                pos = nums.length - i;
                break;
            }
        }
        return Math.max(pos, neg);
    }

    /**
     * 二分查找
     */
    public int maximumCount_v2(int[] nums) {
        int neg = bs(nums, 0);
        int pos = bs(nums, 1);
        return Math.max(neg, nums.length - pos);
    }

    public int bs(int[] nums, int key) {
        int low = 0, high = nums.length;
        while (low != high) {
            int middle = (low + high) >> 1;
            if (nums[middle] >= key) {
                high = middle;
            } else {
                low = middle + 1;
            }
        }
        return low;
    }
}

性能

2009.使数组连续的最少操作数

目标

给你一个整数数组 nums 。每一次操作中,你可以将 nums 中 任意 一个元素替换成 任意 整数。

如果 nums 满足以下条件,那么它是 连续的 :

  • nums 中所有元素都是 互不相同 的。
  • nums 中 最大 元素与 最小 元素的差等于 nums.length - 1 。

比方说,nums = [4, 2, 5, 3] 是 连续的 ,但是 nums = [1, 2, 3, 5, 6] 不是连续的 。

请你返回使 nums 连续 的 最少 操作次数。

示例 1:

输入:nums = [4,2,5,3]
输出:0
解释:nums 已经是连续的了。

示例 2:

输入:nums = [1,2,3,5,6]
输出:1
解释:一个可能的解是将最后一个元素变为 4 。
结果数组为 [1,2,3,5,4] ,是连续数组。

示例 3:

输入:nums = [1,10,100,1000]
输出:3
解释:一个可能的解是:
- 将第二个元素变为 2 。
- 将第三个元素变为 3 。
- 将第四个元素变为 4 。
结果数组为 [1,2,3,4] ,是连续数组。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9

思路

这道题让我们求将一个数组变为连续数组所需的最小操作,所谓连续数组指数组中元素各不相同,且max - min = nums.length - 1,也就是说如果将数组从小到大排序,会得到一个公差为1的数列。

我们首先将数组排序,然后初始的数组中会有不连续的边界,也会有相等的元素。如何处理才能使数列连续,并且保证操作数最小。

我陷入了这个困境,尝试了好几个小时,试图处理各种特殊情况。比如我会先找到最大的连续子序列并且记录边界的差值,然后以它为中心分别从后面、前面获取元素来填充这个边界。并且还要计数相同元素来填充边界,这里填充的时机也会对最小的操作有影响。有时需要先填充,有时则需要最后。总之,并没有一个统一的,明确的算法来满足所有情况。

看了官网的解答,应该使用滑动窗口思想。之前一直有看到这个题型分类,没想到这就是所谓的滑动窗口。

当我遇到困难的时候会有一种执念,不愿意去看题解,想知道沿着自己的思路下去为什么不行,到最后真的有点沮丧。这样我想起了迪杰斯特拉的一个采访。

An interview with Edsger W. Dijkstra

The computer science luminary, in one of his last interviews before his death in 2002, reflects on a programmer’s life.

There’s a curious story behind your “shortest path” algorithm.

In 1956 I did two important things, I got my degree and we had the festive opening of the ARMAC(Automatic Calculator Mathematical Centre, 自动计算器数学中心).

We had to have a demonstration. Now the ARRA(Automatic Relay Calculator Amsterdam, 自动继电器计算器 阿姆斯特丹), a few years earlier, had been so unreliable that the only safe demonstration we dared to give was the generation of random numbers, but for the more reliable ARMAC I could try something more ambitious. For a demonstration for noncomputing people you have to have a problem statement that non-mathematicians can understand; they even have to understand the answer. So I designed a program that would find the shortest route between two cities in the Netherlands, using a somewhat reduced roadmap of the Netherlands, on which I had selected 64 cities (so that in the coding six bits would suffice to identify a city).

What’s the shortest way to travel from Rotterdam to Groningen? It is the algorithm for the shortest path, which I designed in about 20 minutes. One morning I was shopping in Amsterdam with my young fiancée, and tired, we sat down on the café terrace to drink a cup of coffee and I was just thinking about whether I could do this, and I then designed the algorithm for the shortest path. As I said, it was a 20-minute invention. In fact, it was published in 1959, three years later. The publication is still quite nice. One of the reasons that it is so nice was that I designed it without pencil and paper. Without pencil and paper you are almost forced to avoid all avoidable complexities.

Eventually that algorithm became, to my great amazement, one of the cornerstones of my fame. I found it in the early 1960s in a German book on management science—“Das Dijkstra’sche Verfahren” [“Dijkstra’s procedure”].

Suddenly, there was a method named after me. And it jumped again recently because it is extensively used in all travel planners. If, these days, you want to go from here to there and you have a car with a GPS and a screen, it can give you the shortest way.

1956年,迪杰斯特拉与未婚妻在阿姆斯特丹逛商场累了,坐在咖啡厅的露台上喝咖啡,他想到了一个问题"从鹿特丹到格罗宁根最短的旅行路线是什么?",并且在20分钟内构思了最短路径算法。该算法在三年后被发表在一篇三页的论文中,题为 A note on two problems in connexion with graphs。Dijkstra 因其对开发结构化编程语言的基本贡献而获得 1972 年图灵奖,但最短路径算法仍然是他最著名的工作。

During an interview in 2001, Edsger Wybe Dijkstra revealed that he designed the algorithm in just 20 minutes while shopping in Amsterdam with his fiancée in 1956.

He was inspired by the question, "What's the shortest way to travel from Rotterdam to Groningen?"

He designed it without pencil and paper. He even mentioned that the advantage of designing without pencil and paper is that you are almost forced to avoid all avoidable complexities.

The algorithm was published three years later in a three-page article titled "A note on two problems in connexion with graphs".

https://twitter.com/LinuxHandbook/status/1754392486657798198

也许这就是天赋吧。我只是个普通人,不可能解决所有问题,没有什么可沮丧的。要努力站在巨人的肩膀上,知识并不是免费的,需要投入时间。所以没有必要死磕一个问题,快速高效的爬上去,然后当面对新的未知的问题时,再花费精力研究才对。

思考的意义可能就是让自己对困难、问题有了更深的体会,看解答要弄明白问题是如何解决的。

就以这个题为例,困难点在于如何使操作最小。虽然知道长度是固定的,但是并没有意识到如何遍历所有可能的操作方法并取其最小。陷入了具体的、面向过程的、针对案例而设计的算法之中。当我先入为主的认为,应该以最大连续子数组为中心不变,从两端借数填充这个错误的指导思想进行实现时,就注定得不到正确答案。

之所以开始会认为应该保持最大的连续子数组不变,是因为观察了几个测试案例,想当然的认为,既然最后要变成连续数组,那么保持最大的不变,操作数会最小,但其实并非如此。并且,先从后面填还是前面也是没有道理的,都是根据具体的测试案例写的。

也许其它时间,灵光一闪也会想到滑动窗口算法也说不定。如果之前接触过这个概念,那这个题还是挺简单的。

代码

// todo: 过几天自己实现一遍

性能

// todo

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());
    }
}

性能

1483.树节点的第K个祖先

目标

给你一棵树,树上有 n 个节点,按从 0 到 n-1 编号。树以父节点数组的形式给出,其中 parent[i] 是节点 i 的父节点。树的根节点是编号为 0 的节点。

树节点的第 k 个祖先节点是从该节点到根节点路径上的第 k 个节点。

实现 TreeAncestor 类:

  • TreeAncestor(int n, int[] parent) 对树和父数组中的节点数初始化对象。
  • getKthAncestor(int node, int k) 返回节点 node 的第 k 个祖先节点。如果不存在这样的祖先节点,返回 -1 。

说明:

  • 1 <= k <= n <= 5 * 10^4
  • parent[0] == -1 表示编号为 0 的节点是根节点。
  • 对于所有的 0 < i < n ,0 <= parent[i] < n 总成立
  • 0 <= node < n
  • 至多查询 5 * 10^4 次

思路

这个题让我们维护一个数据结构,来查找树中任意节点的第k个祖先节点。直接的想法是保存每一个节点的父节点,需要的时候直接根据下标获取。刚开始用的 int[][] 超出了空间限制,后来改成 List<Integer>[] 虽然多通过了几个测试用例,但是后面会超时。仔细分析最坏的情况下(所有节点仅有一个子树的情况),需要添加 n(n+1)/2 个父节点(首项为1,公差为1的等差数列求和),时间复杂度是O(n^2)。

一个解决办法是不要保存重复的父节点,以只有一个子树的情况举例,最后一个节点第k个祖先,就是其父节点的第k-1个祖先。如果这个节点已经保存有祖先节点的信息,就无需重复计算了。

所以我的解决方案就是使用缓存,如果父节点的祖先信息没有保存,就将当前节点的祖先信息写入缓存,直到遇到存在缓存的祖先节点,如果它记录的祖先节点个数大于k - cnt就直接返回,否则继续向该缓存的祖先节点集合添加,直到遇到下一个有缓存的节点或者cnt == k

这种方法虽然能够通过,但是与测试用例的的顺序是有关的,如果是从子节点逐步向前测试的话,缓存一直不命中,时间复杂度还是O(n^2)。

官方的解法使用的是倍增的思想,好像还挺常用的,算是个模板算法。核心思想是保存当前节点的父节点,爷爷节点,爷爷的爷爷节点......,即每个节点 x 的第 2^i 个祖先节点。这样不论k取什么值,都可以分解为不同的2的幂之和,然后向前查找即可。预处理的时间复杂度是O(nlogn),查询的时间复杂度是O(logk)。

代码

/**
 * @date 2024-04-06 9:45
 */
public class TreeAncestor1483 {

    /**倍增的写法 */
    public static class TreeAncestor_v4 {

        int[][] dp;

        public TreeAncestor_v4(int n, int[] parent) {
            dp = new int[16][];
            dp[0] = parent;
            for (int i = 1; i < 16; i++) {
                dp[i] = new int[n];
                Arrays.fill(dp[i], -1);
            }

            for (int i = 1; i < 16; i++) {
                for (int j = 0; j < n; j++) {
                    if (dp[i - 1][j] != -1) {
                        dp[i][j] = dp[i - 1][dp[i - 1][j]];
                    }
                }
            }
        }

        public int getKthAncestor(int node, int k) {
            int p = node;
            int b = 0;
            int mod;
            while (k != 0) {
                mod = k & 1;
                if (mod == 1) {
                    p = dp[b][p];
                    if (p == -1) {
                        return -1;
                    }
                }
                k = k >> 1;
                b++;
            }
            return p;
        }
    }

    int[] parent;
    List<Integer>[] cache;

    public TreeAncestor1483(int n, int[] parent) {
        this.parent = parent;
        cache = new ArrayList[n];
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new ArrayList<>();
        }
    }

    public int getKthAncestor(int node, int k) {
        if (node == -1) {
            return -1;
        }
        int cnt = 0;
        int p = node;
        while (cnt != k && p != -1) {
            if (cache[p].size() == 0) {
                cache[node].add(parent[p]);
                p = parent[p];
                cnt++;
            } else {
                if (cache[p].size() >= k - cnt) {
                    return cache[p].get(k - cnt - 1);
                } else {
                    cnt += cache[p].size();
                    node = p;
                    p = cache[p].get(cache[p].size() - 1);
                }
            }

        }
        return p;
    }
}

性能

这里是使用缓存写法的耗时,官方题解的耗时差不多也是这个样。

使用倍增的写法