LintCode数组题总结

做算法题的时候,几乎不可避免要跟数组打交道。在LintCode上数组那一章有这么一些题目:

《LintCode数组题总结》

《LintCode数组题总结》

1)547. Intersection of Two Arrays

比较简单。要求找到2个数组的交集,简单点的方法就是用2个hashSet,第一个HashSet存第一个数组的元素。然后扫描第二个数组,如果第二个数组中的元素在第一个HashSet中出现了,那么就把它加到第二个HashSet中。最后第二个HashSet就是两个数组的交集了。

2)6. Merge Two Sorted Arrays

比较简单。对两个数组进行merge,新开一个数组,从后往前扫描。两两比较,较大的加到尾部。最后如果2个数组有剩余,则把剩余的元素也加进去。

3)
64. Merge Sorted Array

比较简单。对2个有序数组A和B进行合并,要求把B合并进A,因为A数组尾部有足够的空间可以容纳B。建议从后往前扫描,这样的代码更加简洁。扫描完了后看B数组有没有剩余,若有剩余则把剩余的元素也添加到A数组头部。代码如下:

    public void mergeSortedArray(int[] A, int m, int[] B, int n) {
        
        while (m >= 1 && n >= 1) {
            if (A[m - 1] >= B[n - 1]) {
                A[m + n - 1] = A[m - 1];
                m--;
            } else {
                A[m + n - 1] = B[n - 1];
                n--;
            }
        }
        
        while (n > 0) {
            A[m + n - 1] = B[n - 1];
            n--;
        }
        
        return;
    }

4)148. Sort Colors

数组中的元素只有0、1、2这三个类型的数字,要求对他排序。Given [1, 0, 1, 2], sort it in-place to [0, 1, 1, 2]。官网提示了一种naive的做法:A rather straight forward solution is a two-pass algorithm using counting sort. First, iterate the array counting number of 0’s, 1’s, and 2’s, then overwrite array with total number of 0’s, then 1’s and followed by 2’s. Could you come up with an one-pass algorithm using only constant space?

但是得循环2次,有没有办法只用一次循环搞定呢?那就得用双指针法了。

从数组两端向中间遍历,前面放0,后面放2。把前面出现的2放到后面,后面出现的0放到前面,这样中间剩下的就是1

我的解法是定义指针start = 0,指针end = n – 1;一次遍历,如果遇到0,交换给start,遇到2,交换给end,遇到1别管。代码如下:

    public void sortColors(int[] nums) {
        int start = 0, end = nums.length - 1;
        int index = 0;
        while (index <= end) {
            if (nums[index] == 0) {
                swap(nums, index, start);
                index++;
                start++;
            } else if (nums[index] == 1) {
                index++;
            } else {
                swap(nums, index, end);
                end--;
            }
        }
    }
    private void swap(int[] nums, int a, int b) {
        int temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }

算法的关键在于,当从前往后扫描数组的时候,把0往最左边放,把2往最右边放。

当index对应的元素是0时,就把它和start交换,往前面放。因为是从左往右扫描的,所以每次遇到0都往左边放,这样能够保证左边都是0。

当index对应的元素是1时,直接继续扫描,不用管它。(因为在之后的扫描会把这个1变为0或者维持1不变)

当index对应的元素是2时,我们把它与end交换后,只需要把end指针往左边挪一位,而不用给index++。但是当index对应的元素是0时,我们却要给index++。这是为啥呢。

原因在于,index对应的值为2时,end对应的值也可能为2,在这种情况下,交换完了之后,index自己还是2,所以不行,index还的继续留在这个位置;

5)143. Sort Colors II

新开一个数组,用于记录1到k出现的次数,然后再根据出现的次数把这些数字写回原数组。时间复杂度为O(n)。以空间换时间。

    public void sortColors2(int[] colors, int k) {
        int[] kArray = new int[k + 1];
        for (int i = 0; i < colors.length; i++) {
            kArray[colors[i]]++;
        }
        
        int index = 0;
        for (int i = 1; i < kArray.length; i++) {
            while (kArray[i] > 0) {
                colors[index++] = i;
                kArray[i]--;
            }
        }

    }

6)56. Two Sum

给定一个target,要求在数组中找到两个数之和等于target,并返回这两个数的下标。最简单的是2层循环嵌套暴力求解O(n^2),稍微好一点的解法是先排序,然后再在排好序的数组中从两边往中间扫描,如果找到2个数之和等于target,就在原数组中扫描找到它们原来的下标并返回,时间复杂度是O(nlog(n)),代码如下:

    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[]{-1, -1};
        int[] oldNums = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            oldNums[i] = nums[i];
        }
        Arrays.sort(nums);
        int left = 0, right = nums.length - 1;
        while (left < right) {
            if (target - nums[left] == nums[right]) {
                res[0] = nums[left];
                res[1] = nums[right];
                break;
            } else if (target - nums[left] < nums[right]) {
                right--;
            } else {
                left++;
            }
        }
        int[] toReturn = new int[2];
        for (int i = 0; i < oldNums.length; i++) {
            if (oldNums[i] == res[0]) {
                toReturn[0] = i + 1;
                break;
            }
        }
        for (int i = 0; i < oldNums.length; i++) {
            if (oldNums[i] == res[1] && i != toReturn[0] - 1) {
                toReturn[1] = i + 1;
                break;
            }
        }
        Arrays.sort(toReturn);
        return toReturn;
    }

有没有O(n)的解法呢?有的,那就是利用HashMap,如下所示:

我的目标是要找到a+b=target中的a和b,从左往右扫描数组,每次都把target-a加进map中,同时每次都在map中寻找看target-a存不存在:

    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; i++) {
            if (map.get(nums[i]) != null) {
                res[0] = map.get(nums[i]) + 1;
                res[1] = i + 1;
            }
            map.put(target - nums[i], i);
        }
        return res;
    }

7)
533. Two Sum Closest

要求在数组中找到2个数,使得这两个数之和最接近给定的target。Given array nums = [-1, 2, 1, -4], and target = 4. The minimum difference is 1. (4 – (2 + 1) = 1).

一开始我用的是2层循环,暴力的对数组中每两个数求和。不过后来在youtube上看到了geeksforgeeks的一个老印的解法,是O(nlog)的。第一遍就排序。然后再设置2个指针,分别从两边往中间扫描。2个指针所对应的数之和如果大于target,那么就把右指针往左边移动;反之则把左指针往右边移动。这样能够保证找到最接近于target的两数之和。(具体怎么证明我还没想清楚,但这个思路绝对是对的,求大神给个清晰地数学证明)。

算法流程如下:

1) Sort all the elements of the array. 

2) Keep 2 index variables for left and right index. 

3) Initialize: left index l = 0. r = n-1. 

4) sum = a[l]+a[r] 

5) If sum is negative than l++ else r — . 

6) Keep track of absolute min sum. 

7) Repeat steps 4,5,6 until l < r

