954. 二倍数对数组 : 「逐个构造」&「成组构造」&「拓扑排序」

简介: 954. 二倍数对数组 : 「逐个构造」&「成组构造」&「拓扑排序」

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 954. 二倍数对数组 ,难度为 中等


Tag : 「优先队列」、「堆」、「构造」、「哈希表」、「拓扑排序」


给定一个长度为偶数的整数数组 arr,只有对 arr 进行重组后可以满足 “对于每个0 <= i < len(arr) / 20<=i<len(arr)/2,都有 arr[2 * i + 1] = 2 * arr[2 * i]arr[2i+1]=2arr[2i]” 时,返回 true;否则,返回 false


示例 1:


输入:arr = [3,1,3,6]
输出:false
复制代码


示例 2:


输入:arr = [2,1,2,6]
输出:false
复制代码


示例 3:


输入:arr = [4,-2,2,-4]
输出:true
解释:可以用 [-2,-4] 和 [2,4] 这两组组成 [-2,-4,2,4] 或是 [2,4,-2,-4]
复制代码


提示:


  • 0 <= arr.length <= 3 * 10^40<=arr.length<=3104
  • arr.length 是偶数
  • -10^5 <= arr[i] <= 10^5105<=arr[i]<=105


逐个构造 + 优先队列



整理一下题意:是否能对 arr 进行重组,使得每一个奇数位置的值均是前一个位置的值的两倍,即凑成 \frac{n}{2}2n 组形如 (x, 2 * x)(x,2x) 的数对。


对于一个任意的有理数而言,对其乘 22 仅会改变数值的大小,而不会改变其方向(正负性质)。


因此如果我们每次都拿最接近 00 的值作为起点,整个构造过程就是唯一确定的。


具体的,我们可以借助优先队列(堆)来实现,构造一个以与 00 值距离作为基准的小根堆。每次从堆中取出元素 xx,根据当前元素 xx 是否被「预定」过进行分情况讨论:


  • 当前值 xx 没有被预定过,说明 xx 必然是数对中的「绝对值」的较小值,此时给 xx 并预定一个 x * 2x2,即对 x * 2x2 的预定次数加一;
  • 当前值 xx 已经被预定过,说明 xx 和此前的某个数 \frac{x}{2}2x 组成过数对,对 xx 的预定次数减一。


当且仅当构成过程结束后,所有数的「预定」次数为 00 时,arr 可以凑成 \frac{n}{2}2n 组形如 (x, 2 * x)(x,2x) 的数对。


一些细节:由于 arr[i]arr[i] 的数值范围为 [-10^5, 10^5][105,105],同时存在乘 22 操作,因此我们需要对计算结果进行 2 * 10^52105 的偏移操作,确保其为正数。


代码:


class Solution {
    static int N = 100010, M = N * 2;
    static int[] cnts = new int[M * 2];
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(cnts, 0);
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->Math.abs(a)-Math.abs(b));
        for (int i : arr) q.add(i);
        while (!q.isEmpty()) {
            int x = q.poll(), t = x * 2;
            if (cnts[x + M] != 0 && --cnts[x + M] >= 0) continue;
            cnts[t + M]++;
        }
        for (int i = 0; i < M * 2; i++) {
            if (cnts[i] != 0) return false;
        }
        return true;
    }
}
复制代码


  • 时间复杂度:令 nnarr 长度,mmarr[i]arr[i] 的值域范围,起始将所有数值放入优先队列(堆)的复杂度为 O(n\log{n})O(nlogn),从优先队列(堆)中取出并构造复杂度为 O(n\log{n})O(nlogn),检查构造是否成功复杂度为 O(m)O(m),整体复杂度为 O(n\log{n} + m)O(nlogn+m)
  • 空间复杂度:O(n + m)O(n+m)


成组构造 + 排序 + 预处理剪枝



上述解法中,我们不可避免的对 arr 进行一遍完成的尝试构造,并且在尝试构造结束后再进行一次性的合法性检查。


