LeetCode第239场周赛

周赛地址:https://leetcode-cn.com/contest/weekly-contest-239

第一题:到目标元素的最小距离

遍历一遍数组,判断 n u m s [ i ] = = t a r g e t nums[i]==target nums[i]==target的,求一下距离 M a t h . a b s ( s t a r t − i ) Math.abs(start - i) Math.abs(starti),维护一个距离最小值,最后即为答案。

class Solution {
    public int getMinDistance(int[] nums, int target, int start) {
        int answer = Integer.MAX_VALUE, length = nums.length;
        for (int i = 0; i < length; i++) {
            if (nums[i] == target && Math.abs(start - i) < answer) {
                answer = Math.abs(start - i);
            }
        }
        return answer;
    }
}

第二题:将字符串拆分为递减的连续值

给定一个字符串,字符串的长度最长为 20 20 20,将字符串拆分成递减的连续数值,考虑字符串长度为 20 20 20的时候,假设字符串满足要求,那么第一个满足条件的字符串最长是10位,这个数据是爆int的,所以要用long来存储。
一旦第一个字符串确定了,为了使字符串可以拆分成递减的连续值,那么后面的数字也就可以确定了,就是需要判断一下后面的值是否满足。
在查找第一个数的时候,需要去除前导0(如果有的话),然后尝试确定第一个数字,当第一个数字确定了之后,后面的数字就可以确定了,将子字符串转成long类型的数,判定是否满足降序连续。
判定是一个递归的过程, 如果可以走到最后一个字符串,说明可以满足题意,否则, 当前的第一个数是不满足的。

class Solution {
    boolean answer = false;

    public boolean splitString(String s) {
        int length = s.length(), i = 0;
        // 去除前导0
        for (; i < length; i++) {
            if (s.charAt(i) != '0') {
                break;
            }
        }
        // 尝试确定第一个数,一旦确定了第一个数,后面的数字也就可以确定了,从而判定是否满足降序
        // length最大是20,假设字符串可以分隔成两个数字,那么每个数字最多是10位,比如"98765432119876543210"
        for (int j = i + 1; j < Math.min(length, i + 11); j++) {
            dfs(s, Long.parseLong(s.substring(i, j)), j, length);
            // 有一个满足条件,剩下的就不用再尝试了
            if (answer) {
                return true;
            }
        }
        return answer;
    }

    private void dfs(String s, long previous, int position, int length) {
        // 递归截止条件,已经走到最后了,可以判断当前字符串符合要求
        if (position == length) {
            answer = true;
            return;
        }
        for (int i = position; i < length; i++) {
            long current = Long.parseLong(s.substring(position, i + 1));
            // current已经大于previous了,就没必要继续循环了,向后循环只会越来越大
            if (current >= previous) {
                break;
            }
            // 满足previous - current == 1,继续向后判断
            if (previous - current == 1) {
                dfs(s, current, i + 1, length);
            }
        }
    }
}

第三题:邻位交换的最小次数

用C++解题的话,可以直接调用STL中的next_permutation()函数,直接求得下一个排列。这里用Java实现next_permutation()和prev_permutation()。
调用k次next_permutation()函数即可得到k次后的排列。有了初始排列和目标排列,通过交换相邻字符来实现初始排列到目标排列。
考虑目标序列中各个字符互不相同,建立字符和下标的对应关系。假设目标排列是bdca,初试排列是cdba,对目标排列做如下映射:b→0,d→1,c→2,a→3,此时初试排列用映射表示为"2103",将初始排列化为目标排列,也就是对初试排列进行排序,使得对应映射呈现依次递增。
也可以这么来理解,将目标序列看做一个标准,将初始看做一个需要排序的序列,通过交换相邻元素的方式,使得初始序列对应的值满足递增规律。
如果有重复的元素,对于重复的元素,按照从左向右的顺序填充映射,比如目标序列abacda,a对应0,2,5这3个下标映射,在替换初始排列dcabaa中的重复字符的时候,按照从左到右的顺序依次替换,即"430125"。
计算交换的最小次数,即计算初始排列变成递增序列冒泡排序交换的次数,也是初始排列对应映射的逆序对数。