代码如下:

    public int twoSumCloset(int[] nums, int target) {
        Arrays.sort(nums);
        int left = 0, right = nums.length - 1;
        int min = Integer.MAX_VALUE;
        
        while (left < right) {
            int sum = nums[left] + nums[right];
            int diff = Math.abs(sum - target);
            min = Math.min(min, diff);
            if (sum > target) {
                right--;
            } else {
                left++;
            }
        }
        
        return min;
    }

8)138. Subarray Sum

要求在一个数组中找到一个子数组,使得这个子数组的和为0。就是比如一个数组是[12, -3, 1, 2, -3, 4] ,[-3, 1, 2]  这一段的和是0。这道题有个技巧,我用sum函数代表前面n个元素的和.

sum(1) = 12

sum(2) = 9

sum(3) = 10

sum(4) = 12

发现规律木有?那就是sum(1) 等于 sum(4) ,而第1+1个元素到第4个元素的字数组和恰好就为0。

所以方法就是求连续子数组的和,然后看哪两个sum相等,相等的下标区间就是答案了。代码如下:

    public ArrayList<Integer> subarraySum(int[] nums) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        map.put(0, -1);
        
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (map.containsKey(sum)) {
                res.add(map.get(sum) + 1);
                res.add(i);
                break;
            }
            map.put(sum, i);
        }
        
        return res;
    }

405. Submatrix Sum

是上道题的变种,在矩阵中找到一个子矩阵的和为0。

sum[i][j]表示matrix[0][0]到matrix[i-1][j-1]所有元素的和。 

建立sum矩阵,为n+1行,m+1列。将第0行和第0列都初始化为0。 

遍历matrix,根据公式 sum[i][j] = matrix[i – 1][j – 1] + sum[i][j – 1] + sum[i – 1][j] -sum[i – 1][j – 1] 计算所有sum。 

然后取两个row:row1, row2。用一个线col从左到右扫过row1和row2,每次都用subArraySum=sum[row2][col]-sum[row1][col]来表示row1-row2和0-col这个矩形元素的sum。如果在同一个row1和row2中,有两条线(col1,col2)的subArraySum相等,则表示row1-row2和col1-col2这个矩形中的元素和为0。 

比如矩阵是:

[1 ,5  ,7],
[3 ,7  ,-8],
[4 ,-8 ,9],

那么对应的sum矩阵就是:

[1 ,6  ,13],
[4 ,16 ,15],
[8 ,12 ,20],

20-13=7,8-1=7,说明右下角的2*2的子矩阵的和就是0

代码如下:

    public int[][] submatrixSum(int[][] matrix) {
        int[][] res = new int[2][2];
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return res;
        }
        int m = matrix.length;
        int n = matrix[0].length;
        int[][] sum = new int[m + 1][n + 1];
        // initialize
        for (int i = 0; i <= m; i++) {
            sum[i][0] = 0;
        }
        for (int i = 0; i <= n; i++) {
            sum[0][i] = 0;
        }
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i-1][j] + sum[i][j-1] + matrix[i-1][j-1] - sum[i-1][j-1];
            }
        }
        
        for (int row1 = 0; row1 < m; row1++) {
            for (int row2 = row1 + 1; row2 <= m; row2++) {
                HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
                for (int col = 0; col <= n; col++) {
                    int subArraySum = sum[row2][col] - sum[row1][col];
                    if (map.containsKey(subArraySum)) {
                        res[1][0] = row2 - 1;
                        res[1][1] = col - 1;
                        res[0][0] = row1;
                        res[0][1] = map.get(subArraySum);
                        return res;
                    } else {
                        map.put(subArraySum, col);
                    }
                }
            }
        }
        
        return res;
    }

404. Subarray Sum II

给定一个正整数数组,找到所有和在区间[start, end]的子数组,返回符合要求的子数组个数即可。比如Given [1,2,3,4] and interval = [1,3], return 4。

我们一开始会想到先预处理数组,求出preSum数组,比如sum[5]就代表前5个数之和,sum[2]代表前2个数之和,那么如果sum[5]-sum[2]的差在给定区间范围内的话,那么arr[3], arr[4], arr[5]组成的子数组之和就是满足条件的,result++。这样的话,写出代码就是两层for循环,只要判断sum[i]-sum[j]的值落在区间内就统计自增。但是这样的代码最后会超时。所以我们需要用二分查找优化一下,我们外面用一层for循环,对于遍历到的每个sum[j],我只需要查看他前面有多少个数是落在区间[sum[j]-end, sum[j]-start]内就行,所以就是在它前面找对应的range内有多少个数字。

非负整数 Subarray 相关题目的一个常用解法就是通过计算前缀和数组,然后利用二分搜索来找到目标。在这里,类似的思路同样适用。举个例子,假设给定一个目标区间[low, high],当我们遍历到prefixSum[i]时,我们的搜索范围为prefixSum[0,i-1],而搜索目标有两个:

  1. argminj (prefixSum[i]prefixSum[j]high)argminj (prefixSum[i]−prefixSum[j]≤high)
    argminj (prefixSum[i]highprefixSum[j])⇒argminj (prefixSum[i]−high≤prefixSum[j])
  2. argmaxk (prefixSum[i]prefixSum[k]low)argmaxk (prefixSum[i]−prefixSum[k]≥low)
    argmaxk (prefixSum[i]lowprefixSum[k])⇒argmaxk (prefixSum[i]−low≥prefixSum[k])

分别为第一个大于或等于prefixSum[i] - high的位置,即最左边的位置,以及最后一个小于或等于prefixSum[i] - low的位置,即最右边的位置。

    public int find(int[] A, int len, int rightbound) {
        int ans = 0, l = 0, r = len - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (A[mid] >= rightbound) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return ans;
    }
    public int subarraySumII(int[] A, int start, int end) {
        if (A == null || A.length == 0 || start > end) {
            return 0;
        }
        int[] sum = new int[A.length];
        sum[0] = A[0];
        for (int i = 1; i < A.length; ++i)
            sum[i] = sum[i-1] + A[i];
        
        int res = 0;
        for (int i = 0; i < sum.length; i++) {
            if (sum[i] >= start && sum[i] <= end) {
                res++;
            }
            int l = sum[i] - end, r = sum[i] - start;
            res += find(sum, sum.length, r + 1) - find(sum, sum.length, l);
        }
        
        return res;
    }

9)139. Subarray Sum Closest

给定一个数组,要求找到他的子数组,使得子数组之和最接近于0。

做法就是利用前缀和,先用一个数组sum[i]来保存从nums[0]到nums[i]的和,同时还要记录下标。那么,我们想要得到nums[i]到nums[j]的和,只要用sum[j] – sum[i-1]就可以了。剩下的工作就是对sum数组排序,找到排序后相邻的差的绝对值最小的那一对节点。

class Pair {
    int sum;
    int index;
    public Pair(int s, int i) {
        sum = s;
        index = i;
    }
}
    
