last == i背后的直觉?

时间:2019-04-04 21:00:06

标签: java algorithm

我正在解决关于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)并不满意。有人可以指出正在做什么吗?

谢谢!

2 个答案:

答案 0 :(得分:3)

因此,要了解此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,我们可以说{{1}之间的字符}和start是一个组,将其添加到列表中,并将last的值设置为start的索引。这是为下一组设置正确的开始位置。

现在,如果我们移至下一个索引,即索引9,我们将拥有一个从未见过的新字符。我们只是简单地重新开始该过程,就好像我们在位置1一样。索引9是last+1,它的最后一个索引为14。由于我们不在索引14上,所以我们继续下一个。在索引10处,我们有'd'。该索引的最后一个索引为15,大于'e'的索引14,因此我们取15,因为它更大。这基本上意味着,如果'd''d'在一个组中,则至少必须一直到索引15,才能封装两个字符。然后,它遍历其余所有文件,更新'e',直到结束,然后将其作为一个组切断。

希望这会有所帮助!

答案 1 :(得分:0)

简而言之,解决方案如下:从字符串的开头开始,让我们迭代地查找满足语句条件的最短子字符串(即,对于该子字符串中的每个字母,所有出现的字母都属于到此子字符串)。这是所谓贪婪算法的一种应用,它是正确的,相对直观:我们采用的子字符串越短,就会有越多的子字符串,并且我们已经确定要采用人类可能的最短子字符串。您要查询的特定行将执行以下操作:

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

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

让我们看一下该方法如何工作的示例。让我们尝试对字符串abacdbzz进行分区。我们构造了类似于map的辅助数组{2, 5, 3, 4, 0, ..., 0, 7}

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,并用其最右边出现的索引last更新map['z'-'a']=7

i = 7:找到第二个z,并且满足last == i(对于字符串中的最后一个字符,这将始终为true),因此我们将此字符串添加到分区中。

结束。