class Solution {
    public int getMinSwaps(String num, int k) {
        int length = num.length(), answer = 0;
        int[] nums = new int[length];
        for (int i = 0; i < length; i++) {
            nums[i] = num.charAt(i) - '0';
        }
        while (k-- > 0) {
            nextPermutation(nums);
        }
        // 通过交换相邻元素,求将num变为nums的最小交换次数
        // 假设nums=[bdca],num="cdba",对nums做如下映射(先考虑没有重复数字的情况):
        // b→0,d→1,c→2,a→3,此时num用映射表示为"2103"
        // 求num到nums的距离,就是求"2103"变为"0123"需要交换相邻元素的次数
        // 计算交换相邻元素的次数,就是升序冒泡排序的交换次数,也是"2103"的逆序对数
        // 如果有重复的,比如nums=[abacda],a对应0,2,5这3个下标映射
        // 在替换num="dcabaa"中的重复字符的时候,按照从左到右的顺序依次替换,即"430125"
        Map<Integer, LinkedList<Integer>> map = new HashMap<>();
        // 建立映射关系
        for (int i = 0; i < length; i++) {
            map.computeIfAbsent(nums[i], l -> new LinkedList<>()).add(i);
        }
        // 对num,按照映射关系还原,转存到nums里
        for (int i = 0; i < length; i++) {
            LinkedList<Integer> linkedList = map.get(num.charAt(i) - '0');
            nums[i] = linkedList.pollFirst();
        }
        // 对nums求逆序数
        for (int i = 0; i < length; i++) {
            for (int j = i + 1; j < length; j++) {
                if (nums[i] > nums[j]) {
                    answer++;
                }
            }
        }
        return answer;
    }

	private void prevPermutation(int[] nums) {
        int length = nums.length, i = length - 1, j = length - 1;
        for (; i > 0; i--) {
            // 从右向左找第一个nums[i - 1] > nums[i]的i
            if (nums[i - 1] > nums[i]) {
                break;
            }
        }
        // i > 0说明有上一个排列
        if (i > 0) {
            for (; j >= i; j--) {
                // 从右向左找第一个nums[i - 1] > nums[j]的j
                if (nums[i - 1] > nums[j]) {
                    break;
                }
            }
            swap(i - 1, j, nums);
        }
        j = length - 1;
        // 此时[i, length - 1]是升序排列,将其改为降序
        while (i < j) {
            swap(i++, j--, nums);
        }
    }

    private void nextPermutation(int[] nums) {
        int length = nums.length, i = length - 1, j = length - 1;
        for (; i > 0; i--) {
            // 从右向左找第一个nums[i - 1] < nums[i]的i
            if (nums[i - 1] < nums[i]) {
                break;
            }
        }
        // i > 0说明有下一个排列
        if (i > 0) {
            for (; j >= i; j--) {
                // 从右向左找第一个nums[i - 1] < nums[j]的j
                if (nums[i - 1] < nums[j]) {
                    break;
                }
            }
            swap(i - 1, j, nums);
        }
        j = length - 1;
        // 此时[i, length - 1]是降序排列,将其改为升序
        while (i < j) {
            swap(i++, j--, nums);
        }
    }