public class Solution {
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number 
     *          and the index of the last number
     */
    public int[] subarraySumClosest(int[] nums) {
        int[] res = new int[2];
        if (nums == null || nums.length == 0) {
            return res;
        } 
        
        int len = nums.length;
        if(len == 1) {
            res[0] = res[1] = 0;
            return res;
        }
        Pair[] sums = new Pair[len+1];
        int prev = 0;
        sums[0] = new Pair(0, 0);
        for (int i = 1; i <= len; i++) {
            sums[i] = new Pair(prev + nums[i-1], i);
            prev = sums[i].sum;
        }
        Arrays.sort(sums, new Comparator<Pair>() {
           public int compare(Pair a, Pair b) {
               return a.sum - b.sum;
           } 
        });
        int ans = Integer.MAX_VALUE;
        for (int i = 1; i <= len; i++) {
            
            if (ans > sums[i].sum - sums[i-1].sum) {
                ans = sums[i].sum - sums[i-1].sum;
                int[] temp = new int[]{sums[i].index - 1, sums[i - 1].index - 1};
                Arrays.sort(temp);
                res[0] = temp[0] + 1;
                res[1] = temp[1];
            }
        }
        
        return res;
    }
}

10)
31. Partition Array

要求对数组进行分组,所有小于k的都放在左边,所有≥k的都放在右边。如果是链表的话就好做很多,数组的话就稍微棘手一点。但弄清楚算法思路之后其实不难。

如果左指针对应的数≥k并且右指针对应的数<k的话,那就交换(这是满足交换的情况)

不满足交换的情况的话,那我们就继续往中间移动左右指针

    public int partitionArray(int[] nums, int k) {
        int start = 0, end = nums.length - 1;
        while (start < end) {
            if (nums[start] >= k && nums[end] < k) {
                swap(nums, start, end);
            } 
            if (nums[end] >= k) {
                end--;
            }
            if (nums[start] < k) {
                start++;
            }
        }
        int count = 0;
	    for (int i = 0; i < nums.length; i++) {
	        if (nums[i] < k) {
	            count++;
	        }
	    }
	    return count;
    }
    private void swap(int[] nums, int a, int b) {
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }

11)
65. Median of two Sorted Arrays

要求找到2个有序数组的中数,比如Given A=[1,2,3,4,5,6] and B=[2,3,4,5], the median is 3.5. Given A=[1,2,3] and B=[4,5], the median is 3.

一种直观的思路就是新开一个数组,把两个有序数组进行合并,然后再在新开的数组里面找到中数。代码如下:

    public double findMedianSortedArrays(int[] A, int[] B) {
        // write your code here
        int m = A.length;
        int n = B.length;
        int[] all = new int[m + n];
        while (m - 1 >= 0 && n - 1 >= 0) {
            if (A[m - 1] > B[n - 1]) {
                all[m + n - 1] = A[m - 1];
                m--;
            } else {
                all[m + n - 1] = B[n - 1];
                n--;
            }
        }
        
        while (m - 1 >= 0) {
            all[m + n - 1] = A[m - 1];
                m--;
        }
        while (n - 1 >= 0) {
            all[m + n - 1] = B[n - 1];
                n--;
        }
        
        int len = all.length;
        if (len % 2 == 1) {
            return (double)all[len / 2];
        } else {
            return (all[len / 2] + all[len / 2 - 1]) / 2.0;
        }
    }

这样的时间复杂度是O(m + n),好像题目要求要O(log(m + n))的解法,应该是用二分法才能做到log级别。

1. 我们借用findKthNumber的思想。先实现findKthNumber,如果是偶数个,则把中间2个加起来平均,奇数就用中间的。

2. 为了达到LOG级的复杂度,我们可以这样: 每次在A,B取前k/2个元素。有以下这些情况:

1). A的元素不够k/2. 则我们可以丢弃B前k/2. 反之亦然

证明: 我们使用反证法。 假设第K大在B的前k/2中,例如位置在索引m(m <= k/2-1)那么A必然拥有前k中的k -(m+1)个元素,而 m <= k/2-1,则 m+1 <= k/2 , k – (m+1) > k/2与条件:A的元素不够k/2矛盾,所以假设不成立,得证。 举个栗子: A: 6 7 8 B: 1 2 3 4 5 找第8大的数字,

2). A[mid] < B[mid] (mid是k/2 -1索引处的元素)。 这种情况下,我们可以丢弃A前k/2。

证明: 我们使用反证法。 假设第K大在A的前k/2中记为maxK,例如位置在索引m(m <= k/2-1)那么B必然拥有前k中的k -(m+1)个元素,而 m <= k/2-1,则 m+1 <= k/2 , k – (m+1) > k/2 推出B[mid] <= maxK 而A[mid] >= maxK 推出 A[mid]>=B[mid], 与题设矛盾。所以假设不能成立。 举个栗子: A: 1 2 B: 4 5 6 7 8 找第四大的数字 我们就可以首先排除1,2.

    public int findKth(int A[], int B[], int indexA, int indexB, int k) {
        int lenA = A.length;
        int lenB = B.length;
        
        if (indexA >= lenA) {
            return B[indexB + k - 1];
        } else if (indexB >= lenB) {
            return A[indexA + k - 1];
        }
        
        // Base Case, pay attention. 在这里必须要退出。因为k = 1的时候,不可能再分了。
        if (k == 1) {
            return Math.min(A[indexA], B[indexB]);
        }
        
        // -1是因为索引本身是从0开始的。而前k大元素含有k个元素。
        int mid = k / 2 - 1;
        
        // 注意,越界条件是 >= lenA. 怎么老是犯这个错误。。
        int keyA = indexA + mid >= lenA ? Integer.MAX_VALUE: A[indexA + mid];
        int keyB = indexB + mid >= lenB ? Integer.MAX_VALUE: B[indexB + mid];
        
        // 因为丢弃了k / 2个元素
        int kNew = k - k / 2;
        
        if (keyA < keyB) {
            return findKth(A, B, indexA + k / 2, indexB, kNew);
        } else {
            return findKth(A, B, indexA, indexB + k / 2, kNew);
        }
    }
    public double findMedianSortedArrays(int[] A, int[] B) {
        if (A == null || B == null) {
            return 0;
        }
    
        int len = A.length + B.length;
        
        double ret = 0;
        // 偶数个元素
        if (len % 2 == 0) {
            ret = (findKth(A, B, 0, 0, len / 2) + findKth(A, B, 0, 0, len / 2 + 1)) / (double)2.0;
        } else {
            // 奇数个元素
            ret = findKth(A, B, 0, 0, len / 2 + 1);            
        }
        
        return ret;
    }

12)49. Sort Letters by Case

一个数组中既有大写字母,也有小写字母。要求按照小写字母在左边、大写字母在右边进行的规则进行排序。用左右两个指针往中间扫描,左指针遇到一个大写字母则与右指针交换,右指针遇到一个小写字母则与左指针交换。

13)149. Best Time to Buy and Sell Stock

用一个数组表示股票每天的价格,数组的第i个数表示股票在第i天的价格。 只允许进行一次交易,也就是说只允许买一支股票并卖掉,求最大的收益。

动态规划法。从前向后遍历数组,记录当前出现过的最低价格,作为买入价格,并计算以当天价格出售的收益,作为可能的最大收益,整个遍历过程中,出现过的最大收益就是所求。

f[i]代表第i天的最大收益,min[i]代表到i为止最小的数字。

