leetCode进阶算法题+解析(三十五)

今天周五,明天就放假了,开心心~~哈哈,刷题之前打个广告:java技术交流群:130031711,欢迎各位大佬萌新踊跃加入。然后开始刷题了。

区域和检索-数组可修改

题目:给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。update(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。

示例:
Given nums = [1, 3, 5]
sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
说明:
数组仅可以在 update 函数下进行修改。
你可以假设 update 函数与 sumRange 函数的调用次数是均匀分布的。

思路:首先这个题目是个开放题目,就是实现很简单,最无脑的方式也可以实现。其次这种题评论很不友好,优越感贼鸡儿高,所以我肯定是不打算看评论了。最后直接每次修改或者累加不太好。所以我还是得看看怎么优化一下。
算了,想不出怎么优化了,要么更加麻烦要么费大量空间,我觉得放弃了,简单粗暴的实现试试看吧。

class NumArray {
    
    int[] nums;
    public NumArray(int[] nums) {
        this.nums = nums;
    }
    
    public void update(int i, int val) {
        nums[i] = val;
    }
    
    public int sumRange(int i, int j) {
        int sum = 0;
        for(int n = i;n<=j;n++){
            sum += nums[n];
        }
        return sum;
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(i,val);
 * int param_2 = obj.sumRange(i,j);
 */

性能不好是意料之中,解决办法我直接看性能排行第一的代码了,知识量不足,想不出什么好思路了。
看了题解,但是没看懂,,我可能是没救了。
直接附上两个性能排行考前的题解,然后我先过了:

class NumArray {

    private int[] tree;

    private int n;

    public NumArray(int[] nums) {
       n = nums.length;
       tree = new int[ 2 * n];
       for(int i = 0; i < n; i++) {
           tree[i + n] = nums[i];
       }
       for(int i = n - 1; i > 0; i--) {
           tree[i] = tree[ 2 * i] + tree[2 * i + 1];
       }
    }
    
    public void update(int i, int val) {
        i += n;
        int add = val - tree[i];
        while( i > 0) {
            tree[i] += add;
            i = i >> 1;
        } 
    }
    
    public int sumRange(int i, int j) {
        int l = i + n;
        int r = j + n;
        int sum = 0;
        while(l <= r) {
           if(l % 2 == 1){
               sum += tree[l];
               l++;
           }
           if(r % 2 == 0) {
               sum += tree[r];
               r--;
           }
           l = l >> 1;
           r = r >> 1;
        }
        return sum;
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(i,val);
 * int param_2 = obj.sumRange(i,j);
 */
class NumArray {

    private int[] nums;

    private int[] c;

    private int n;

    public NumArray(int[] nums) {
       this.nums = nums;
       n = nums.length + 1;
       c = new int[n];
       for(int i = 0; i < nums.length; i++) {
           updateAdd(i, nums[i]);
       }
    }
    
    public void update(int i, int val) {
        int add = val - nums[i];
        nums[i] = val;
        updateAdd(i, add);
    }

    private void updateAdd(int i, int val) {
        i++;
        while(i < n ) {
            c[i] += val;
            i += lowbit(i);
        }
    }

    private int sum(int i) {
        i++;
        int res = 0;
        while(i > 0) {
            res += c[i];
            i -= lowbit(i);
        }
        return res;
    }
    
    public int sumRange(int i, int j) {
        return sum(j) - sum(i - 1);
    }

    private int lowbit(int x) {
        return x & (-x);
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(i,val);
 * int param_2 = obj.sumRange(i,j);
 */

最佳买卖股票时机包含冷冻期

题目:给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

思路:典型dp没说的,我记得最佳买卖股票本身就是一个典型动态规划的题目,好像还是我dp入门题目。这个只不过加个冷冻期但是大体的逻辑的不变的。我直接去写代码了。特么越写越不对,今天状态不好,还是来分析分析题目吧。首先第一点买入:一定是当前值小于下一个值的情况下才可能买。不然怎么也不对。所以买入的时机是可以按条件判断的。最后说卖出,也一定是当前值大于前一个值(小不小于后面的不一定)。所以以这两个点选择买入卖出。然后再用一个合适的dp算出最大值。好的呢,就这样了。我再去试试。
好了,代码出来了(上周五开始做,中间经历了吵架,辞职等好多事,这周一才开始写完代码,也是比较波折的一道题了。)

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length<2) return 0;
        int[][] dp = new int[prices.length][2];
        //第一个数赚钱数是不买不卖,0元收益
        //第二个数是当前值买入
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        //第二天钱如果大于第一天钱数则赚钱是1买2卖,否则就要不买不卖
        dp[1][0] = prices[0]<prices[1]?prices[1]-prices[0]:0;
        //昨天买入和今天买入哪个更合适选择哪个。(因为买入是花钱,所以是负数)
        dp[1][1] = prices[0]<prices[1]?-prices[0]:-prices[1];
        for(int i =2;i<prices.length;i++){
            //主要是判断现在卖出是不是比昨天挣得多,是则今天卖出,不是则昨天收益是最多的
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            //因为中间有冷冻期,所以要i-2。前天最大盈利-今天买入 和 昨天最适合持有的股,哪个钱多选择哪个。
            dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0]-prices[i]);
        }
        return dp[prices.length-1][0];
    }
}

其实我觉得这个题已经脱离了简单dp的范围,因为真正做的时候发现还是比较复杂的,因为分为三种情况:一种是买,一种是卖,还有一种是冷冻期,所以一个简单数组记录中间过程是实现不了了。中间我甚至都有过脱离dp,用笨办法实现了,其实这个买入和卖出是有规律的,比如当前值小于后一个值才会买入,不然是没必要买的,可以下一个再买。但是当前值小于后一个值卖不卖是要看冻结期统一规划的,所以我这里将整个思路划分成了两部分:
1. 当前天的最大收益。
2. 当前天最合适的买入股。

如上面代码:

  • 第一天没啥疑问,不可能赚钱,所以dp[0][0]是0.dp[0][1]是-prices[0],也就是没有课比较的,所以当天最合适的买入股就是今天的股。
  • 第二天则开始判断:如果第二天钱数大于第一天的,那么就第一天买第二天卖,赚的钱就是差值。 而第二天手里应该有的股不考虑盈利的情况下,肯定是买便宜的那个(最开始会是负值,但是 -小值 是大的那个)。比如 1,0,3。这个的合理做法就是第一天不买不卖,第二天买, 第三天卖,这样才是最大值3.
    所以如果是这个例子,第二天手里持股应该是0(买便宜的那个)。

因为是典型dp,前两天的情况列出来了,后面的是重叠子问题了,所以用循环填充数据:
每一个循环都是一个套路:

  • 判断今天的最大利润是:昨天的最大利润 和 今天股票脱手赚的钱的哪个多,哪个多今天的最大利润就是哪个。
  • 判断今天手中股应该是哪个: 昨天的手中最合适的股 和 前天的盈利(因为中间冷冻期1天,所以前天操作完昨天是冷冻期,今天才能买入,虽说前天的盈利不一定是卖了股,但是保险起见肯定是要做预留)+今天买入的钱哪个多,哪个多选择哪个。

大概逻辑是这样,但是我写出来之前是一直调试的,真的这样的二维dp我也第一次做。然后一点点改动,但是真的写出来了回看,思路才真正清晰起来了。
差不多可以看成把当天最大盈利和 手中持股分开判断情况。 而手中持股卖出是有一天冷冻期,所以要昨天的股 比较 前天的钱数-今天买入的钱数。
然后dp问题其实调试也是一个好的解决办法,但是要慢慢的一步一步走,毕竟本来代码就少,而且可能数据多的时候不一定哪一步出问题,反正我觉得调试真的是个考验耐心的活。
这道题就这样了,我上面代码的性能也不错,超过百分之九十九了,直接下一题了。

最小高度树

题目:对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树。给出这样的一个图,写出一个函数找到所有的最小高度树并返回他们的根节点。格式:该图包含 n 个节点,标记为 0 到 n - 1。给定数字 n 和一个无向边 edges 列表(每一个边都是一对标签)。你可以假设没有重复的边会出现在 edges 中。由于所有的边都是无向边, [0, 1]和 [1, 0] 是相同的,因此不会同时出现在 edges 里。

示例 1:
输入: n = 4, edges = [[1, 0], [1, 2], [1, 3]]

    0
    |
    1
   / \
  2   3 

输出: [1]
示例 2:
输入: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]

 0  1  2
  \ | /
    3
    |
    4
    |
    5 

输出: [3, 4]
说明:
根据树的定义,树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
树的高度是指根节点和叶子节点之间最长向下路径上边的数量。

思路:emmmm....亲自做了demo才明白了题目的意思。比如第一个图最小高度从1开始的话都是1,所以答案是1.第二个demo中,从3开始最小高度是2,(3-4-5),从4开始最小高度也是2(4-3-0,4-3-2,4-3-1)。所以答案是3,4。现在为止是题目明白了,具体怎么实现我觉得其实方式应该也不少,但是我能想到的也就是什么链表啊,map啊什么的。主要思路是从每一个点出发记录最长的高度。然后选出其中高度最小的们,加入到结果集返回。我去写代码试试。
在不断看题的基础上又有一些心得。首先,只出现一次的元素说明是一个边界元素,除了只有两个元素外,剩下只出现一次的元素是不可能成为最小高度的根节点的(我是观察并分析得出的结论,还没正式确定)。其次我现在用最笨的map方法存了每一个节点和其相连节点,但是现在又觉得不是那么有必要。因为上面第二个例子中,0,1,2到3的距离是一样的1,所以我觉得是不不是可以只取一个参与计算就行?另外因为不存在环,所以这个节点数最少一个,最多两个对吧?不可能出现三个最小高度是一样的。都是随着写随着出现的零散的思路,我再去好好整理一下用代码实现。
思路理好了,我觉得现在的问题是怎么把出现过一次的叶子节点抛出去。勉强做好了,性能可怜,而且我超时了好几次,真的是,心态都崩了:

class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> result = new ArrayList<>();
        if(n<1) {
            return result;
        }else if(n==1){
            result.add(0);
            return result;
        }else if(n==2){
            result.add(0);
            result.add(1);
            return result;
        }
        int[] edge = new int[n];
        //构建每个结点的邻接表
        HashMap<Integer,List<Integer>> adj = new HashMap<>();
        for(int[] link : edges) {
            edge[link[0]]++;
            edge[link[1]]++;
            if(!adj.containsKey(link[0])) {
                adj.put(link[0],new ArrayList<>());
            }
            if(!adj.containsKey(link[1])) {
                adj.put(link[1],new ArrayList<>());
            }
            List<Integer> temp = adj.get(link[0]);
            temp.add(link[1]);
            adj.put(link[0],temp);
            temp = adj.get(link[1]);
            temp.add(link[0]);
            adj.put(link[1],temp);
        }
        //将每个边的数量为1的结点加入队列(叶子节点,不可能是根节点)
        Queue<Integer> queue = new LinkedList<>();
        for(int i=0;i<n;i++){
            if(edge[i]==1) {
                queue.add(i);
            }
        }
        int count = 0;
        while(!queue.isEmpty()) {
            int size = queue.size();
            //终止条件!当已入队的节点数量和剩余的队列中的节点数量和为n的时候,就是找到了root
            if(count+size==n) {
                for(int i=0;i<size;i++){
                    result.add(queue.poll());
                }
                return result;
            }
            count+=size;
            //正常执行
            for(int i=0;i<size;i++){
                int head = queue.poll();
                //将队中节点对应的邻接结点的边数量减一
                for(int item:adj.get(head)) {
                    edge[item]-=1;
                    //发现边数量为1的节点将其入队
                    if(edge[item]==1) {
                        queue.add(item);
                    }
                }
            }
        }
        return result;
    }
}