    private void swap(int i, int j, int[] array) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

第四题:包含每个查询的最小区间

有3种方法:离线思维、并查集、线段树。

离线思维

因为已知所有的queries和所有的intervals,所以可以将queries和intervals进行排序,从而降低求解的时间复杂度,具体怎么排,后面解释。
根据题意,区间长度定义为 r i g h t i − l e f t i + 1 right_{i}-left_{i}+1 rightilefti+1,并且区间长度越短,优先级越高。再看queries,对于某个queries[i],假设我们可以找到k个符合要求的intervals,但是我们只关心区间长度最短的intervals的区间长度,对于某个queries[i],考虑一种数据结构,这个数据结构可以在一个Collection里快速找到区间长度最短的一个interval,因此,可以想到优先队列,可以在 O ( 1 ) O(1) O(1)时间找到区间长度最小的interval。
对于所有的intervals,我们考虑将它按照 l e f t i left_{i} lefti进行排序,对于某个queries[i]来说,当遍历排序后的intervals的时候,如果intervals[j]不满足queries[i]了,那么intervals[j+1]一定也不满足queries[i],后面的也就都不用遍历了,这是intervals要排序的原因。
对于所有的queries,如果每个queries[i],都循环一遍intervals,整体的复杂度还是很高的,所以考虑对queries进行排序,让相邻的queries能避免重复遍历intervals,这样复杂度就降下来了了。
对于每个queries,我们遍历intervals,根据intervals[i]的 l e f t i left_{i} lefti确定是否将intervals[i]加入优先队列,根据intervals[i]的 r i g h t i right_{i} righti确定是否将intervals[i]移出优先队列,这样优先队列只需要遍历一遍即可,在遍历queries的同时,intervals[i]也在不断的进出优先队列,但是对于intervals,这是单向的。

class Solution {
    public int[] minInterval(int[][] intervals, int[] queries) {
        int queryLength = queries.length, intervalLength = intervals.length;
        int[] answer = new int[queryLength];
        // 优先队列按照区间长度(o[1] - o[0] + 1)排序
        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(o -> o[1] - o[0] + 1));
        // 将intervals排序,对于有相同left的intervals,在priorityQueue里会做处理,保证取出的是区间长度最短的
        // 所以,这里碰到intervals[i][0]相同的时候,可以不用对intervals[i][1]再排序了
        Arrays.sort(intervals, Comparator.comparingInt(interval -> interval[0]));
        // 将queries转存到query,并记录原来的位置,query[i][0]记录queries[i]的值,query[i][1]记录下标i
        int[][] query = new int[queryLength][2];
        for (int i = 0; i < queryLength; i++) {
            query[i] = new int[]{queries[i], i};
        }
        // 对query按照query[i][0]进行排序
        Arrays.sort(query, Comparator.comparingInt(q -> q[0]));
        int index = 0;
        for (int i = 0; i < queryLength; i++) {
            // 如果intervals的左边界满足query[i][0],加入优先队列
            while (index < intervalLength && intervals[index][0] <= query[i][0]) {
                priorityQueue.offer(intervals[index++]);
            }
            // 如果优先队列中的intervals的右边界不满足query[i][0],从优先队列里移除
            while (!priorityQueue.isEmpty() && priorityQueue.peek()[1] < query[i][0]) {
                priorityQueue.poll();
            }
            if (priorityQueue.isEmpty()) {
                answer[query[i][1]] = -1;
            } else {
                answer[query[i][1]] = priorityQueue.peek()[1] - priorityQueue.peek()[0] + 1;
            }
        }
        return answer;
    }
}

并查集(未使用离散化)

直接去想并查集,其实思维还是很跳跃的,甚至想不到并查集可以做。看了y总的讲解,大概明白了怎么回事,记录一下。
先看下疯狂的馒头这道题。
根据题意,可以知道,从时间维度上来看,馒头最后的颜色由最后一次染色决定,可以倒序时间维度来考虑,先考虑最后一次染色,再考虑倒数第二次染色,……,再考虑第一次染色。这么考虑的目的是:如果倒数第二次染色的区间是[1,8],倒数第一次染色的区间是[2,6],那么倒数第二次染色只需要处理[1,1]和[7,8]即可,不要再对[2,6]重复染色了,那么就需要一种方法,在扫描到[2,6]的时候,直接跳到7位置,这里就用到并查集了。
初始化的时候: f [ i ] = i f[i]=i f[i]=i。当对 i i i染色完成,应该更新 f [ i ] f[i] f[i]为下一个可以染色的位置,这就跳过了中间已经染色的区间,即 f [ i ] = f i n d ( i + 1 ) f[i]=find(i+1) f[i]=find(i+1)。所以,在开数组的时候,要多开一位,否则会数组越界。
疯狂的馒头的染色优先级由染色时间决定,时间越靠后,优先级越高,回到这个题目上,这个题目的染色(确定queries[i]对应的intervals)优先级由intervals[i]的区间长度决定,区间长度越小,优先级越高。因此,需要对intervals按照区间长度进行排序,把问题抽象成给区间染色,通过染色和更新下一个可以染色的位置,染色完成,针对queries,就可以知道对应的intervals的区间长度的值是多少了。
这里需要注意的一点是: f i n d ( x ) find(x) find(x)方法不能用递归写法,会爆栈,需要采用循环的写法。

class Solution {
    int[] array;// 染色数组
    int[] f;// 并查集数组

    private int find(int x) {
        while (x != f[x]) {
            f[x] = f[f[x]];
            x = f[x];
        }
        return x;
		// return x == f[x] ? x : (f[x] = find(f[x]));// 会爆栈
    }