所以状态转移方程为f[i] = num[i] – min[i]

如果用序列型动态规划写的话,就是如下这样:

    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        // 状态
        int[] f = new int[prices.length];
        int[] min = new int[prices.length];
        
        // 初始化
        min[0] = prices[0];
        f[0] = 0;
        
        // 方程
        for (int i = 1; i < prices.length; i++) {
            min[i] = Math.min(min[i - 1], prices[i]);
            f[i] = prices[i] - min[i];
        }
        
        // 返回值
        int res = f[0];
        for (int i = 1; i < f.length; i++) {
            res = Math.max(f[i], res);
        }
        return res;
    }

这是一个经过动态规划专题训练的人第一遍会写出来的代码,但是有很多地方是可以优化的,比如可以只用一个min变量而不是一个min数组,也不必用f数组,直接用f变量代表当前的最大利润就行,一趟循环内就可以搞定,优化后的代码如下:

    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        // 状态
        int res = 0;
        int min = prices[0];
        
        // 方程
        for (int i = 1; i < prices.length; i++) {
            min = Math.min(min, prices[i]);
            res = Math.max(prices[i] - min, res);
        }

        return res;
    }

14)
150. Best Time to Buy and Sell Stock II

用一个数组表示股票每天的价格,数组的第i个数表示股票在第i天的价格。与上一题相比,不同的地方在于:交易次数不限,但一次只能交易一支股票,也就是说手上最多只能持有一支股票,求最大收益。这道题用的是贪心法,从前往后遍历数组,只要当天的价格高于前一天的价格,就算入收益。可以通过画个折线图来帮助理解。

15)151. Best Time to Buy and Sell Stock III

用一个数组表示股票每天的价格,数组的第i个数表示股票在第i天的价格。最多交易两次,手上最多只能持有一支股票,求最大收益。

这一题与上一题相比,不同的地方在于,最多只能进行2次交易。

动态规划法。以第i天为分界线,计算第i天之前进行一次交易的最大收益preProfit[i],和第i天之后进行一次交易的最大收益postProfit[i],最后遍历一遍,max{preProfit[i] + postProfit[i]} (0≤i≤n-1)就是最大收益。第i天之前和第i天之后进行一次的最大收益求法同Best Time to Buy and Sell Stock I。

    public int maxProfit(int[] prices) {
        if (prices == null || prices.length < 2) {
            return 0;
        }
        
        // 状态
        int[] pre = new int[prices.length];
        int[] post = new int[prices.length];
        
        // 初始化
        
        
        // 方程
        int curMin = prices[0];
        for (int i = 1; i < prices.length; i++) {
            curMin = Math.min(curMin, prices[i]);
            pre[i] = Math.max(prices[i] - curMin, pre[i - 1]);
        }
        
        int curMax = prices[prices.length - 1];
        for (int i = prices.length - 2; i >= 0; i--) {
            curMax = Math.max(curMax, prices[i]);
            post[i] = Math.max(post[i + 1], curMax - prices[i]);
        }
        
        // 结果
        int res = 0;
        for (int i = 0; i < prices.length; i++) {
            res = Math.max(pre[i] + post[i], res);
        }
        return res;
    }

16)
393. Best Time to Buy and Sell Stock IV

用一个数组表示股票每天的价格,数组的第i个数表示股票在第i天的价格。最多交易k次,手上最多只能持有一支股票,求最大收益。

我们用一个局部最优解和全局最有解表示到第i天进行j次的收益,这就是该动态规划的特殊之处。 

用local[i][j]表示到达第i天时,最多进行j次交易的局部最优解;

用global[i][j]表示到达第i天时,最多进行j次的全局最优解。它们二者的关系如下(其中diff = prices[i] – prices[i – 1]):

local[i][j] = max(global[i – 1][j – 1] , local[i – 1][j] + diff) 

global[i][j] = max(global[i – 1][j], local[i][j])

local[i][j]和global[i][j]的区别是:local[i][j]意味着在第i天一定有交易(卖出)发生

当第i天的价格高于第i-1天(即diff > 0)时,那么可以把这次交易(第i-1天买入第i天卖出)跟第i-1天的交易(卖出)合并为一次交易,即local[i][j]=local[i-1][j]+diff;

当第i天的价格不高于第i-1天(即diff<=0)时,那么local[i][j]=global[i-1][j-1]+diff,而由于diff<=0,所以可写成local[i][j]=global[i-1][j-1]。

global[i][j]就是我们所求的前i天最多进行k次交易的最大收益,可分为两种情况:

如果第i天没有交易(卖出),那么global[i][j]=global[i-1][j];

如果第i天有交易(卖出),那么global[i][j]=local[i][j]。

public class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length < 2) return 0;
        
        int days = prices.length;
        if (k >= days) return maxProfit2(prices);
        
        int[][] local = new int[days][k + 1];
        int[][] global = new int[days][k + 1];
        
        for (int i = 1; i < days ; i++) {
            int diff = prices[i] - prices[i - 1];
            
            for (int j = 1; j <= k; j++) {
                local[i][j] = Math.max(global[i - 1][j - 1], local[i - 1][j] + diff);
                global[i][j] = Math.max(global[i - 1][j], local[i][j]);
             }
        }
        
        return global[days - 1][k];
    }
    
    
    public int maxProfit2(int[] prices) {
        int maxProfit = 0;
        
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i - 1]) {
                maxProfit += prices[i] - prices[i - 1];
            }
        }
        
        return maxProfit;
    }
}

17)41. Maximum Subarray

要求最大的连续子数组之和。Given the array [−2,2,−3,4,−1,2,1,−5,3], the contiguous subarray [4,−1,2,1] has the largest sum = 6

可以用贪心算法解决。

sum用于记录当前的子数组的和,如果当前位置之前的子数组和sum小于0,那么这个sum加上当前的nums[i]肯定会比nums[i]自己要小,所以此时我就把sum清零,从当前位置开始记录子数组和。代码如下:

    public int maxSubArray(int[] nums) {
        // greedy algorithm
        // sum用于记录当前的字数组的和,如果当前位置之前的子数组和sum小于0
        // 那么这个sum加上当前的nums[i]肯定会比nums[i]自己要小,所以此时我就把sum清零,从当前位置开始记录子数组和
        int sum = 0;
        int max = Integer.MIN_VALUE;
        
        for (int i = 0; i < nums.length; i++) {
            if (sum < 0) {
                sum = 0;
            }
            sum += nums[i];
            max = Math.max(sum, max);
        }
        return max;
    }

18)44. Minimum Subarray

这道题和上一题遥相呼应,求最小的连续子数组之和。如果前面的子数组sum比0大,那就从当前位置开始重新计算子数组之和,两个变量:sum代表局部的最小值、min是全局最小值。

    public int minSubArray(ArrayList<Integer> nums) {
        int sum = 0;
        int min = Integer.MAX_VALUE;
        
        for (int i = 0; i < nums.size(); i++) {
            if (sum > 0) {
                sum = 0;
            }
            
            sum += nums.get(i);
            min = Math.min(sum, min);
        }
        
        return min;
    }

19)191. Maximum Product Subarray