这个题的思路很简单,但是实现起来很麻烦。我上面的思路是对的,叶子节点不可能是根节点,所以不断排除叶子节点就行了。而问题也是在第一轮排除后怎么排除第二层叶子节点,我之前试过重新用二维数组表示,但是超时了。也试过叶子节点改值-1.还是超时了。最后还是用了map。我甚至都不报希望了,还好的就是没超时。不过也给自己写恶心了。
当然性能也不咋地。我去看看题解吧,是不是有什么更优雅的实现方式。
好了,回来了,确实是看到一个不错的算法:获取两个最长路径的节点,然后取中间节点(1,2个)作为根节点。
乍一看有点不理解,但是仔细想了就觉得这个思路挺好。因为两个路径最长的节点的中间节点使得节点到每个边的距离最小。没啥问题。然后实现代码我直接去cv吧。今天写的心态有点炸:

class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> res = new ArrayList<>();
        int[][] gra = new int[n][];
        for(int[] edge : edges) {
            int a = edge[0], b = edge[1];
            if(gra[a] == null) gra[a] = edge;
            else gra[b] = edge;
        }
        int root = getRoot(gra);
        int[] node = getNode(gra, root);
        root = reverse(gra, root, node[0]);
        node = getNode(gra, root);
        
        //System.out.println(root + "/" + node[0] + ":" + node[1]);
        
        int len = node[1] / 2;
        int p = node[0];
        while(len-- != 0) p = getNext(gra, p); 
        res.add(p);
        if((node[1] & 1) == 1) res.add(getNext(gra, p));
        
        return res;
    }
    
    private int reverse(int[][] gra, int root, int p) {
        int ret = p;
        int[] pre = null;
        while(p != root) {
            int next = getNext(gra, p);
            int[] temp = gra[p];
            gra[p] = pre;
            pre = temp;
            p = next;
        }
        gra[root] = pre;
        return ret;
    }
    
    private int[] getNode(int[][] gra, int root) {
        int n = gra.length;
        int max = 0, node = 0;
        int[] h = new int[n];
        int[] stack = new int[n];
        int size = 0;
        for(int i = 0; i < n; i++) {
            int p = i, count = 0;
            while(p != root && h[p] == 0) {
                stack[size++] = p;
                p = getNext(gra, p);
            }
            while(size != 0) {
                int temp = stack[--size];
                h[temp] = h[p] + 1;
                if(h[temp] > max) {
                    max = h[temp];
                    node = temp;
                }
                p = temp;
            }
        }
        return new int[]{node, h[node]};
    }
    
    private int getRoot(int[][] gra) {
        int p = 0;
        while(gra[p] != null) p = getNext(gra, p);
        return p;
    }

    private int getNext(int[][] gra, int p) {
        int[] ret = gra[p];
        return ret[0] == p ? ret[1] : ret[0];
    }
}