事实上,如果 arr 能够凑成 \frac{n}{2}2n 组形如 (x, 2 * x)(x,2x) 的数对,并且对于某个 xx 可能会出现多次,我们可以统计 arr[i]arr[i] 的数量,并根据绝对值大小进行排序,进行成组构造:cnts[x]cnts[x]xx 消耗 cnts[x]cnts[x]2 * x2x


同时由于我们提前预处理了每个 arr[i]arr[i] 的出现次数,我们可以提前知道是否有 cnts[x]cnts[x]2 * x2xxx 成组,从而可以边构造边检查合法性。


代码:


class Solution {
    static int N = 100010, 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;
    }
}
复制代码


  • 时间复杂度:统计 arr[i]arr[i] 的出现次数以及对 arr[i]arr[i] 去重的复杂度为 O(n)O(n),对去重数组进行排序的复杂度为 O(n\log{n})O(nlogn),验证是否合法复杂度为 O(n)O(n)。整体复杂度为 O(n\log{n})O(nlogn)
  • 空间复杂度:O(n + m)O(n+m)


成组构造 + 拓扑排序



对于上述两种解法,要么利用「优先队列」要么利用「排序」,目的都是为了找到数对中的「绝对值较小」的那一位,然后开始往后构造。


事实上,我们可以利用任意 xx 只可能与 \frac{x}{2}2x 或者 2 * x2x 组成数对来进行建图,通过对图跑拓扑序来验证是否能够凑成 \frac{n}{2}2n 组形如 (x, 2 * x)(x,2x) 的数对。


不了解「拓扑排序」的同学可以看前置 🧀:图论拓扑排序入门,里面通过图解演示了何为拓扑序,以及通过「反证法」证明了为何有向无环图能够能够进行拓扑排序。


特别的,我们需要特殊处理 arr[i] = 0arr[i]=0 的情况,由于 00 只能与本身组成数对,为了避免自环,我们需要跳过 arr[i] = 0arr[i]=0 的点,同时特判 arr[i] = 0arr[i]=0 的出现数量为奇数时,返回无解。


和解法二一样,先对 arr[i]arr[i] 进行数量统计以及去重预处理(跳过 00),然后对去重数组 list 中出现的数值 xx 进行分情况讨论:


  • xx 为奇数,由于 \frac{x}{2}2x 不为整数,因此 xx 只能作为数对中绝对值较小的那个(即 xx 入度为 00),加入队列;
  • xx 为偶数,首先令 xx 的入度 in[x] = cnts[\frac{x}{2}]in[x]=cnts[2x],代表有 cnts[\frac{x}{2}]cnts[2x]\frac{x}{2}2x 与其对应。当 in[x] = 0in[x]=0 时,说明没有 \frac{x}{2}2x 与其成对,此时 xx 只能作为数对中绝对值较小的那个(即 xx 入度为 00),加入队列。


跑一遍拓扑排序,假设当前出队值为 tt,此时需要消耗掉 cnts[t]cnts[t]t * 2t2 与其形成数对(即 cnts[t * 2] -= cnts[t]cnts[t2]=cnts[t] ),同时 t * 2t2 的入度也要更新(即可 in[t * 2] -= cnts[t]in[t2]=cnts[t] ),若 in[t * 2] = 0in[t2]=0 且此时 cnts[t * 2] > 0cnts[t2]>0,将 t * 2t2 进行入队。同时由于我们明确减少了 t * 2t2 的数量,因此需要同步更新 t * 4t4 的入度,同理,当 t * 4t4 的入度 in[t * 4] = 0in[t4]=0,同时 cnts[t * 4] > 0cnts[t4]>0 时,需要将 t * 4t4 进行入队。


代码:


class Solution {
    static int N = 100010, M = 2 * N;
    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) list.add(i);
        }
        if (cnts[M] % 2 != 0) return false;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i : list) {
            if (i % 2 == 0) {
                in[i + M] = cnts[i / 2 + M];
                if (in[i + M] == 0) d.addLast(i);
            } else { 
                d.addLast(i);
            }
        }
        while (!d.isEmpty()) {
            int t = d.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) d.addLast(t * 2);
            in[t * 4 + M] -= cnts[t + M];
            if (in[t * 4 + M] == 0 && cnts[t * 4 + M] != 0) d.addLast(t * 4);
        }
        return true;
    }
}
复制代码


  • 时间复杂度:统计数量和入度的复杂度为 O(n)O(n);跑拓扑序验证的复杂度为 O(n)O(n)。整体复杂度为 O(n)O(n)
  • 空间复杂度:O(n + m)O(n+m)


相关阅读



图论拓扑排序入门:通过图解演示了何为拓扑序,以及通过「反证法」证明了为何有向无环图能够能够进行拓扑排序 🎉 🎉


最后



这是我们「刷穿 LeetCode」系列文章的第 No.954 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
算法
最小生成树算法:Prim算法
在图论中,最小生成树(Minimum Spanning Tree,简称MST)是一种常用的算法问题。最小生成树是指在一个加权连通图中选取边的子集,使得所有顶点都被覆盖,并且边的总权值最小。
561 0
|
JavaScript 前端开发
nodejs实现解析chm文件列表,无需转换为PDF文件格式,在线预览chm文件以及目录,不依赖任何网页端插件
nodejs实现解析chm文件列表,无需转换为PDF文件格式,在线预览chm文件以及目录,不依赖任何网页端插件
|
11月前
|
域名解析 存储 安全
HTTP【网络】
HTTP协议格式、HTTP的方法 、HTTP的状态码、HTTP常见的Header
1254 6
HTTP【网络】
|
8月前
|
搜索推荐 算法 数据挖掘
探讨淘宝商品 API 接口:运用及收益
在电商蓬勃发展的今天,淘宝作为国内巨头,拥有海量商品数据和庞大用户群体。淘宝商品API接口为开发者、电商从业者和数据分析师提供了丰富的商品信息,如详情、价格、销量、评价等,助力电商平台搭建、推荐系统优化、市场调研及竞品分析,显著提升业务收益。本文将深入探讨该接口的运用方法与价值,并结合实际代码示例,帮助读者更好地理解和应用。
217 6
|
11月前
|
安全 编译器 异构计算
在CPU设计中,为了提高能效比并减少能源消耗,采用了多种节能技术
【10月更文挑战第2天】在CPU设计中,为了提高能效比并减少能源消耗,采用了多种节能技术
302 5
|
安全 物联网 网络安全
智能家居安全:保护您的智能设备免受网络威胁##
在现代科技迅速发展的时代,智能家居设备已经深入人们的生活。然而,随着便利性增加的同时,这些设备也面临着越来越多的网络安全威胁。本文将探讨如何保护您的智能家居设备免受黑客攻击,并介绍几种常见的安全措施和最佳实践。通过采取适当的预防措施,您可以确保家庭网络的安全性,保护个人隐私和数据不受侵害。 ##
|
12月前
|
机器学习/深度学习 人工智能 数据可视化
# Python的一个非常cool的库Gradio
# Python的一个非常cool的库Gradio
354 0
|
编解码 人工智能
PixArt-Σ:华为最新文生图模型,支持4K高清图像生成
【5月更文挑战第18天】华为发布PixArt-Σ模型,一款基于DiT架构的4K图像生成器,提升图像质量和文本对齐度。模型采用“弱到强训练”,以少量参数生成优质图像。引入高质量数据和高效标记压缩方法,实现超高分辨率图像生成。实验显示,PixArt-Σ在遵循复杂文本提示和图像质量上表现优异,与顶尖T2I模型相当。然而,计算资源需求大及处理复杂场景能力有限仍是待解问题。[链接](https://arxiv.org/pdf/2403.04692.pdf)
339 1
|
机器学习/深度学习 算法 数据挖掘
如何评估模型性能以进行模型选择?
【5月更文挑战第4天】如何评估模型性能以进行模型选择?
378 5
|
C语言
C语言之宏详解(超级详细!)
C语言之宏详解(超级详细!)