在数组中找到一个子数组,这个子数组的元素之积最大。这是个典型的动态规划题,跟Maximum Subarray有异曲同工之妙,只不过这里求的不是子数组的和,而是子数组的乘积了。

再来回顾一下Maximum Subarray那道题,用动态规划的方法,就是要找到其转移方程式,也叫动态规划的递推式,动态规划的解法无非是维护两个变量,局部最优和全局最优,我们先来看Maximum SubArray的情况,如果遇到负数,相加之后的值肯定比原值小,但可能比当前值大,也可能小,所以,对于相加的情况,只要能够处理局部最大和全局最大之间的关系即可,对此,写出转移方程式如下:

local = Math.max(local + A[i], A[i]);

global = Math.max(local, global);

对应代码如下:

    public int maxSubArray(int[] A) {
        if (A == null || A.length == 0) {
            return 0;
        }
        
        int local = A[0];
        int global = A[0];
        
        for (int i = 1; i < A.length; i++) {
            local = Math.max(local + A[i], A[i]);
            global = Math.max(local, global);
        }
        
        return global;
    }

而对于Product Subarray,要考虑到一种特殊情况,即负数和负数相乘:如果前面得到一个较小的负数,和后面一个较大的负数相乘,得到的反而是一个较大的数,如{2,-3,-7},所以,我们在处理乘法的时候,除了需要维护一个局部最大值,同时还要维护一个局部最小值.

我们用max数组代表到当前位置为止的最大子数组乘积,min数组代表到当前位置为止的最小子数组乘积。

如果A[i] >0并且前面的子数组之和最大为正数的话,那么max[i] = max[i-1] * A[i];

如果A[i] >0并且前面的子数组之和最大为负数的话,那么max[i] = A[i];

如果A[i] < 0并且前面的子数组之和最大为负数的话,那么max[i] = min[i-1] * A[i];

如果A[i] < 0并且前面的子数组之和最大为正数的话,那么max[i] = Math.min(min[i-1] * A[i], A[i]);

所以综合来看,max[i] = MIN(A[i], max[i-1] * A[i], min[i-1] * A[i]);

如果A[i] >0并且前面的子数组之和最大为正数的话,那么min[i] = Math.min(min[i-1] * A[i], A[i]);

如果A[i] >0并且前面的子数组之和最大为负数的话,那么min[i] = min[i-1] * A[i];

如果A[i] < 0并且前面的子数组之和最大为负数的话,那么min[i] = Math.min(max[i-1] * A[i], A[i]);

如果A[i] < 0并且前面的子数组之和最大为正数的话,那么min[i] = max[i-1] * A[i];

所以综合来看,min[i] = MIN(A[i], max[i-1] * A[i], min[i-1] * A[i]);

由此,可以写出如下的转移方程式:

max[i] = MIN(A[i], max[i-1] * A[i], min[i-1] * A[i]);

min[i] = MIN(A[i], max[i-1] * A[i], min[i-1] * A[i]);

最后再在所有的max[i]中找到最大值返回。

    public int maxProduct(int[] A) {
        if (A == null || A.length == 0) {
            return 0;
        }
        
        // 状态
        int[] max = new int[A.length];
        int[] min = new int[A.length];
        
        // 初始化
        max[0] = min[0] = A[0];
        
        // 方程
        for (int i = 1; i < A.length; i++) {
            max[i] = Math.max(Math.max(A[i], max[i-1]*A[i]), min[i-1]*A[i]);
            min[i] = Math.min(Math.min(A[i], max[i-1]*A[i]), min[i-1]*A[i]);
        }
        
        // 结果
        int res = max[0];
        for (int i = 1; i < max.length; i++) {
            res = Math.max(res, max[i]);
        }
        
        return res;
    }

总结:动态规划题最核心的步骤就是要写出其状态转移方程,但是如何写出正确的方程式,需要我们不断的实践并总结才能达到。

20)42. Maximum Subarray II

给定一个整数数组,找出两个不重叠子数组使得它们的和最大。每个子数组的数字在数组中的位置应该是连续的。返回最大的和。

类似题是[Best Time to Buy and Sell Stock III] 找两个区间,和最大,

则一定存在一个分割线,分割线左右两边分别转化为求一个子数组最大,即[Maximum Subarray I] 

从左往右枚举一遍分割线求出Maximum Subarray,再从右往左枚举一遍分割线求出Maximum Subarray 这样时间复杂度是O(n)

记录left max sum 和 right max sum, 然后求最大的left [i] + right[i+1]

    public int maxTwoSubArrays(ArrayList<Integer> nums) {
        int size = nums.size();
        int[] left = new int[size];
        int[] right = new int[size];
        
        int local = nums.get(0);
        int global = nums.get(0);
        left[0] = nums.get(0);
        // 找到左边的最大子数组之和
        for (int i = 1; i < size; i++) {
            local = Math.max(local + nums.get(i), nums.get(i));
            global = Math.max(local, global);
            left[i] = global;
        }
        local = nums.get(size - 1);
        global = nums.get(size - 1);
        right[size - 1] = nums.get(size - 1);
        // 找到右边的最大子数组之和
        for (int i = size - 2; i >= 0; i--) {
            local = Math.max(local + nums.get(i), nums.get(i));
            global = Math.max(local, global);
            right[i] = global;
        }
        
        // 找到最大的两个子数组之和
        int res = Integer.MIN_VALUE;
        for (int i = 0; i < size - 1; i++) {
            res = Math.max(res, left[i] + right[i + 1]);
        }
        return res;
    }

21)
45. Maximum Subarray Difference

这道题跟上道题有点像,类似题[Maximum Subarray II], 枚举分割线,但本题每次要知道分割线左边和右边的最大/最小数组和  枚举一遍分割线,求max( abs(左最大-右最小), abs(左最小-右最大) )

    public int maxDiffSubArrays(int[] nums) {
        int size = nums.length;
        int[] leftMax = new int[size];
        int[] rightMax = new int[size];
        int[] leftMin = new int[size];
        int[] rightMin = new int[size];
        
        int local = nums[0];
        int global = nums[0];
        leftMax[0] = nums[0];
        // 找到左边的最大子数组之和
        for (int i = 1; i < size; i++) {
            local = Math.max(local + nums[i], nums[i]);
            global = Math.max(local, global);
            leftMax[i] = global;
        }
        
        local = nums[size - 1];
        global = nums[size - 1];
        rightMax[size - 1] = nums[size - 1];
        // 找到右边的最大子数组之和
        for (int i = size - 2; i >= 0; i--) {
            local = Math.max(local + nums[i], nums[i]);
            global = Math.max(local, global);
            rightMax[i] = global;
        }
        
        local = nums[0];
        global = nums[0];
        leftMin[0] = nums[0];
        // 找到左边的最小子数组之和
        for (int i = 1; i < size; i++) {
            local = Math.min(local + nums[i], nums[i]);
            global = Math.min(local, global);
            leftMin[i] = global;
        }
        
        local = nums[size - 1];
        global = nums[size - 1];
        rightMin[size - 1] = nums[size - 1];
        // 找到右边的最小子数组之和
        for (int i = size - 2; i >= 0; i--) {
            local = Math.min(local + nums[i], nums[i]);
            global = Math.min(local, global);
            rightMin[i] = global;
        }
        
        // 找到大的两个子数组之和
        int res = Integer.MIN_VALUE;
        for (int i = 0; i < size - 1; i++) {
            res = Math.max(res, rightMax[i + 1] - leftMin[i]);
            res = Math.max(res, leftMax[i] - rightMin[i + 1]);
        }
        return res;
    }

