LeetCode Weekly Contest 27解题思路

LeetCode Weekly Contest 27解题思路

赛题

本次周赛主要分为以下4道题:

  • 557 Reverse Words in a String III (3分)
  • 554 Brick Wall (6分)
  • 556 Next Greater Element III (7分)
  • 549 Binary Tree Longest Consecutive Sequence II (9分)

557 Reverse Words in a String III

Problem:

Given a string, you need to reverse the order of characters in each word within a sentence while still preserving whitespace and initial word order.

Example 1:

Input: “Let’s take LeetCode contest”
Output: “s’teL ekat edoCteeL tsetnoc”

Note:

In the string, each word is separated by single space and there will not be any extra space in the string.

很简单的一道题,直接根据题目意思来写代码就行,不需要动什么脑子。

public String reverseWords(String s) {

        if (s.length() == 0) return s;

        String[] words = s.split(" ");

        StringBuilder ans = new StringBuilder();
        for (int i = 0; i < words.length; i++){
            StringBuilder tmp = new StringBuilder(words[i]);
            ans.append(tmp.reverse().toString()+" ");
        }

        return ans.toString().substring(0,ans.toString().length()-1);
    }

554 Brick Wall

Problems:

There is a brick wall in front of you. The wall is rectangular and has several rows of bricks. The bricks have the same height but different width. You want to draw a vertical line from the top to the bottom and cross the least bricks.

The brick wall is represented by a list of rows. Each row is a list of integers representing the width of each brick in this row from left to right.

If your line go through the edge of a brick, then the brick is not considered as crossed. You need to find out how to draw the line to cross the least bricks and return the number of crossed bricks.

You cannot draw a line just along one of the two vertical edges of the wall, in which case the line will obviously cross no bricks.

Example:

Input:
[[1,2,2,1],
[3,1,2],
[1,3,2],
[2,4],
[3,1,2],
[1,3,1,1]]
Output: 2

LeetCode Weekly Contest 27解题思路_第1张图片

Note:

  • The width sum of bricks in different rows are the same and won’t exceed INT_MAX.
  • The number of bricks in each row is in range [1,10,000]. The height of wall is in range [1,10,000]. Total number of bricks of the wall won’t exceed 20,000.

这道题的关键在于对问题的简化,首先就拿例子中画的那条线来看,第二行,第三行和最后两行都能通过缝隙,而第一行和第四行被切割开来了,好吧,其实就是个累加和。所以,我们自然而然的想法就是求每一行的累加和,得到如下的矩阵。

row\col 0 1 2 3
0 1 3 5 6
1 3 4 6
2 1 4 6
3 2 6
4 3 4 6
5 1 4 5 6

此时你再拿第一行的元素挨个和每一行的元素比较,如果相等,则说明可以被切分,答案自然而然就出来,所以也就有了我最初的代码。

    public int leastBricks(List> wall) {
        if (wall.size() == 0) return 0;
        if (wall.get(0).size() == 0) return 0;

        Set set = new HashSet<>();
        int row = wall.size();

        int sum = 0;
        for (int j = 0; j < wall.get(0).size(); j++){
            sum += wall.get(0).get(j);
        }

        Map> map = new HashMap<>();
        for (int i = 0; i < row; i++){
            int tmpSum = 0;
            List list = new ArrayList<>();
            for (int j =0; j < wall.get(i).size(); j++){
                tmpSum += wall.get(i).get(j);
                list.add(tmpSum);
                if(tmpSum == sum) continue;
                set.add(tmpSum);
            }
            map.put(i, list);
        }

        int min = row;
        for (int num : set){
            int count = 0;
            for (int i =0; i< row; i++){
                List list = map.get(i);

                int lf = 0, rt = list.size()-1;
                while (lf < rt){
                    int mid = lf + (rt -lf) / 2;
                    if (list.get(mid) < num){
                        lf = mid + 1;
                    }else{
                        rt = mid;
                    }
                }
                if (list.get(rt) != num){
                    count++;
                }
            }
            min = Math.min(min, count);
        }
        return min;
    }

分析下自己的代码吧,存在很多问题,我的想法一开始就麻烦了,因为我被人为解决该问题的思路所限制住了。前面已经说了,是对每一行的累加和元素进行比较,而这种切分可以有很多,如第一行可以有1,3,5的切分,第二行有3,4的切分,所以我把每行各种可能的切分全部存在一个hashSet的集合中,防止重复切分,所以代码前半部分都是在做这事,这事的时间复杂度为 O(n2) ,接下来,我就得想办法把每行所有的累加和记录下来,所以用一个map>的结构记录,它遍历求解的过程。

