【问题标题】:Intuition behind last==i?last==i 背后的直觉?
【发布时间】:2019-04-04 21:00:06
【问题描述】:

我正在解决LeetCode.com上的一个问题:

给出了一个由小写字母组成的字符串 S。我们希望将这个字符串分成尽可能多的部分,以便每个字母最多出现在一个部分中,并返回一个表示这些部分大小的整数列表。

示例:
输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8]
说明:
分区是“ababcbaca”、“defegde”、“hijhklij”。 这是一个分区,因此每个字母最多出现在一个部分中。 像“ababcbacadefegde”、“hijhklij”这样的分区是不正确的,因为它将 S 分成更少的部分。

highly upvoted solutions之一如下:

public List<Integer> partitionLabels(String S) {
        if(S == null || S.length() == 0){
            return null;
        }

        List<Integer> list = new ArrayList<>();
        int[] map = new int[26];  // record the last index of the each char

        for(int i = 0; i < S.length(); i++){
            map[S.charAt(i)-'a'] = i;
        }
        
        // record the end index of the current sub string
        int last = 0;
        int start = 0;
        for(int i = 0; i < S.length(); i++){
            last = Math.max(last, map[S.charAt(i)-'a']);
            if(last == i){
                list.add(last - start + 1);
                start = last + 1;
            }
        }
        return list;
    }
}

虽然我确实了解解决方案,但我对声明 last = Math.max(last, map[S.charAt(i)-'a']); 和子句 if(last == i) 不太满意。这里到底在做什么?