22)43. Maximum Subarray III

求最大子数组之和,有k个子数组。动态规划题 f[i][j]代表选取前i个数字、分成了j组的子数组之和。
d[i][j] = max(d[i][j], d[m][j-1] + max) 
m ∈ [j-1,  i-1]; 
max是[j-1,  i-1]范围内的max subarray, 用求max subarray的方法,需要从后往前算

    public int maxSubArray(int[] nums, int k) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // 状态
        int n = nums.length;
        int[][] f = new int[n+1][k+1];
        
        // 方程
        for (int j = 1; j <= k; j++) {
            for (int i = j; i <= n; i++) {
                f[i][j] = Integer.MIN_VALUE;
                int global = Integer.MIN_VALUE;
                int local = 0;
                for (int m = i-1; m >= j-1; m--) {
                    local = Math.max(nums[m], nums[m]+local);
                    global = Math.max(local, global);
                    f[i][j] = Math.max(f[i][j], f[m][j-1]+global);
                }
                
            }
        }
        
        return f[n][k];
    }

23)
57. 3Sum

在数组中找到3个数之和等于0,a + b + c = 0。其实跟2 sum那道题很像,先对数组排序,我先找到一个a,然后再在a右边的数组中找到两数之和,使得这2数之和等于0-a。所以就相当于2 sum那道题再多了一层循环。代码里需要注意的地方就是要跳过重复的元素。

24)
58. 4Sum 求数组中等于某个target的四个数之和,跟3Sum有点像,也是要先排序,然后比3Sum多了层循环。

    public ArrayList<ArrayList<Integer>> fourSum(int[] num, int target) {
        ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer> >();
        if (num == null || num.length < 4) {
            return res;
        }
        
        Arrays.sort(num);
        
        for (int i = 0; i < num.length - 3; i++) {
            // skip duplicate
            if (i != 0 && num[i] == num[i - 1]) {
                continue;
            }
            
            for (int j = i + 1; j < num.length - 2; j++) {
                // skip duplicate
                if (j != i + 1 && num[j] == num[j - 1]) {
                    continue;
                }
                int left = j + 1, right = num.length - 1;
                while (left < right) {
                    int sum = num[i] + num[j] + num[left] + num[right];
                    if (sum == target) {
                        ArrayList<Integer>tmp = new ArrayList<Integer>();
                        tmp.add(num[i]);
                        tmp.add(num[j]);
                        tmp.add(num[left]);
                        tmp.add(num[right]);
                        res.add(tmp);
                        left++;
                        right--;
                        // skip duplicate
                        while (left < right && num[left] == num[left - 1]) {
                            left++;
                        }
                        while (left < right && num[right] == num[right + 1]) {
                            right--;
                        }
                    } else if (sum < target) {
                        left++;
                    } else {
                        right--;
                    }
                }
            }
        }
        
        return res;
    }

25)59. 3Sum Closest

跟3Sum那道题又有一点像,求三个数之和跟target的差最小。思路和3Sum一样,只不过多了2个变量用于记录3数和与target的差。

    public int threeSumClosest(int[] num, int target) {
        if (num == null || num.length < 3) {
            return 0;
        }
        
        Arrays.sort(num);
        int diff = Integer.MAX_VALUE;
        int res = 0;
        for (int i = 0; i < num.length - 2; i++) {
            int left = i + 1;
            int right = num.length - 1;
            while (left < right) {
                int sum = num[i] + num[left] + num[right];
                if (Math.abs(sum - target) < diff) {
                    res = sum;
                    diff = Math.abs(sum - target);
                }
                if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return res;
    }

26)144. Interleaving Positive and Negative Numbers

一个数组中有正数和负数,要求对这个数组做操作,使得正负数交替排列。可以先对数组进行partition操作,负数全部移动到左边,正数移动到右边。然后再对他进行交替更换。

    public void rerange(int[] A) {
        partition(A);
        interleave(A);
    }
   private void partition(int[] A) {
       int left = 0, right = A.length - 1;
        while (left <= right) {
            while (A[left] < 0) {
                left++;
            }
            while (A[right] >= 0) {
                right--;
            }
            if (left <= right) {
                int tmp = A[left];
                A[left] = A[right];
                A[right] = tmp;
                left++;
                right--;
            }
        }
   }
   private void interleave(int[] A) {
       int odd = 0, even = 0;
       for (int i = 0; i < A.length; i++) {
           if (A[i] < 0) {
               odd++;
           } else {
               even++;
           }
       }
       int left, right;
       if (odd == even) {
           left = 0;
           right = A.length - 1;
       } else if (odd > even) {
           left = 1;
           right = A.length - 1;
       } else {
           left = 0;
           right = A.length - 2;
       }
       while (left < right) {
           int tmp = A[left];
           A[left] = A[right];
           A[right] = tmp;
           left = left + 2;
           right = right - 2;
       }
   }


50. Product of Array Exclude Itself

要求计算数组的除了当前元素之外的累乘和,定义如下:Define B[i] = A[0] * … * A[i-1] * A[i+1] * … * A[n-1], calculate B WITHOUT divide operation。不准用除法操作。举个例子:For A = [1, 2, 3], return [6, 3, 2].

也就是要求一个新的数组B,这个新的数组的每一个元素,与原数组A之间的关系定义是这样:B[i] = A[0] * … * A[i-1] * A[i+1] * … * A[n-1]。

分析一下,我们需要的就是左边的累乘x,以及右边的累乘y,然后把x和y相乘,得到的就是新数组的元素了。

是一道典型的前向-后向遍历题:

    public ArrayList<Long> productExcludeItself(ArrayList<Integer> A) {
        long[] left = new long[A.size()];
        long[] right = new long[A.size()];
        ArrayList<Long> res = new ArrayList();
        
        left[0] = 1;
        // 从前往后遍历,计算左乘数组
        for (int i = 1; i < left.length; i++) {
            left[i] = A.get(i-1) * left[i-1];
        }
        
        right[right.length - 1] = 1;
        // 从后往前遍历,计算右乘数组
        for (int i = right.length - 2; i >= 0; i--) {
            right[i] = A.get(i+1) * right[i+1];
        }
        
        // 左边的累乘 * 右边的累乘
        for (int i = 0; i < A.size(); i++) {
            res.add(left[i] * right[i]);
        }
        return res;
    }

402. Continuous Subarray Sum

跟maximum subarray那道题很像。不同之处在于这道题让返回最大子数组的范围座标,而之前那道题只需要返回最大和即可,所以这道题我们要及时更新left和right变量,分别为子数组的起始和结束位置。我们用curSum来记录当前位置的累计和,如果某一个位置之前的累计和为负数,那么我们直接从当前位置开始重新算即可,因为加上负数还不如不加,此时将left和right都更新为当前位置i;如果之前的累计和大于等于0,那么我们把累计和curSum再加上当前的数字,然后更新end位置为i。此时我们更新最大子数组之和max,以及res即可,参见代码如下:

    public ArrayList<Integer> continuousSubarraySum(int[] A) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        res.add(-1);
        res.add(-1);
        
        int left = 0, right = 0, curSum = 0, max = Integer.MIN_VALUE;
        for (int i = 0; i < A.length; i++) {
            if (curSum < 0) {
                curSum = A[i];
                left = right = i;
            } else {
                curSum += A[i];
                right = i;
            }
            // update global maximum
            if (curSum > max) {
                max = curSum;
                res.remove(0);
                res.remove(0);
                res.add(left);
                res.add(right);
            }
        }
        
        return res;
    }

