Java&C++题解与拓展——leetcode954.二倍数对数组(父子对)【拓扑排序,priority_queue排序,queue学习】

147 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

每日一题做题记录,参考官方和三叶的题解

题目要求

在这里插入图片描述

理解

一个找父子对的过程,每个孩子都有爸就true,缺爸缺孩子就false

思路:逐一判断with优先队列

遍历每一个值,找它在数组里的爸爸。 遍历顺序:从最接近00的值开始,即对绝对值排序。 采用优先队列实现以与00的距离为基准的小根堆,开始遍历:

  • 当前值xx没有儿子,则只能当儿子,给它找一个爸x2x*2,对son[x2]son[x*2]加一;
  • 当前值xx有儿子,说明可以和之前的数x2\frac{x}{2}组成父子对,则son[x]son[x]减一。

遍历完成后,所有值的儿子数量均为00,则原数组可以构成n2\frac{n}{2}个父子对。

【由于同时存在负数和乘二操作,需对结果进行偏移操作,即给每个结果加上21052*10^5

Java

class Solution {
    static int N = 100001;
    static int M = N * 2; //矫正数据范围
    static int[] son = new int[M * 2]; //儿子数量
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(son, 0);
        PriorityQueue<Integer> pq = new PriorityQueue<>((a,b) -> Math.abs(a) - Math.abs(b));
        for(int i : arr)
            pq.add(i);
        while(!pq.isEmpty()) {
            int x = pq.poll(), t = x * 2;
            if(son[x + M] != 0 && --son[x + M] >= 0)
                continue;
            son[t + M]++;
        }
        for(int i = 0; i < M * 2; i++)
            if(son[i] != 0)
                return false;
        return true;
    }
}
  • 时间复杂度:O(nlogn+m)O(n\log n + m)nnarrarr长度,mmarr[i]arr[i]的值域范围,将所有值放入优先队列和取出的复杂度均为O(nlogn)O(n\log n),检查是否存在合法结果的复杂度为O(m)O(m)
  • 空间复杂度:O(m+n)O(m+n)

C++

【在绝对值排序的地方卡了一会……】

class Solution {
public:
    bool canReorderDoubled(vector<int>& arr) {
        int N = 100001;
        int M = N * 2; //矫正数据范围
        int son[M * 2]; //儿子数量

        memset(son, 0, sizeof(son));
        auto cmp = [](int a, int b) { return abs(a) > abs(b); };
        priority_queue<int, vector<int>, decltype(cmp)> pq(cmp);
        for(int i : arr)
            pq.push(i);

        while(!pq.empty()) {
            int x = pq.top(), t = x * 2;
            cout << x <<endl;
            pq.pop();
            if(son[x + M] != 0 && --son[x + M] >= 0)
                continue;
            son[t + M]++;
        }
        for(int i = 0; i < M * 2; i++)
            if(son[i] != 0)
                return false;
        return true;
    }
};
  • 时间复杂度:O(nlogn+m)O(n\log n + m)
  • 空间复杂度:O(m+n)O(m+n)

priority_queue自定义排序

思路:成对构造

要组成父子对,所以直接成对得消除等量的父子。所以预处理一下所有的值出现的次数cntscnts并进行去重,然后按顺序一边构造父子对一边检查合法性。

Java

class Solution {
    static int N = 100001;
    static int M = N * 2; //矫正数据范围
    static int[] cnts = new int[M * 2];
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(cnts, 0);
        List<Integer> list = new ArrayList<>();
        for(int i : arr)
            if(++cnts[i + M] == 1)
                list.add(i);
        Collections.sort(list, (a,b) -> Math.abs(a) - Math.abs(b)); //排序
        for(int i : list) {
            if(cnts[i * 2 + M] < cnts[i + M]) //不够组对
                return false;
            cnts[i * 2 + M] -= cnts[i + M]; //减去可组对项
        } 
        return true;
    }
}
  • 时间复杂度:O(nlogn)O(n\log n),统计出现次数并去重的复杂度为O(n)O(n),对去重数组排序复杂度为O(nlogn)O(n\log n),构造并检查合法性复杂度为O(n)O(n)
  • 空间复杂度:O(m+n)O(m+n)

C++