好了,有了每个待切分的元素,我们只需要遍历它,然后对每种切分循环遍历6行,统计是否有相同的元素,为了加快每行元素的查找,我使用了二分,使得内循环可以缩到 O(logn) ,但很遗憾,即时这样我还是超时了,而在真实的比赛环境中,我并没有找出更优的求解方案来。

那么问题到底出在哪里?

int min = row;
for (int 【num】 : set){
    int count = 0;
    for (int i =0; i< row; i++){
        List 【list】 = map.get(i);

        int lf = 0, rt = list.size()-1;
        while (lf < rt){
            int mid = lf + (rt -lf) / 2;
            if (list.get(mid) < num){
                lf = mid + 1;
            }else{
                rt = mid;
            }
        }
        if (list.get(rt) != num){
            count++;
        }
    }
    min = Math.min(min, count);
}
     return min;

如果不知道正确的优化解,或许我也无法总结出来,所以这里还是要多做,多看,多思考。我也不太喜欢直接给出答案,那么就失去了找寻优化点的奇迹,如果在不知道具体场景时,光靠看代码优化就成了一件不可能的事。

很简单,在上述代码中,我标出了【num】和【list】,你会发现,它们两的元素其实是一样的,无非一个是无重复元素,而一个是有重复元素,而我们在内循环中做了什么事,恰巧是在做一个对外层元素的计数工作!发现问题没,这就好比,我们要对每个相同元素计数时,我先循环一次,统计出所有非重复元素,然后针对每个不重复元素循环一次,遇到相同的元素,进行累加得到结果。

多么愚蠢的做法,也不知道为啥我尽能想出这种愚蠢的做法,很大一部分原因在于我被该问题的实际物理解法给限制住了,而没有融入计算机视角中来。但不管怎样,对我是一种教训,起码在对两个集合遍历时,重点关注集合中的元素是否有无包含关系,以及观察内循环的功能,或许能找到一些优化的地方。

回到这个问题的正确思路来,就上述问题,计算机怎么做,或者更聪明的做法是什么,空间换时间!计数的思想就是分类的思想,我们对同类元素的分类是借助空间来实现的,如我把所有1元素,放在一个位置上,把4元素放在令一个位置上,而此时只需要遍历一次数组,就能把它们放在各自的位置上。究其本质在于它的属性决定了空间摆放,所以当我们看到它时,一眼就能辨识出之前所在的位置,这种关联关系当然要好好利用。

所以,此时你再看看题目中所举的那个例子和所画的图,是不是有了一些新的认识,对于每一个划分,就是一个分类,能够划分的那些行,左侧整个整体符合一个性质(每行所有元素的累加和是相等的),而题目让我求的就是符合这一性质的有多少行(或者不符合该性质的有多少行)。

而空间换时间,根据元素值能一眼辨识位置的结构有哪个?没错,就是HashMap,所以有了如下优化代码:

public int leastBricks(List> wall) {
        if (wall.size() == 0) return 0;
        if (wall.get(0).size() == 0) return 0;

        Map map = new HashMap<>();

        for (int i =0; i < wall.size(); i++){
            int sum = 0;
            for (int j = 0; j < wall.get(i).size()-1;j++){
                sum += wall.get(i).get(j);
                map.put(sum,map.getOrDefault(sum, 0)+1);
            }
        }

        int min = wall.size();
        for (int count : map.values()){
            min = Math.min(min, wall.size()-count);
        }

        return min;
    }

一次循环就能解决分类计数问题,是吧,非常巧妙。聪明的娃都不需要上述我的分析,直接就能蹦出答案了。也不知道为啥自己想不到,真是蠢,唉,难道经验不足么!!!只能这么自我安慰了,哈哈哈。

556 Next Greater Element III

Problems:

Given a positive 32-bit integer n, you need to find the smallest 32-bit integer which has exactly the same digits existing in the integer n and is greater in value than n. If no such positive 32-bit integer exists, you need to return -1.

Example 1:

Input: 12
Output: 21

Example 2:

Input: 21
Output: -1

好吧,这道题更坑,我简直对自己无语,前一道题没找到优化的地方,心态爆炸,又开始没有理解题目的情况下,匆匆写答案,导致的结果就是考虑不全,得不到正解。

这是一道求nextPermutation的解,我直接说说后来我AC后的思路吧,拿一个数来举例,如12443322,如何求它的nextPermutation,很简单,我从后往前开始遍历每个元素,如果一直递增,那么我们没必要停下来,因为一定是当前最大的,我们找到第一个递减元素,如在上述例子中,第二位的2,就是我们要找的,找到后,我们和之前扫描过的元素中最小但大于等于2的元素,此处为3进行位置交换,交换完毕之后直接求后续元素的最小值即可。那么就有了13222344