403. Continuous Subarray Sum II

与上一题的不同之处在于数组是循环数组,就是要在这个循环数组里找到和最大的子数组。比如:Give [3, 1, -100, -3, 4], return [4,1]。因为4,3,1组成的子数组的和最大。

那我们可以基于上题扩展思路一下,对于循环数组的处理我们换一种思路,我们如果能求出[-100, -3]不就可以求出[4,3,1]了嘛。而[-100, -3]是和最小的子数组,所以我们只要求出和最小的子数组就行了。同时也求出和最大的子数组,然后比较一下两者的较大值,再返回较大值的下标。

    public ArrayList<Integer> continuousSubarraySumII(int[] A) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        
        int[] maxIndex = maxContinuousSubarraySum(A);
        int[] minIndex = minContinuousSubarraySum(A);
        
        int maxSum = 0, minSum = 0, totalSum = 0;
        for (int i = 0; i < A.length; i++) {
            totalSum += A[i];
            if (i >= maxIndex[0] && i <= maxIndex[1]) {
                maxSum += A[i];
            }
            if (i >= minIndex[0] && i <= minIndex[1]) {
                minSum += A[i];
            }
        }
        if (maxSum >= totalSum - minSum || minIndex[0] == 0 && minIndex[1] == A.length - 1) {
            res.add(maxIndex[0]);
            res.add(maxIndex[1]);
        } else {
            res.add(minIndex[1] + 1);
            res.add(minIndex[0] - 1);
        }
        
        return res;
    }
    public int[] minContinuousSubarraySum(int[] A) {
        int[] res = new int[]{-1, -1};
        
        int left = 0, right = 0, curSum = 0, min = Integer.MAX_VALUE;
        for (int i = 0; i < A.length; i++) {
            if (curSum > 0) {
                curSum = A[i];
                left = right = i;
            } else {
                curSum += A[i];
                right = i;
            }
            // update global maximum
            if (curSum < min) {
                min = curSum;
                res[0] = left;
                res[1] = right;
            }
        }
        return res;
    }
        
    public int[] maxContinuousSubarraySum(int[] A) {
        int[] res = new int[]{-1, -1};
        
        int left = 0, right = 0, curSum = 0, max = Integer.MIN_VALUE;
        for (int i = 0; i < A.length; i++) {
            if (curSum < 0) {
                curSum = A[i];
                left = right = i;
            } else {
                curSum += A[i];
                right = i;
            }
            // update global maximum
            if (curSum > max) {
                max = curSum;
                res[0] = left;
                res[1] = right;
            }
        }
        return res;
    }

574. Build Post Office

给定一个二维网格,每个格子要么是房子 1,要么是空地 0,我们需要找到一个空地盖邮局使得所有房子到邮局的距离总和最小。同时,房子和空地都可以随意通过。

在该题设中,因为房子并不会充当一个阻碍物,所以并不需要用 BFS 等算法来计算最短路径,简单地计算两个格子之间在网格上的曼哈顿距离(Manhattan distance)就好了,即

d(P,Q)=|P.xQ.x|+|P.yQ.y|

首先扫描一遍将所有house的点记录下来,然后遍历图中所有0的点,计算每个0的点到这些house的距离和,选最小的那个即可。这种情况下可以优化到O(k * n ^ 2),但是如果数据量很大还是过不了。

    public int distance(int a, int b, int x, int y) {
        return Math.abs(a - x) + Math.abs(b - y);
    }
    public int shortestDistance(int[][] grid) {
        ArrayList<Integer> house = new ArrayList<Integer>();
        int n = grid.length, m = grid[0].length;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 1) {
                    house.add(i);
                    house.add(j);
                }
            }
        }
        
        int res = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int tmp = 0;
                if (grid[i][j] == 1) {
                    continue;
                }
                for (int k = 0; k < house.size(); k = k + 2) {
                    tmp += distance(i, j, house.get(k), house.get(k + 1));
                }
                res = Math.min(res, tmp);
            }
        }
        
        return res;
    }

算法参考:https://zhengyang2015.gitbooks.io/lintcode/build_post_office_573.html

因此需要减少搜索的点。想到的方法是在所有房子围成的形状的重心位置附近建邮局则到所有房子的距离之和最短(怎么证明?)。

因此步骤如下: 

1. 首先找到所有房子的重心。找所有房子x值的median和y值的median(如果是奇数个就是排序后取中间值,如果是偶数则取中间两个数再取平均值)即为重心。 

2. 然后用bfs来搜索。将重心加入queue中,然后开始一圈一圈(将出队的每个点周围八个点加入队中)向外找,用的是和逐层遍历二叉树的类似的方法(即在每一层开始的时候记录一下本层的点的个数,然后一次出队这么多点即可将本层的点全部出队)。每一圈结束时,返回该圈上的点作为post office能取的最小值,如果存在则停止搜索。即如果存在可以作为post office的点,则外圈点到各个房子的距离一定不会比内圈点更优。

    public int getMedian(ArrayList<Integer> arr) {
        Collections.sort(arr);
        int median = arr.get(arr.size() / 2);
        if (arr.size() % 2 == 0) {
            median = (median + arr.get(arr.size() / 2 - 1)) / 2;
        }
        return median;
    }
    public int getDistance(ArrayList<Integer> house, int x, int y) {
        int res = 0;
        for (int i = 0; i < house.size(); i = i + 2) {
            res += Math.abs(house.get(i) - x) + Math.abs(house.get(i + 1) - y);
        }
        return res;
    }
    public int shortestDistance(int[][] grid) {
        int[] dx = {0, 0, -1, 1, -1, 1, -1, 1};
        int[] dy = {-1, 1, 0, 0, -1, -1, 1, 1};
        
        int n = grid.length, m = grid[0].length;
        boolean[][] visit = new boolean[n][m];
        ArrayList<Integer> house = new ArrayList<Integer>();
        ArrayList<Integer> xArr = new ArrayList<Integer>();
        ArrayList<Integer> yArr = new ArrayList<Integer>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 1) {
                    house.add(i);
                    house.add(j);
                    xArr.add(i);
                    yArr.add(j);
                }
            }
        }
        // find median of all the house positions
        int xMedian = getMedian(xArr), yMedian = getMedian(yArr);
        Queue<Integer> queue = new LinkedList<Integer>();
        queue.add(xMedian);
        queue.add(yMedian);
        int res = Integer.MAX_VALUE;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i = i + 2) {
                int x = queue.poll(), y = queue.poll();
                if (grid[x][y] == 0) {
                    res = Math.min(res, getDistance(house, x, y));
                }
                for (int j = 0; j < 8; j++) {
                    int nextX = x + dx[j], nextY = y + dy[j];
                    if (nextX >= 0 && nextX < n && nextY >= 0 && nextY < m && !visit[nextX][nextY]) {
                        visit[nextX][nextY] = true;
                        queue.add(nextX);
                        queue.add(nextY);
                    }
                }
            }
            if (res != Integer.MAX_VALUE) {
                return res;
            }
        }
        
        return -1;
    }