class Solution {
public:
    bool canReorderDoubled(vector<int>& arr) {
        int N = 100001;
        int M = N * 2; //矫正数据范围
        int cnts[M * 2];

        memset(cnts, 0, sizeof(cnts));
        vector<int> list;
        for(int i : arr)
            if(++cnts[i + M] == 1)
                list.push_back(i);
        sort(list.begin(), list.end(), [](int a, int b) { return abs(a) < abs(b); });

        for(int i : list) {
            if(cnts[i * 2 + M] < cnts[i + M]) //不够组对
                return false;
            cnts[i * 2 + M] -= cnts[i + M]; //减去可组对项
        }
        return true;
    }
};
  • 时间复杂度:O(nlogn)O(n\log n)
  • 空间复杂度:O(m+n)O(m+n)

思路:拓扑排序

由于一个值只能和爸爸2x2*x或儿子x2\frac{x}{2}组对,那么可以基于此建一个由儿子指向爸爸的图,通过拓扑排序来验证结果。那么进行数量统计和去重之后:

  • 当前值xx可以有儿子(为偶数),那令其入度为儿子数in[x]=cnts[x2]in[x]=cnts[\frac{x}{2}]。入度为00时,说明没有儿子与之成对,那它只能做儿子,加入儿子队列;
  • 当前值xx不会有儿子(为奇数),那么它入度必为00,加入儿子队列。

过程中要特殊处理只能和自己组对的00,不把它放进图里,避免成环。

执行拓扑排序过程,设当前出队值为tt,此时需消耗掉cnt[t]cnt[t]个爸爸t2t*2与之配对,同时更新爸爸的入度,若其入度为00且还有剩余,那么说明它没有儿子可以配对了,将该爸爸加入儿子队列。由于消耗了该爸爸数量,那需要同步更新爷爷t4t*4的入度,那同样爷爷若是入度也消耗为00了,则也加入儿子队列。

Java

class Solution {
    static int N = 100001;
    static int M = N * 2; //矫正数据范围
    static int[] cnts = new int[M * 2], in = new int[M * 2];
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(cnts, 0);
        Arrays.fill(in, 0);
        List<Integer> list = new ArrayList<>();
        for(int i : arr)
            if(++cnts[i + M] == 1 && i != 0) //0会成环
                list.add(i);
        if(cnts[M] % 2 != 0) //0和自己无法成对
            return false;
        
        Deque<Integer> son = new ArrayDeque<>(); //儿子队伍
        for(int i : list) {
            if(i % 2 == 0) { //偶数
                in[i + M] = cnts[i / 2 + M]; //入度为儿子数
                if(in[i + M] == 0)
                    son.addLast(i);
            }
            else
                son.addLast(i);
        }
        while(!son.isEmpty()) {
            int t = son.pollFirst();
            if(cnts[t * 2 + M] < cnts[t + M]) //儿子比爸多
                return false;
            cnts[t * 2 + M] -= cnts[t + M]; //减掉儿子数量
            in[t * 2 + M] -= cnts[t + M];
            if(in[t * 2 + M] == 0 && cnts[t * 2 + M] != 0)
                son.addLast(t * 2);
            in[t * 4 + M] -= cnts[t + M];
            if(in[t * 4 + M] == 0 && cnts[t * 4 + M] != 0)
                son.addLast(t * 4);
        }
        return true;
    }
}
  • 时间复杂度:O(n)O(n),统计cntscntsinin的复杂度是O(n)O(n),拓扑序验证复杂度也是O(n)O(n)
  • 空间复杂度:O(n+m)O(n+m)

拓扑排序

  • 学习参考链接
  • 对有向图构造拓扑序列的过程,有向无环图才能进行拓扑排序。
  • 删除入度为0的点,并对其他点更新。

C++