    public int[] minInterval(int[][] intervals, int[] queries) {
        int queryLength = queries.length;
        int[] answer = new int[queryLength];
        array = new int[10000002];
        f = new int[10000002];
        // 按照区间长度递增排序,区间长度短的优先级高
        Arrays.sort(intervals, Comparator.comparingInt(interval -> interval[1] - interval[0] + 1));
        // 初始的时候,f[i]=i
        for (int i = 1; i < 10000002; i++) {
            f[i] = i;
        }
        for (int[] interval : intervals) {
            int left = interval[0], right = interval[1];
            left = find(left);// 定位到能染色的地方
            while (left <= right) {
                array[left] = interval[1] - interval[0] + 1;// 染色(确定区间长度)
                f[left] = find(left + 1);// 更新f
                left = find(left);// 跳到下一个位置
            }
        }
        for (int i = 0; i < queryLength; i++) {
            answer[i] = array[queries[i]] == 0 ? -1 : array[queries[i]];
        }
        return answer;
    }
}

并查集(使用了离散化)

未使用离散化的时候,内存消耗爆炸,应该在OOM的边缘徘徊吧。使用离散化,可以把一个大区间映射到一个小区间,并且保持相对大小关系不变,从而节省内存。
离散化需要对数组去重+排序,此时就可以确定一个最终数组,要想确定某个数离散化后对应的值,需要一个 l o w e r _ b o u n d ( a r r a y , t a r g e t ) lower\_bound(array,target) lower_bound(array,target)函数。其他变化不大,对于需要做离散化的地方,套一层 l o w e r _ b o u n d ( ) lower\_bound() lower_bound()即可,此时内存消耗和时间消耗都变少了。

class Solution {
    int[] array;// 染色数组
    int[] f;// 并查集数组
    int[] distinct;// 存放排序+去重之后的值,用于二分查找

    private int find(int x) {
        while (x != f[x]) {
            f[x] = f[f[x]];
            x = f[x];
        }
        return x;
    }

    /**
     * 查找第一个大于等于x的值所在下标(二分写法)
     * 没有找到返回-1(在离散化这里不存在返回-1的情况)
     * 用于确定离散之化后,target值的rank
     */
    public static int lower_bound(int[] array, int target) {
        int l = 0, r = array.length - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (array[mid] > target) {
                r = mid - 1;
            } else if (array[mid] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        if (l == array.length) {
            return -1;
        }
        return l;
    }

    /**
     * 对intervals和queries做离散化
     * 将去重+排序后的值放入distinct数组,返回数组的长度
     * distinct数组用于二分查询,确定离散化后值对应的位置
     */
    private int discretization(int[][] intervals, int[] queries) {
        // TreeSet用于去重和排序
        Set<Integer> set = new TreeSet<>();
        // 将intervals放入set
        for (int[] interval : intervals) {
            set.add(interval[0]);
            set.add(interval[1]);
        }
        // 将queries放入set
        for (int query : queries) {
            set.add(query);
        }
        int size = set.size(), index = 0;
        distinct = new int[size];
        // 把去重和排序后的值放入distinct数组,用于二分查找
        for (int i : set) {
            distinct[index++] = i;
        }
        return size;
    }

    public int[] minInterval(int[][] intervals, int[] queries) {
        int queryLength = queries.length;
        int[] answer = new int[queryLength];
        int length = discretization(intervals, queries);
        array = new int[length + 1];
        f = new int[length + 1];
        Arrays.fill(array, -1);// 如果array[i]没有被染色,标记为-1
        // 并查集初始化:f[i]=i
        for (int i = 1; i < length + 1; i++) {
            f[i] = i;
        }
        // 按照区间长度递增排序,区间长度短的优先级高
        Arrays.sort(intervals, Comparator.comparingInt(interval -> interval[1] - interval[0] + 1));
        // 依次遍历所有区间
        for (int[] interval : intervals) {
            int left = lower_bound(distinct, interval[0]), right = lower_bound(distinct, interval[1]); // left和right是离散化后的值
            left = find(left);// 定位到能染色的地方
            while (left <= right) {
                array[left] = interval[1] - interval[0] + 1;// 染色(记录left位置的对应的区间长度)
                f[left] = find(left + 1);// 更新f:根据left + 1查找下一个能染色的值,赋给f[left]
                left = find(left);// 跳到下一个位置
            }
        }
        for (int i = 0; i < queryLength; i++) {
            int index = lower_bound(distinct, queries[i]);// 离散化后的值
            answer[i] = array[index];
        }
        return answer;
    }
}

线段树

你可能感兴趣的:(LeetCode周赛)