array专题7

714 Best Time to Buy and Sell Stock with Transaction Fee

思路

首先是暴力枚举。考虑在第idx天能做的操作:买?卖?不操作?

/**
     * 暴力枚举
     * 
     * @param prices
     * @param fee
     * @return
     */
    public int maxProfitV99(int[] prices, int fee) {
        return dfs(prices, 0, 0, fee, 0);
    }

    /**
     ** 考虑在第idx天是买?卖?不操作?
     **/
    private int dfs(int[] prices, int idx, int buyed, int fee, int profit) {
        if (idx >= prices.length) {
            return profit;
        }
        int max = Integer.MIN_VALUE;
        if (buyed == 0) {
            // 买
            max = Math.max(max, dfs(prices, idx + 1, 1, fee, profit - prices[idx]));
        } else {
            // 卖
            max = Math.max(max, dfs(prices, idx + 1, 0, fee, profit + prices[idx] - fee));
        }
        // 不操作
        max = Math.max(max, dfs(prices, idx + 1, buyed, fee, profit));
        return max;
    }

接着:用动态规划的思路来解决

关键是找到递归方程。定义:hold[i]是在第i天持有股票的状态下的最大收益;sold[i]是在第i天卖掉股票的状态下的最大收益; 方程式:hold[i] = Math.max(hold[i-1],sold[i-1]-prices[i])//第i-1天就持有股票,或者第i天购买股票。sold[i] = Math.max(hold[i-1]+prices[i]-fee,sold[i-1])。大家能看到上面的暴力搜索的思路并不完全浪费。在找递归方程中起了作用:hold[i]的计算可能是前一天已经hold,或者是前一天卖了,今天买入。sold[i]的计算可能是前一天已经hold今天卖,或者是前一天已经sold。初始化:hold[0] = -prices[0];sold[0] = 0。

public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        if(n<2) return 0;
        int[] hold = new int[prices.length];
        int[] sold = new int[prices.length];
        hold[0] = -prices[0];
        sold[0] = 0;
        for(int i=1;i<prices.length;i++){
            hold[i] = Math.max(hold[i-1],sold[i-1]-prices[i]);
            sold[i] = Math.max(hold[i-1]+prices[i]-fee,sold[i-1]);
        }
        return Math.max(hold[n-1], sold[n-1]);
    }

还可以用贪心

关键是选择一个能不能卖掉股票的点,重新开始寻找买入机会。例如序列 1 3 2 8 ,如果发现2<3,就完成交易买1卖3,此时,由于fee=2,那么收益=(3-1-fee)+(8-2-fee) < (8-1-fee),说明卖早了。令maxP是当前最大的price,当(maxP-prices[i]>=fee),时则可以在maxP处卖出,且不会存在早卖的情况。
证明:不明白。

public int maxProfitV3(int[] prices, int fee) {
        int n = prices.length;
        if (n < 2)
            return 0;
        int maxP = prices[0];
        int minP = prices[0];
        int curProfit = 0;
        int profit = 0;
        for (int i = 1; i < prices.length; i++) {
            maxP = Math.max(maxP, prices[i]);
            minP = Math.min(minP, prices[i]);
            curProfit = Math.max(curProfit, prices[i] - minP - fee);
            if(maxP - prices[i]>=fee){
                profit += curProfit;//在maxP处交易
                maxP = prices[i];
                minP = prices[i];
                curProfit = 0 ;
            }
        }
        return profit+curProfit;
    }

代码

376 Wiggle Subsequence

思路:用贪心或者动态规划可以解决。
代码

84 Largest Rectangle in Histogram

思路:首先是暴力枚举。 枚举矩形的左右边界。
学习:接着还是暴力枚举,只是这次枚举的是矩形的最低高度。对于一个heights[i],算一下能够到达的最左边的下标和最右边的下标。复杂度还是没降低。在这次枚举过程中会发现可以执行跳跃,省掉一些计算。例如找左边最远:如果当前设置变量j = i;如果heights[i]<=heights[j-1],那直接跳到left[j+1];j=left[j-1],循环再继续判断。例如找右边最远:如果当前设置变量j = i;如果heights[i]<=heights[j+1],那直接跳到right[j+1];j=right[j+1],循环再继续判断。这种跳跃的思想,是学习到的。
代码

621 Task Scheduler

思路:每次应该选择符合条件的剩余总数多的那个任务。符合条件是指符合间隔条件。需要一个map,计算每个任务的数量;需要第二个map,记录当前状态下还有多少个时间间隔。就形成了第一版本的代码。在这个过程中,我没有考虑n=0,没有考虑 每个任务数量不同的情况。是后来才加上的代码。当然代码是超时的。
学习:计算每个不同任务的数量。每次小循环i从1到n,优先选择数量多的不同任务。这里很关键的地方是:只与不同任务的数量有关系,而与任务名称没有关系。用一个一个的数字表示不同的任务。这是我之前一直转不过弯的地方。例如 3个A 1个B 1个C 1个D 1个E ,n=2,最好的顺序是A->B->C->A->D->E->A。第一个小循环用数字表示就是 3-1,1-1,1-1;第二个小循环用数字表示就是2-1,1-1,1-1,第3个小循环是:1-1,因为数组为空,退出。
该思路的难点是处理小循环,注意小循环退出的条件。该思路可以用数组、优先队列两种方法实现。
代码