只有更麻烦,没有最麻烦。emmmm....怎么说呢,看第一的代码也写了这么多心里平衡多了。我又找了找,看到一种入度的方式更优雅的写法, 用的二维数组来处理的,我觉得还挺容易理解的,我再贴上来:

class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> res = new ArrayList<>();
        if (n < 2) {
            if (n == 1) {
                res.add(0);
            }
            return res;
        }
        int[] degrees = new int[n];
        int[][] nodes = new int[n][];
        int[] neighborsLen = new int[n];
        
        for (int[] edge : edges) {
            degrees[edge[0]]++;
            degrees[edge[1]]++;
        }
        Queue<Integer> queue = new LinkedList<>();
        int count = n;
        for (int i = 0; i < n; i++) {
            nodes[i] = new int[degrees[i]];
            if (degrees[i] == 1) {
                queue.offer(i);
                count--;
            }
        }
        for (int[] edge : edges) {
            nodes[edge[0]][neighborsLen[edge[0]]++] = edge[1];
            nodes[edge[1]][neighborsLen[edge[1]]++] = edge[0];
        }
        while (count > 0) {
            int len = queue.size();
            for (int i = 0; i < len; i++) {
                int v = queue.poll();
                for (int neighbor : nodes[v]) {
                    if (--degrees[neighbor] == 1) {
                        queue.offer(neighbor);
                        count--;
                    }
                }
            }
        }
        return (List<Integer>)queue;
        /*while (!queue.isEmpty()) {
            res.add(queue.poll());
        }
        return res;*/
    }
}

然后这道题就这样吧,说实话我觉得这种题处理的麻烦性比思路还不好想。我记得这个叫做拓扑排序,可能是我用的还不习惯。所以真的,list,map,数组之类的写的死去活来的,哈哈
今天的笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!生活健健康康!顺便打个广告。java技术交流群:130031711,欢迎各位大佬萌新踊跃加入。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容