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

性能

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

使用倍增的写法

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

性能

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

1379.找出克隆二叉树中的相同节点

目标

给你两棵二叉树,原始树 original 和克隆树 cloned,以及一个位于原始树 original 中的目标节点 target。

其中,克隆树 cloned 是原始树 original 的一个 副本 。

请找出在树 cloned 中,与 target 相同 的节点,并返回对该节点的引用(在 C/C++ 等有指针的语言中返回 节点指针,其他语言返回节点本身)。

注意:你 不能 对两棵二叉树,以及 target 节点进行更改。只能 返回对克隆树 cloned 中已有的节点的引用。

说明:

  • 树中节点的数量范围为 [1, 10^4] 。
  • 同一棵树中,没有值相同的节点。
  • target 节点是树 original 中的一个节点,并且不会是 null 。

进阶:如果树中允许出现值相同的节点,将如何解答?

思路

这道题挺简单的,这让我想起 100.相同的树 这道题,都是两棵树的同步遍历。

代码

/**
 * @date 2024-04-03 0:01
 */
public class GetTargetCopy1379 {
    public final TreeNode getTargetCopy(final TreeNode original, final TreeNode cloned, final TreeNode target) {
        if (original == null) {
            return null;
        } else if (target.equals(original)) {
            return cloned;
        } else {
            TreeNode res = getTargetCopy(original.left, cloned.left, target);
            if (res == null) {
                res = getTargetCopy(original.right, cloned.right, target);
            }
            return res;
        }
    }
}

性能

2810.故障键盘

目标

你的笔记本键盘存在故障,每当你在上面输入字符 'i' 时,它会反转你所写的字符串。而输入其他字符则可以正常工作。

给你一个下标从 0 开始的字符串 s ,请你用故障键盘依次输入每个字符。

返回最终笔记本屏幕上输出的字符串。

示例 1:

输入:s = "string"
输出:"rtsng"
解释:
输入第 1 个字符后,屏幕上的文本是:"s" 。
输入第 2 个字符后,屏幕上的文本是:"st" 。
输入第 3 个字符后,屏幕上的文本是:"str" 。
因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "rts" 。
输入第 5 个字符后,屏幕上的文本是:"rtsn" 。
输入第 6 个字符后,屏幕上的文本是: "rtsng" 。
因此,返回 "rtsng" 。

示例 2:

输入:s = "poiinter"
输出:"ponter"
解释:
输入第 1 个字符后,屏幕上的文本是:"p" 。
输入第 2 个字符后,屏幕上的文本是:"po" 。
因为第 3 个字符是 'i' ,屏幕上的文本被反转,变成 "op" 。
因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "po" 。
输入第 5 个字符后,屏幕上的文本是:"pon" 。
输入第 6 个字符后,屏幕上的文本是:"pont" 。
输入第 7 个字符后,屏幕上的文本是:"ponte" 。
输入第 8 个字符后,屏幕上的文本是:"ponter" 。
因此,返回 "ponter" 。

说明:

  • 1 <= s.length <= 100
  • s 由小写英文字母组成
  • s[0] != 'i'

思路

当输入i的时候,之前所有的输入都需要反转,i字符本身不显示。使用split无法处理连续为i以及结尾为i的情况。

最直接的做法就是模拟操作,但是反转字符串的复杂度较高。考虑使用双端队列,当反转时,相当于从队首加入字符,从队尾开始读取。

代码

/**
 * @date 2024-04-01 8:42
 */
public class FinalString2810 {
    public String finalString_v1(String s) {
        StringBuilder res = new StringBuilder();
        Deque<Character> q = new ArrayDeque<>();
        boolean reverse = false;
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if ('i' == ch) {
                reverse = !reverse;
            } else if (reverse) {
                q.push(ch);
            } else {
                q.offer(ch);
            }
        }
        Iterator<Character> it;
        if (reverse) {
            it = q.descendingIterator();
        } else {
            it = q.iterator();
        }
        while (it.hasNext()) {
            res.append(it.next());
        }
        return res.toString();
    }

    public String finalString(String s) {
        StringBuilder res = new StringBuilder();
        char[] chars = s.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if ('i' == chars[i]) {
                res.reverse();
            } else {
                res.append(chars[i]);
            }
        }
        return res.toString();
    }
}

性能

按道理来说应该是finalString_v1更快一些,res.reverse()时间复杂度是 O(n/2),会将元素左右互换。但是leetcode显示的用时分布反而finalString更快,耗时是finalString_v1的一半2ms,应该与数据规模有关吧,字符串长度最大才100。finalString_v1有更多的流程控制,还体现不出性能。