class Solution {
public:
    bool canReorderDoubled(vector<int>& arr) {
        int N = 100001;
        int M = N * 2; //矫正数据范围
        int cnts[M * 2], in[M * 2];
        memset(cnts, 0, sizeof(cnts));
        memset(in, 0, sizeof(in));
        vector<int> list;
        for(int i : arr)
            if(++cnts[i + M] == 1 && i != 0) //0会成环
                list.push_back(i);
        if(cnts[M] % 2 != 0) //0和自己无法成对
            return false;
        
        queue<int> son;
        for(int i : list) {
            if(i % 2 == 0) { //偶数
                in[i + M] = cnts[i / 2 + M]; //入度为儿子数
                if(in[i + M] == 0)
                    son.push(i);
            }
            else
                son.push(i);
        }
        while(!son.empty()) {
            int t = son.front();
            son.pop();
            if(cnts[t * 2 + M] < cnts[t + M]) //儿子比爸多
                return false;
            cnts[t * 2 + M] -= cnts[t + M]; //减掉儿子数量
            in[t * 2 + M] -= cnts[t + M];
            if(in[t * 2 + M] == 0 && cnts[t * 2 + M] != 0)
                son.push(t * 2);
            in[t * 4 + M] -= cnts[t + M];
            if(in[t * 4 + M] == 0 && cnts[t * 4 + M] != 0)
                son.push(t * 4);
        }
        return true;
    }
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(m+n)O(m+n)

queue

思路四:双指针

【思来想去还是觉得这方法能用,花了好半天给他撕出来了,感觉是时空复杂度最佳的方法。】
排序后注意负数和正数的爸爸和儿子相反,负数在找儿子,正数在找爸爸。

Java

class Solution {
    public boolean canReorderDoubled(int[] arr) {
        Arrays.sort(arr);
        int n = arr.length, len = 0, zlen = 0;
        for(int i = 0; i < n; i++) {
            if(arr[i] >= 0) { //统计负数数量
                len = i;
                break;
            }
        }        
        if((len & 1) != 0) //奇数个
            return false;

        boolean[] used = new boolean[n];
        return check(arr, used, 0, len) && check(arr, used, len, n);
    }

    public static boolean check(int[] arr, boolean[] used, int sta, int end) {
        int l = sta, r = l + 1;
        while(l < end) {
            if(used[l]) { //已组过对
                l++;
                continue;
            }
            if(l >= r)
                r = l + 1;
            
            //负数l为爸爸,正数r为爸爸
            while(r < end && ((arr[r] < 0 && 2 * arr[r] < arr[l]) || (arr[r] > 0 && arr[r] < 2 * arr[l])))
                r++; //向右滑动直至可以匹配
            if(r == end || ((arr[r] < 0 && 2 * arr[r] > arr[l]) || (arr[r] > 0 && arr[r] > 2 * arr[l])))
                return false;

            used[r] = true; //标记组过对
            l++;
            r++;
        }
        return true;
    }
}
  • 时间复杂度:O(nlogn)O(n\log n)
  • 空间复杂度:O(n)O(n)

C++

class Solution {
public:
    bool canReorderDoubled(vector<int>& arr) {
        sort(arr.begin(), arr.end());
        int n = arr.size(), len = 0;
        for(int i = 0; i < n; i++) {
            if(arr[i] >= 0) { //统计负数数量
                len = i;
                break;
            }
        }        
        if((len & 1) != 0) //奇数个
            return false;

        vector<bool> used;
        used.resize(arr.size());
        return check(arr, used, 0, len) && check(arr, used, len, n);
    }

    static bool check(vector<int>& arr, vector<bool>& used, int sta, int end) {
        int l = sta, r = l + 1;
        while(l < end) {
            if(used[l]) { //已组过对
                l++;
                continue;
            }
            if(l >= r)
                r = l + 1;
            
            //负数l为爸爸,正数r为爸爸
            while(r < end && ((arr[r] < 0 && 2 * arr[r] < arr[l]) || (arr[r] > 0 && arr[r] < 2 * arr[l])))
                r++; //向右滑动直至可以匹配
            if(r == end || ((arr[r] < 0 && 2 * arr[r] > arr[l]) || (arr[r] > 0 && arr[r] > 2 * arr[l])))
                return false;

            used[r] = true; //标记组过对
            l++;
            r++;
        }
        return true;
    }
};
  • 时间复杂度:O(nlogn)O(n\log n)
  • 空间复杂度:O(n)O(n)

总结

这道题解决方法好多,除了文中的还看到有暴力哈希表统计(低配思路一)、贪心向上遍历、匹配模式等,但是今天事有点多,就不一一整理考虑了。
今天的收获是认真看了下拓扑排序。
但是太忙了,C++没认真搞好,改天回来给他结束掉。 熬夜搞好√


欢迎指正与讨论!