贪心算法、矩阵排序


根据身高重建队列

假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列,使得队列满足上面的性质。

注意:
总人数少于1100人。

示例

输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]

输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

贪心算法
1)让我们从最简单的情况下思考,当队列中所有人的 (h,k) 都是相同的高度 h,只有 k 不同时,解决方案很简单:每个人在重建后队列的位置(索引)即 k。


2)即使不是所有人都是同一高度,这个策略也是可行的。因为个子矮的人相对于个子高的人是 “看不见” 的,所以可以先安排个子高的人。

3)总结下来的算法就是:先按照身高从大到小排序,然后依序遍历,用LinkedList链表存储重建后的序列,每个人插入重建后序列时,位置(索引)即为其对应的k值。

 public static int[][] reconstructQueue(int[][] people) {

        Arrays.sort(people, new Comparator() {
            @Override
            public int compare(int[] o1, int[] o2) {
                // 按照身高排序, 身高一样按照k排序(为了减少链表的中间插入操作)
                return o2[0] == o1[0] ? o1[1] - o2[1] : o2[0] - o1[0];
            }
        });

        LinkedList sorted = new LinkedList<>();
        for(int[] p : people){
            sorted.add(p[1], p);
        }

        int[][] ret = new int[people.length][2];
        int ind = 0;
        for(int[] p : sorted){
            ret[ind++] = p;
        }
        return ret;

    }