斐波那契数列

斐波那契数列的计算可以用动态规划计算,也可以用矩阵计算,有 O ( l o g n ) 的时间复杂度。
算法去冗余有两种角度:时间冗余和空间冗余。去时间冗余,就是利用空间,将本次计算结果存下来,下次用的时候直接取。 F ( n ) = F ( n 1 ) + F ( n 2 ) ,在这个式子中就有重复计算,可以用数组f[i]保存是i时候的结果。空间冗余,就看本次计算相关因素,只需要记录相关因素。还是 F ( n ) = F ( n 1 ) + F ( n 2 ) ,不需要记录f[0],f[1],只需要记录f[n-1],f[n-2],两个变量就可以。
动态规划时间复杂度:状态个数*(每个状态计算的时间复杂度);空间复杂度:数组大小。
动态规划解题套路:
1 设计暴力算法,找到冗余;
2 设计并存储状态(一维、二维、三维数组)
3 递归式(状态转移方程)
4 自底向上计算

48 Rotate Image

思路:不多写了。感叹一下自己没想到可以通过多个步骤得到旋转后的矩阵。
代码

153 Find Minimum in Rotated Sorted Array

学习:主要还是要观察所求值的特征。自己总结特征。最小元素的特点有两个:1 如果是旋转元素,那么 n u m s [ m i n ] < n u m s [ m i n 1 ] ;2 如果不是旋转元素,那么它应该是nums[0]。所以可以使用二分查找法:如果 n u m s [ m i d ] < n u m s [ m i d 1 ] ,则就是最小元素;否则如果 nums[mid]>nums[start] && nums[mid]>nums[end] ,那最小元素在右侧;否则在左侧。
代码

560 Subarray Sum Equals K

思路:最近看了动态规划的视频,知道一言不合就暴力搜索。子数组,那可能的枚举就是从0到1,2,3,….从1到2,3,…..。计算这些子数组的和是不是等于k。不过我遇到的几个问题是:1,开始是用递归写的,出现栈溢出,用循环解决。2 没有考虑负数的情况,在sum>=k的时候就break。3 没有考虑[0,0,0,0,0] 这种情况,在以i为开始,找到和为k的情况就break。时间复杂度O( n 2 )。
学习:子数组的和就是指从[i,j]的和。sum[i,j] = sum[0,j]-sum[0,i-1]。
代码

795 Number of Subarrays with Bounded Maximum

思路:分析题意要求每个子数组的最大值maxVal必须满足 m a x V a l >= L and m a x V a l <= R m a x V a l <= R 可以推出子数组中每个元素 <= R
子数组的暴力枚举:以i为起始,可以行程[i][i,i+1],[i,i+2]….[i,n-1]个子数组。在枚举子数组的时候把不符合条件的去掉,计数有效子数组即可。时间复杂度O(n^2)。
学习:改为O(n)。为什么不是O(nlogn)。因为一般logn需要有二分,而二分一般涉及到排序。本题是不能排序的。所以应该是O(n)。要想O(n)就应该是一个一个元素遍历,或者根据需要多遍历几次。
遍历每个元素i,会发现,如果 A [ i ] >= L A [ i ] <= R
数组[2,1,4,3] L=2,R=3
下标0: A [ i ] >= L A [ i ] <= R 增加个数1
下标1: A [ i ] < L 自己不能单独成为子数组,可以追加在前面的子数组后面。所以前面有几个子数组这里就增加几。
下标2: A [ i ] > R 不增加
下标3: A [ i ] >= L A [ i ] <= R ,增加1个。因为前一个元素超出范围了,只能从下标3开始计算。
代码

162 Find Peak Element

思路:找极值点。之前在贪心里面有一道题比这个还要难。思路就是找到一个元素i 如果 (nums[i]-nums[i-1])*(nums[i+1]-nums[i])<0,就找到了极值点的下标i。
学习:二分查找。这道题目居然也可以用二分。一直以为不排序的数组没法用二分查找。原因是这样的。只要找到一个极值点即可。在达到极值点前或者过了极值点之后是有序的。
i f n u m s [ i 1 ] < n u m s [ i ] < n u m s [ i + 1 ] ,那么极值点一定在i+1,i+2…n-1中。
i f n u m s [ i 1 ] > n u m s [ i ] > n u m s [ i + 1 ] ,那么极值点一定在[0,1,…i-1]中
i f n u m s [ i 1 ] < n u m s [ i ] > n u m s [ i + 1 ] ,那么极值点就是i
所以学习到局部有序,也可以用二分查找。
代码

猜你喜欢

转载自blog.csdn.net/flying_all/article/details/79688311