573. Build Post Office II

这道题跟上题非常类似,只不过多了一堵墙,0表示空地,1表示房子,2表示墙。邮局只能建立在空地上,墙壁和房子都不能穿过,所以不能只是简单地计算曼哈顿距离了。

这道题和I比较类似,但是因为不能穿过wall和house,所以必须用bfs的方法搜索最近距离,而不能直接计算几何距离。

1. 将数组扫描一遍找到所有房子。

2. 为每一个房子建立一个距离矩阵,计算该房子到所有0点的距离。即distance[i][j][k]为k房子到grid[i][j]上的点的距离。计算距离的时候用bfs搜索。

3. 然后遍历图上所有为0的点,查询k张距离矩阵,将所有房子到该点的距离加起来即为在该点建邮局的距离总和。若在查询过程中遇到某个点在某张距离矩阵上的值为无穷大,则说明该点无法到达该房子,直接停止搜索即可。

4. 选3中距离最小的点即可。

算法参考链接: https://zhengyang2015.gitbooks.io/lintcode/build_post_office_ii_573.html

public class Solution {
    /**
     * @param grid a 2D grid
     * @return an integer
     */
    class Node{
        int x;
        int y;
        int dis;
        public Node(int x, int y, int dis){
            this.x = x;
            this.y = y;
            this.dis = dis;
        }
    }

    public int shortestDistance(int[][] grid) {
        // Write your code here
        if(grid == null || grid.length == 0 || grid[0].length == 0){
            return -1;
        }

        int n = grid.length;
        int m = grid[0].length;
        ArrayList<Node> house = new ArrayList<Node>();
        //find house position
        for(int i = 0; i < n; i++){
            for(int j = 0; j < m; j++){
                if(grid[i][j] == 1){
                    house.add(new Node(i, j, 0));
                }
            }
        }
        //no empty place
        int k = house.size();
        if(k == n * m){
            return -1;
        }

        int[][][] distance = new int[k][n][m];
        for(int i = 0; i < k; i++){
            for(int j = 0; j < n; j++){
                Arrays.fill(distance[i][j], Integer.MAX_VALUE);
            }
        }

        for(int i = 0; i < k; i++){
            getDistance(house.get(i), distance, i, grid);
        }

        int min = Integer.MAX_VALUE;
        for(int i = 0; i < n; i++){
            for(int j = 0; j < m; j++){
                if(grid[i][j] == 0){
                    int sum = 0;
                    for(int l = 0; l < k; l++){
                        if(distance[l][i][j] == Integer.MAX_VALUE){
                            sum = Integer.MAX_VALUE;
                            break;
                        }
                        sum += distance[l][i][j];
                    }
                    min = Math.min(min, sum);
                }
            }
        }

        if(min == Integer.MAX_VALUE){
            return -1;
        }
        return min;
    }

    int[] dx = {-1, 1, 0, 0};
    int[] dy = {0, 0, -1, 1};
    //BFS search for shortest path
    private void getDistance(Node curt, int[][][] distance, int k, int[][] grid){
        int n = grid.length;
        int m = grid[0].length;
        Queue<Node> queue = new LinkedList<Node>();
        boolean[][] visited = new boolean[n][m];
        queue.offer(curt);
        visited[curt.x][curt.y] = true;

        while(!queue.isEmpty()){
            Node now = queue.poll();
            for(int i = 0; i < 4; i++){
                int nextX = now.x + dx[i];
                int nextY = now.y + dy[i];
                if(nextX >= 0 && nextX < n && nextY >= 0 && nextY < m && grid[nextX][nextY] == 0 && !visited[nextX][nextY]){
                    distance[k][nextX][nextY] = now.dis + 1;
                    queue.add(new Node(nextX, nextY, now.dis + 1));
                    visited[nextX][nextY] = true;
                }
            }
        }
    }
}

553. Bomb Enemy

这道题相当于一个简单的炸弹人游戏,让我想起了小时候玩的红白机的炸弹人游戏,放一个炸弹,然后爆炸后会炸出个‘十’字,上下左右的东西都炸掉了。这道题是个简化版,字母E代表敌人,W代表墙壁,这里说明了炸弹无法炸穿墙壁。数字0表示可以放炸弹的位置,让我们找出一个放炸弹的位置可以炸死最多的敌人。那么我最开始想出的方法是建立四个累加数组v1, v2, v3, v4,其中v1是水平方向从左到右的累加数组,v2是水平方向从右到左的累加数组,v3是竖直方向从上到下的累加数组,v4是竖直方向从下到上的累加数组,我们建立好这个累加数组后,对于任意位置(i, j),其可以炸死的最多敌人数就是v1[i][j] + v2[i][j] + v3[i][j] + v4[i][j],最后我们通过比较每个位置的累加和,就可以得到结果,参见代码如下:

    public int maxKilledEnemies(char[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        int n = grid.length, m = grid[0].length;
        int[][] up = new int[n][m];
        int[][] down = new int[n][m];
        int[][] left = new int[n][m];
        int[][] right = new int[n][m];
        int res = 0;
        
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int tmp = (j == 0 || grid[i][j] == 'W') ? 0 : left[i][j - 1];
                left[i][j] = grid[i][j] == 'E' ? tmp + 1 : tmp;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = m - 1; j >= 0; j--) {
                int tmp = (j == m - 1 || grid[i][j] == 'W') ? 0 : right[i][j + 1];
                right[i][j] = grid[i][j] == 'E' ? tmp + 1 : tmp;
            }
        }
        for (int j = 0; j < m; j++) {
            for (int i = 0; i < n; i++) {
                int tmp = (i == 0 || grid[i][j] == 'W') ? 0 : up[i - 1][j];
                up[i][j] = grid[i][j] == 'E' ? tmp + 1 : tmp;
            }
        }
        for (int j = 0; j < m; j++) {
            for (int i = n - 1; i >= 0; i--) {
                int tmp = (i == n - 1 || grid[i][j] == 'W') ? 0 : down[i + 1][j];
                down[i][j] = grid[i][j] == 'E' ? tmp + 1 : tmp;
            }
        }


        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == '0') {
                    int tmp = up[i][j] + down[i][j] + left[i][j] + right[i][j];
                    res = Math.max(tmp, res);
                }
            }
        }
        return res;
    }

点赞