最长上升子序列(https://leetcode-cn.com/problems/longest-increasing-subsequence/)

给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

动态规划
定义 dp[i]为考虑前 i 个元素,以第 i个数字结尾的最长上升子序列的长度,注意nums[i] 必须被选取。

我们从小到大计算 dp[]数组的值,在计算 dp[i]之前,我们已经计算出dp[0…i−1] 的值,则状态转移方程为:


即考虑往 dp[0…i−1] 中最长的上升子序列后面再加一个 nums[i]。由于 dp[j] 代表 nums[0…j] nums[j] 结尾的最长上升子序列,所以如果能从 dp[j]这个状态转移过来,那么 nums[i] 必然要大于nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。
最后,整个数组的最长上升子序列即所有 dp[i] 中的最大值。

总结:最后的求解值,未必一定是dp数组中的某个值,而是通过简单的遍历可以从dp数组中获取到。

public static int lengthOfLIS(int[] nums) {

        if (nums.length <= 1) {
            return nums.length;
        }
        int[] dp = new int[nums.length];
        // 初始化
        dp[0] = 1;
        // 规划
        for (int tail = 1; tail < nums.length; tail++) {
            int max = 0;
            for (int i = 0; i < tail; i++) {
                if (nums[tail] > nums[i]) {
                    max = Math.max(max, dp[i]);
                }
            }
            dp[tail] = max + 1;
        }
        // 输出dp数组中最大的即可
        int max = 1;
        for (int i = 0; i < nums.length; i++) {
            max = Math.max(max, dp[i]);
        }
        return max;
    }

贪心+二分法
对于此题的进阶,一旦出现logn的时间复杂度,往往可以联想到二分法。
考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个数组 d[i],表示长度为 i 的最长上升子序列的末尾元素的最小值(已知元素),用 len 记录目前最长上升子序列的长度,起始时 len 为 1,d[1]=nums[0]。

同时我们可以注意到 d[i]是关于 i 单调递增的。

我们依次遍历数组nums[] 中的每个元素,并更新数组 d[] 和 len 的值。如果nums[i]>d[len] 则更新 len=len+1,否则在d[1…len]中找满足d[i−1]

根据 d 数组的单调性,我们可以使用二分查找寻找下标 i,优化时间复杂度。

public static int lengthOfLIS2(int[] nums) {
        if (nums.length <= 1) {
            return nums.length;
        }
        int len = 1;
        int[] minTail = new int[nums.length+1];
        minTail[len] = nums[0];

        for (int i = 1; i < nums.length; i++) {
            if (len < nums.length && nums[i] > minTail[len]) {
                len += 1;
                minTail[len] = nums[i];
            } else {
                int l = 1;
                int r = len;
                while (l <= r) {
                    int m = l + (r - l) / 2;
                    if (minTail[m] < nums[i]) {
                        l = m + 1;
                    } else {
                        r = m - 1;
                    }
                }
                minTail[l] = nums[i];
            }
        }

        return len;
    }

优势洗牌(https://leetcode-cn.com/problems/advantage-shuffle/)

给定两个大小相等的数组 A 和 B,A 相对于 B 的优势可以用满足 A[i] > B[i] 的索引 i 的数目来描述。
返回 A 的任意排列,使其相对于 B 的优势最大化。
示例 1:
输入:A = [2,7,11,15], B = [1,10,4,11]
输出:[2,11,7,15]
示例 2:
输入:A = [12,24,8,32], B = [13,25,32,11]
输出:[24,32,8,12]

贪心算法
如果 A 中最小的牌 a 能击败 B 中最小的牌 b,那么我们应当将它们配对。否则, a 将无益于我们的比分,因为它无法击败任何牌。

我们为什么要在 a > b 时将 a 和 b 配对呢?这是因为此时在 A 中的每张牌都比 b 要大,所以不管我们在 b 前面放置哪张牌都可以得分。我们可以用手中最弱的牌来与 b 配对,这样会使 A 中剩余的牌严格地变大,因此会有更多得分点。

public static int[] advantageCount(int[] A, int[] B) {

        // 排序
        Arrays.sort(A);
        int[] originalB = B.clone();
        Arrays.sort(B);

        // 找出A中每个元素应该对应哪个B元素
        Map> assign = new HashMap<>();
        Queue remain = new LinkedList<>();
        int index = 0;
        for(int a : A){
            if(a > B[index]){
                if(assign.containsKey(B[index])){
                    assign.get(B[index]).add(a);
                }else {
                    Queue queue = new LinkedList<>();
                    queue.add(a);
                    assign.put(B[index], queue);
                }
                index++;
            }else {
                remain.add(a);
            }
        }

        int[] ret = new int[A.length];
        index = 0;
        for(int b : originalB){
            if(assign.containsKey(b) && assign.get(b).size() > 0){
                ret[index++] = assign.get(b).poll();
            }else {
                ret[index++] = remain.poll();
            }
        }
        return  ret;
    }

跳跃游戏

贪心算法

    public static boolean canJump(int[] nums) {
        int rightMost = 0;
        for (int i = 0; i < nums.length; i++) {
            if (i <= rightMost) {
                rightMost = Math.max(rightMost, nums[i] + i);
                if (rightMost >= nums.length - 1) {
                    return true;
                }
            }else {
                break;
            }
        }
        return false;
    }

加油站



贪心算法
算法

  • 初始化 totalSave_tank 和 current_tank 为 0 ,并且选择 0 号加油站为起点。totalSave_tank 表示如果跑完一圈剩余的油量,如果小于0,必然不可能环形一圈;current_tank表示当前剩余油量。
    遍历所有的加油站:
  • 每一步中,都通过加上 gas[i] 和减去 cost[i] 来更新 totalSave_tank 和 current_tank 。
  • 如果在 i 号加油站, current_tank < 0 ,将 i + 1 号加油站作为新的起点,同时重置 current_tank = 0 ,让油箱也清空。
  • 如果 totalSave_tank < 0 ,返回 -1 ,否则返回 starting station。
public static int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int[] save = new int[n];
        int totalSave_tank = 0;
        int current_tank = 0;
        int start = 0;
        for (int i = 0; i < n; i++) {
            save[i] = gas[i] - cost[i];
            totalSave_tank += save[i];
            current_tank += save[i];
            if(current_tank < 0){
                start = i + 1;
                current_tank = 0;
            }
        }
        return totalSave_tank >= 0 ? start : -1;
    }

用最少数量的箭引爆气球

  • 为了最大程度地利用每一箭,射出的每一箭必定是某个气球的end坐标;所以我们将坐标(start, end)按照end从小到大进行排序;
  • 记录前一次射出的箭的坐标,在遍历每一个气球的坐标时,判断气球的开始坐标是否小于等于前一次射出的箭的坐标,如果是,那么此气球已经被前一箭射爆了;如果不是那么需要在这个气球的end位置射出一箭;
    public int findMinArrowShots(int[][] points) {
        Arrays.sort(points, new Comparator() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1];
            }
        });
        int count = 0;
        long lastEnd = (long) Integer.MIN_VALUE - 1;
        for(int i=0; i

整数拆分


贪心算法

public int integerBreak(int n) {
        if(n <= 3){
            return n-1;
        }
        int a = n / 3;
        int b = n % 3;
        if(b == 0){
            return (int) Math.pow(3, a);
        }else if(b == 1){
            return (int) Math.pow(3, a-1) * 4;
        }else {
            return (int) Math.pow(3, a) * 2;
        }
    }

有效的括号字符串


贪心算法

  • 只需关注 左括号至少几个、至多几个,维护对应的两个变量,min和max
    1)遇左括号则至多/至少都增加
    2)遇右则至多/至少都减少
    若至多 max 都不够抵右括号,则 return false
    3)遇 '*' 则可能减少/不变/增多,即 min--; max++;
    若至少 min 小于 0 ,说明'*'不能当做右括号处理;所以min减至0,则不可以再减;
    public boolean checkValidString(String s) {
        // possible range
        int min = 0, max = 0; // 维护当前左括号的数量范围: [min, max]
        for (char c : s.toCharArray()) {
            if (c == '(') {
                ++min;
                ++max;
            } else if (c == ')') {
                if (min > 0) min--;
                if (max-- == 0) {
                    return false;// 左括号不够
                }
            } else {
                if (min > 0) {
                    min--; // 可作为右括号,抵消
                }
                ++max; // 可作为左括号
            }
        }
        return min == 0;
    }