public int nextGreaterElement(int n) {
        String num = Integer.toString(n);

        // 找到第一个递减的元素
        int index = -1;

        for (int i = num.length() - 1; i >= 1; i--) {
            if (num.charAt(i - 1) < num.charAt(i)) {
                index = i - 1; // 要置换的下标
                break;
            }

        }

        if (index == -1)
            return -1;

        int changeEle = num.charAt(index) - '0';
        //找寻待替换的元素
        int min = Integer.MAX_VALUE;
        int index2 = -1;
        for (int i = num.length() - 1; i >= index; i--) {
            if (num.charAt(i) - '0' > num.charAt(index) - '0') {
                min = Math.min(num.charAt(i) - '0', min);
                if (num.charAt(i) - '0' == min) {
                    index2 = i;
                }
            }
        }
        //index之后的元素加入优先队列(堆排序)
        PriorityQueue queue = new PriorityQueue<>();
        for (int i = index + 1; i < num.length(); i++) {
            // change
            if (index2 == i)
                queue.add(changeEle);
            else
                queue.add(num.charAt(i) - '0');
        }

        String ans = "";

        ans += num.substring(0, index);
        ans += min;

        //依次输出最小元素
        while (!queue.isEmpty()) {
            ans += queue.poll();
        }

        try {
            return Integer.parseInt(ans);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

这个代码我觉得写的不够好,在找递减元素时遍历了一次,在交换元素位置时也遍历了一次,而放入优先队列时有遍历了一次。原因就是在找第一个递减元素时,我并不知道到底是哪一个,导致我在后续交换元素时,也不清楚到底找谁和递减元素交换。这的确让我比较蛋疼,我看了一些比较好的答案,针对这个问题都没有新的思路,我也就暂且认为这部分无法优化了。

我以为优先队列对元素的排序已经是非常完美的了,时间复杂度为 O(n logn) ,但我又忽略了一个非常重要的潜在条件,在搜索第一个递减元素时,后续的所有元素是降序,所以只要把这部分元素进行reverse就好了!!!那么复杂度就是一个 O(n) 而已,呵呵。

public int nextGreaterElement(int n) {
        char[] num = Integer.toString(n).toCharArray();
        nextPermutation(num);
        try {
            int ans = Integer.parseInt(new String(num));
            if (ans <= n){
                return -1;
            }
            return ans;
        } catch (NumberFormatException e) {
            return -1;
        }
    }


    private void swap(char[] a, int i , int j){
        char tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }

    private void reverse(char[] a ,int start, int end){

        while(start < end){
            swap(a, start, end);
            start ++;
            end --;
        }

    }

    private char[] nextPermutation(char[] num){

        int i = num.length-2;
        while (i >= 0 && num[i] - num[i+1] >= 0){
            i--;
        }

        if (i == -1)
            return num;

        int j = num.length-1;
        while(j > i && num[j] <= num[i]){
            j--;
        }

        swap(num, i, j);
        reverse(num, i+1, num.length-1);

        return num;
    }

这个代码写的相当完美了,我喜欢nextPermutation中的while循环搜索ij,相比我之前的for循环简洁很多。

549 Binary Tree Longest Consecutive Sequence II

Problems:

Given a binary tree, you need to find the length of Longest Consecutive Path in Binary Tree.

Especially, this path can be either increasing or decreasing. For example, [1,2,3,4] and [4,3,2,1] are both considered valid, but the path [1,2,4,3] is not valid. On the other hand, the path can be in the child-Parent-child order, where not necessarily be parent-child order.

Example 1:

Input:
1
/ \
2 3
Output: 2
Explanation: The longest consecutive path is [1, 2] or [2, 1].

Example 2:

Input:
2
/ \
1 3
Output: 3
Explanation: The longest consecutive path is [1, 2, 3] or [3, 2, 1].

Note:

All the values of tree nodes are in the range of [-1e7, 1e7].

好吧,这题看上去很难求解,但它是可以慢慢通过递归构建出来的,遇到这样的题,我们只需要动态的理解它的过程就可以了,如给你[1,2],你难道还不能求么?上述两个例子给了你三个结点[1,2,3],同样的能够根据两个结点来求出。

像树的题目,我已经自然而然的把它分为根结点,左子树和右子树来看。这样,如果可以从左子树和右子树的解加上根结点能够推得更高一层的解,那么它就是能够递归求解的。刚开始做这道题时,我的一个递归思路是:


public int dfs(TreeNode root){

    //1.边界条件的判断

    //2. 开始构建左右子树+根的解
    ......
    写了一堆代码
    计算count
    ......
    dfs(root.left)
    dfs(root.right)

    return count;
}

这是我递归的主题思路,但我发现这种递归在实际操作时,会非常非常复杂,你要判断各种情况,导致最后你压根没法成功写出一个AC解。这是我遇到树问题的一种典型思路,不管三七二十一,自顶向下的构建解,但这个问题显然不适合自顶向下的思路,你找路径的时候是从中间root开始找的么?人为分析问题时,显然它是从某个结点开始向上搜索,然后再向下,这是一个典型的自底向上构建解的问题。

所以,一个好的解法是,我假设左子树的解和右子树的解已经得知,我根据这些信息再来归并根结点和左右子树,那么这种做法就是典型的后序遍历,也是树后序递归的核心思想,自底向上构建解。

好了,回到这个问题来,那么一切都变得理所当然和简单,但需要注意一些细节,如向上过程中,我们需要两个变量来记录降序和升序的情况。参考高手的AC解,代码如下:

public int longestConsecutive(TreeNode root) {
        if (root == null) return 0;
        dfs(root);
        return max;
    }

class Result{
        TreeNode node;
        int inc;
        int des;
    }

    int max;
    private Result dfs(TreeNode node) {
        if (node == null) return null;

        Result left = dfs(node.left);
        Result right = dfs(node.right);

        Result curr = new Result();
        curr.node = node;
        curr.inc = 1;
        curr.des = 1;

        if (left != null) {
            if (node.val - left.node.val == 1) {
                curr.inc = left.inc + 1;
            }
            else if (node.val - left.node.val == -1) {
                curr.des = left.des + 1;
            }
        }

        if (right != null) {
            if (node.val - right.node.val == 1) {
                curr.inc = Math.max(curr.inc, right.inc + 1);
            }
            else if (node.val - right.node.val == -1) {
                curr.des = Math.max(curr.des, right.des + 1);
            }
        }

        max = Math.max(max, curr.inc + curr.des - 1);

        return curr;
    }

简单分析一下,对我来说,理解起来还是比较有难度的。主要关注dfs()中的代码,首先,假设左右子树已经得到了正确的解,两句话解决问题Result left = dfs(node.left)Result right= dfs(node.right).所以接下来,我们只需要知道左子树有升序和降序两个答案,右子树有升序降序两个答案,根据root和左右子树第一个结点的关系来构建解。

看初始化

Result curr = new Result();
        curr.node = node;
        curr.inc = 1;
        curr.des = 1;

这段所代表的意思就是根结点这个单独的情况,不管左右子树存不存在,它自身应该算作一个解,所以有curr.inc = 1,curr.des = 1

看第一个判断语句:

if (left != null) {
            if (node.val - left.node.val == 1) {
                curr.inc = left.inc + 1;
            }
            else if (node.val - left.node.val == -1) {
                curr.des = left.des + 1;
            }
        }

这是构建的过程,什么时候根结点的inc和des相加?很明显,符合那个递增和递减的约束条件咯,所以当存在左子树的时候,符合约束情况,我把根结点的情况加上去就好了。接下来就应该加右子树的情况了,但这里就要注意了,怎么加?看代码:

if (right != null) {
            if (node.val - right.node.val == 1) {
                curr.inc = Math.max(curr.inc, right.inc + 1);
            }
            else if (node.val - right.node.val == -1) {
                curr.des = Math.max(curr.des, right.des + 1);
            }
        }

此处有很多细节,首先就是max里面比较的两个东东了,含义就是说,取左子树+根结点的值,也就是curr.inc,和右子树,也就是right.inc+1的值的最大值作为根结点的值,因为两边都是升序,所以取左右其中最大的那个就行,降序是一个道理。

那么,问题来了,如果遇到降序+升序怎么办呢?向上构建的时候,我们并没有把这种情况放入递归中,所以在递归函数最后有了这句话。

max = Math.max(max, curr.inc + curr.des - 1);

非常巧妙,可以有如下四种种情况:

  • 左子树升序存在,右子树降序存在,表达式就是curr.inc + curr.des -1,多加了根结点,减一去掉就好了。
  • 左子树升序不存在, 右子树降序存在,你会发现表达式还是同一个,因为升序不存在curr.inc = 1,只算了一个根结点,所以是一样的。
  • 左子树升序存在,右子树降序不存在,同上。
  • 左子树升序不存在,右子树升序不存在,同上。

最后,在自底向上构建的过程当中,用一个全局变量max记录下来,并随时比较就好了,简直完美。令我神奇的是,这结构实在太简单了,实在不能理解怎么能想到如此优美的答案来,两个if语句的判断加最后的max求解,经典中的经典。显然,我还没有参透递归中的精髓,日后再慢慢总结吧。

你可能感兴趣的:(算法竞赛,算法集中营)