【问题讨论】:

    标签: java algorithm


    【解决方案1】:

    所以,要了解这个for 循环到底在做什么,您必须了解map 的设置方式。它使用这个循环来填充它:

    for(int i = 0; i < S.length(); i++){
        map[S.charAt(i)-'a'] = i;
    }
    

    现在,这也花了我一秒钟,但请耐心等待。它正在做的是循环遍历S 的每个字符。很简单。现在,它正在获取将i 放入数组的索引:S.charAt(i)-'a'。这是一些非常聪明的编程。它正在做的是让角色处于当前位置。例如,如果我们位于字符串中的索引1,则S.charAt(i) 将是'b'。然后我们从中减去'a',它将它们转换成它们的UTF-16字符代码并相互减去它们。这会将它们放置在数组中的位置1。然后它将该索引设置为等于i。所以在字符串的索引 1 处,数组的元素 1 等于 1。有点混乱,但让我们继续。如果我们在字符串的索引 5 处,我们将最后一次出现 'b'。由于'b'-'a' 仍为1,它将覆盖索引1 处的数组,但由于i 已更改,因此那里的值也已更改。由于这是最后一个索引,我们可以知道数组中每个字符的最后一个索引。

    现在我们已经解决了数组填充问题,让我们来回答您的实际问题。在下一个循环中,它像第一次一样遍历数组,但这次它知道所有字符的最后一个索引。因此,当我们运行语句last = Math.max(last, map[S.charAt(i)-'a']); 时,它所做的是使用与前面描述的相同的方法从数组中获取当前字符的最后一个索引。然后将其与当前的last 值进行比较。为什么这是特别的,因为该值是持久的。所以,它得到最后一个索引'a',它得到最后一个索引'b',并得到两者中较大的一个。这实际上是将它们放在各自的部分中。因此,现在我们将 last 作为当前部分的最后一个索引,我们可以将其与实际的当前索引进行比较。如果它们相等,我们就在该部分的末尾,并且可以将其添加到列表中。

    我希望这一切都能从那时起,不要犹豫,提出任何问题!

    编辑: 让我们看一个例子。假设我们有字符串:ababcbacadefegdehijhklij。如果我们运行填充map 数组的第一个循环,它将如下所示:

    +----------------+---+---+---+----+----+----+----+----+----+----+----+----+
    | Index          | 0 | 1 | 2 | 3  | 4  | 5  | 6  | 7  | 8  | 9  | 10 | 11 |
    +----------------+---+---+---+----+----+----+----+----+----+----+----+----+
    | Value          | 8 | 5 | 7 | 14 | 15 | 11 | 13 | 19 | 22 | 23 | 20 | 21 |
    +----------------+---+---+---+----+----+----+----+----+----+----+----+----+
    | Character      | a | b | c | d  | e  | f  | g  | h  | i  | j  | k  | l  |
    | representation |   |   |   |    |    |    |    |    |    |    |    |    |
    +----------------+---+---+---+----+----+----+----+----+----+----+----+----+
    

    (注意:字符表示仅供参考哪些索引是哪些)

    当我们开始第二个 for 循环时,我们得到字符串中的字符为 0,即'a'。然后我们检查map,看看它的最后一个索引在哪里,即8。由于当前索引是0而不是8,所以我们转到下一个。下一个字符'b'map 中的值为5。我们得到last 的前一个值(8 和5)之间的最大值,以获得这两个字符组合的最后一个索引。

    让我们跳到位置 8。此时我们已经看到了 'a''b''c'。他们所有最后一个索引中最大的是'a',为8。由于我们在位置8,last的值为8,我们可以说startlast之间的字符是一个组,将其添加到列表中,并将start 的值设置为last+1 的索引。这是为下一组设置正确的起始位置。

    现在,如果我们移动到下一个索引,索引 9,我们就有了一个我们以前从未见过的新角色。我们只是重新开始这个过程,就好像我们在位置 1。索引 9 是 'd',它的最后一个索引是 14。由于我们不在索引 14,我们继续下一个。在索引 10 处,我们有 'e'。这个的最后一个索引是 15,比'd' 的 14 大,所以我们取 15,因为它更大。这基本上意味着如果'd''e' 在一个组中,它至少必须一直到索引15,以便封装它们的所有字符。然后它遍历其余部分,更新last,直到它到达最后,将其作为一个组切断。

    希望这会有所帮助!

    【讨论】:

    • 您能否详细说明“这实际上是把它们放在他们的部分中”?计算lasts 中的较高者如何将它们放入它们的部分?更好的是,什么首先创建了一个部分?
    • @J.Doe 我刚刚更新了答案,以提供更多信息并举例说明。
    • 我明白你的意思。感谢您的解释。感谢您的帮助!
    • 嗯,实际上,charAt 在字符串中的某个位置返回 UTF-16 代码单元。 (而且,没有必要将 map 限制为 26 个元素。Character.MAX_VALUE + 1 会更简单。)
    • Java 字符确实是 UTF-16 编码的。那是我的错,但有一个非常具体的原因 map 应该是 26 个元素。由于您从'a' 中减去特定字母的整数值,因此您得到的值相对于 0x0 而不是 0x60。这意味着 a-z 的范围仅为 0-26。
    【解决方案2】:

    通俗地说,解决方法如下:从字符串的开头开始,让我们迭代地不断寻找满足语句条件的最短子字符串(即对于这个子字符串中的每个字母,它的所有出现都属于到这个子字符串)。这是所谓的贪心算法的一个应用,它为什么是正确的相对直观:我们采用的子字符串越短,它们的数量就越多,而且我们已经确定我们正在采用人类可能的最短子字符串。您质疑的特定行执行以下操作:

    last = Math.max(last, map[S.charAt(i)-'a']) - 我们正在计算当前子字符串中的所有字符中,哪个字符尽可能出现在右侧

    if(last == i){ - 我们正在检查在当前子字符串中的所有字符中,出现在尽可能右边的那个是我们当前正在查看的那个 - 这意味着子字符串可以右结束这里

    让我们看一个例子来说明这种方法是如何工作的。让我们尝试对字符串abacdbzz 进行分区。我们构造了这个看起来像{2, 5, 3, 4, 0, ..., 0, 7} 的辅助数组map

    i=0:我们从字符串的左边开始看字符a。如果字符串中没有更多的a,我们可以在这里完成我们的第一个子字符串并从下一个字符开始一个新的子字符串。不幸的是,字符串中有更多的a,我们通过将last 更新为map['a'-'a']=2 来实现。

    i=1:我们正在查看b 并意识到b 比我们当前的last 更远,即在ab 之间,最后一个b 出现在更右侧。我们通过将last 更新为map['b'-'a']=5 来反映这一观察结果。

    i=2:我们再次遇到a,它是字符串中的最后一个a,但我们不能在这里结束我们的子字符串,因为我们知道我们的子字符串中有一些东西(b)仍然在它之外有事件。

    i=3, 4:我们遇到了 cd,它们不会改变我们的状态,因为它们没有任何其他事件。 last 仍然是 5。

    i=5:我们终于找到了最后一个b。至此,条件last == i满足;换句话说,我们的子字符串(abcd)中没有出现从左到右的字母。这是我们可以采用的最短子串,我们很自豪地将它添加到分区中,并在下一个位置开始新的子串。

    i=6:我们遇到 z 并使用其最右侧出现的索引 map['z'-'a']=7 更新 last

    i=7:找到第二个z,满足last == i(对于字符串中的最后一个字符,总是这样),所以我们将这个字符串添加到分区中。

    结束。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-11
      • 2021-04-21
      • 1970-01-01
      • 2018-07-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多