双栈:利用一个临时栈

  • 遇到'('或'*',入栈;遇到')'将栈中最上面的'('号先出栈,此时要利用一个临时栈,用于存储'('号上面可能存在的'*'号,然后在'('出栈之后,需要将临时栈中的'*'号重新入栈。如果栈中没有'('号,则出栈一个'*'号;如果栈为空,则返回false;
  • 待遍历完所有符号之后,至少所有的右括号都匹配到了左括号;此时只需要分析栈中剩余的'('和'*'符号,需要为'('号匹配')'括号,即用'*'号代替;依次出栈元素,此时仍然要使用到临时栈,用于存储出栈的'*'号;当出栈'('号时,临时栈同时出栈一个'*'号匹配,如果临时栈为空,返回false;直到所有元素顺利出栈,即成功。
    public static boolean checkValidString(String s) {
        Stack stack = new Stack<>();
        Stack temp = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '(' || c == '*') {
                stack.push(c);
            } else {
                if (stack.isEmpty()) {
                    return false;
                }
                while (!stack.isEmpty() && stack.peek() == '*') {
                    temp.push(stack.pop());
                }
                if (!stack.isEmpty()) {
                    stack.pop();
                } else {
                    temp.pop();
                }
                while (!temp.isEmpty()) {
                    stack.push(temp.pop());
                }
            }
        }
        while (!stack.isEmpty()) {
            char c = stack.pop();
            if (c == '*') {
                temp.push(c);
            } else if (!temp.isEmpty()) {
                temp.pop();
            } else {
                return false;
            }
        }
        return true;
    }

括号的分数

贪心
事实上,我们可以发现,只有 () 会对字符串 S 贡献实质的分数,其它的括号只会将分数乘二或者将分数累加。因此,我们可以找到每一个 () 对应的深度 x,那么答案就是 的累加和。

    public int scoreOfParentheses(String S) {
        int ans = 0, bal = 0;
        for (int i = 0; i < S.length(); ++i) {
            if (S.charAt(i) == '(') {
                bal++;
            } else {
                bal--;
                if (S.charAt(i-1) == '(')
                    ans += 1 << bal;
            }
        }
        return ans;
    }

你可能感兴趣的:(贪心算法、矩阵排序)