相信科学系列,两种 100% 解法背后的分析证明|Java 刷题打卡

简介: 相信科学系列,两种 100% 解法背后的分析证明|Java 刷题打卡

题目描述



这是 LeetCode 上的765. 情侣牵手,难度为 Hard


NNN 对情侣坐在连续排列的 2N2N2N 个座位上,想要牵到对方的手。


计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。


一次交换可选择任意两人,让他们站起来交换座位。


人和座位用 0002N−12N-12N1 的整数表示,情侣们按顺序编号,第一对是 (0,1)(0, 1)(0,1),第二对是 (2,3)(2, 3)(2,3),以此类推,最后一对是 (2N−2,2N−1)(2N-2, 2N-1)(2N2,2N1)


这些情侣的初始座位 row[i]row[i]row[i] 是由最初始坐在第 iii 个座位上的人决定的。


示例 1:


输入: row = [0, 2, 1, 3]
输出: 1
解释: 我们只需要交换row[1]和row[2]的位置即可。
复制代码


示例 2:


输入: row = [3, 2, 0, 1]
输出: 0
解释: 无需交换座位,所有的情侣都已经可以手牵手了。
复制代码


说明:


  • len(row)len(row)len(row) 是偶数且数值在 [4,60][4, 60][4,60] 范围内。
  • 可以保证 row 是序列 0...len(row)−10...len(row)-10...len(row)1 的一个全排列。


并查集



首先,我们总是以「情侣对」为单位进行设想:


  1. 当有两对情侣相互坐错了位置,ta们两对之间形成了一个环。需要进行一次交换,使得每对情侣独立(相互牵手)
  2. 如果三对情侣相互坐错了位置,ta们三对之间形成了一个环,需要进行两次交换,使得每对情侣独立(相互牵手)
  3. 如果四对情侣相互坐错了位置,ta们四对之间形成了一个环,需要进行三次交换,使得每对情侣独立(相互牵手)


也就是说,如果我们有 kkk 对情侣形成了错误环,需要交换 k−1k - 1k1 次才能让情侣牵手。


于是问题转化成 n/2n / 2n/2 对情侣中,有多少个这样的环。


可以直接使用「并查集」来做。


由于 0和1配对、2和3配对 ... 因此互为情侣的两个编号除以 2 对应同一个数字,可直接作为它们的「情侣组」编号。


代码:


class Solution {
    int[] p = new int[70];
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    public int minSwapsCouples(int[] row) {
        int n = row.length, m = n / 2;
        for (int i = 0; i < m; i++) p[i] = i;
        for (int i = 0; i < n; i += 2) {
            union(row[i] / 2, row[i + 1] / 2);
        }
        int cnt = 0;
        for (int i = 0; i < m; i++) {
            if (i == find(i)) cnt++;
        }
        return m - cnt;
    }
}
复制代码


  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)


贪心算法



还是以「情侣对」为单位进行分析:


由于题目保证有解,我们也可以从前往后(每两格作为一步)处理,对于某一个位置而言,如果下一个位置不是应该出现的情侣的话。


则对下一个位置进行交换。


同时为了方便我们找到某个值的下标,需要先对 rowrowrow 进行预处理(可以使用哈希表或数组)。


代码:


class Solution {
    public int minSwapsCouples(int[] row) {
        int n = row.length;
        int ans = 0;
        int[] cache = new int[n];
        for (int i = 0; i < n; i++) cache[row[i]] = i;
        for (int i = 0; i < n - 1; i += 2) {
            int a = row[i], b = a ^ 1;
            if (row[i + 1] != b) {
                int src = i + 1, tar = cache[b];
                cache[row[tar]] = src;
                cache[row[src]] = tar;
                swap(row, src, tar);
                ans++;
            }
        }
        return ans;
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}
复制代码


  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)


证明



我们这样的做法本质是什么?


其实相当于,当我处理到第 k 个位置的时候,前面的 k - 1 个位置的情侣已经牵手成功了。我接下来怎么处理,能够使得总花销最低。


分两种情况讨论:


a. 现在处理第 k 个位置,使其牵手成功:


那么我要使得第 k 个位置的情侣也牵手成功,那么必然是保留第 k 个位置的情侣中其中一位,再进行修改,这样的成本是最小的(因为只需要交换一次)。


而且由于前面的情侣已经牵手成功了,因此交换的情侣必然在 k 位置的后面。


然后我们再考虑交换左边或者右边对最终结果的影响。


分两种情况来讨论:


  1. 与第 k 个位置的匹配的两个情侣不在同一个位置上:这时候无论交换左边还是右边,后面需要调整的「情侣对数量」都是一样。假设处理第 k 个位置前需要调整的数量为 n 的话,处理完第 k 个位置(交换左边或是右边),需要调整的「情侣对数量」都为 n - 1


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


  1. 与第 k 个位置的匹配的两个情侣在同一个位置上:这时候无论交换左边还是右边,后面需要调整的「情侣对数量」都是一样。假设处理第 k 个位置前需要调整的数量为 n 的话,处理完第 k 个位置(交换左边或是右边),需要调整的「情侣对数量」都为 n - 2


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


因此对于第 k 个位置而言,交换左边还是右边,并不会影响后续需要调整的「情侣对数量」。


b. 现在先不处理第 k 个位置,等到后面的情侣处理的时候「顺便」处理第 k 位置:

由于我们最终都是要所有位置的情侣牵手,而且每一个数值对应的情侣数值是唯一确定的。


因此我们这个等“后面”的位置处理,其实就是等与第 k 个位置互为情侣的位置处理(对应上图的就是我们是在等 【0 x】和【8 y】或者【0 8】这些位置被处理)。


由于被处理都是同一批的联通位置,因此和「a. 现在处理第 k 个位置」的分析结果是一样的。


不失一般性的,我们可以将这个分析推广到第一个位置,其实就已经是符合「当我处理到第 k 个位置的时候,前面的 k - 1 个位置的情侣已经牵手成功了」的定义了。


综上所述,我们只需要确保从前往后处理,并且每次处理都保留第 k 个位置的其中一位,无论保留的左边还是右边都能得到最优解。


最后



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


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


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


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

相关文章
|
8月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
335 1
|
8月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
364 2
|
9月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
10月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
319 4
|
10月前
|
机器学习/深度学习 安全 Java
Java 大视界 -- Java 大数据在智能金融反洗钱监测与交易异常分析中的应用(224)
本文探讨 Java 大数据在智能金融反洗钱监测与交易异常分析中的应用,介绍其在数据处理、机器学习建模、实战案例及安全隐私等方面的技术方案与挑战,展现 Java 在金融风控中的强大能力。
|
11月前
|
存储 Java 大数据
Java 大视界 -- Java 大数据在智能家居能源消耗模式分析与节能策略制定中的应用(198)
简介:本文探讨Java大数据技术在智能家居能源消耗分析与节能策略中的应用。通过数据采集、存储与智能分析,构建能耗模型,挖掘用电模式,制定设备调度策略,实现节能目标。结合实际案例,展示Java大数据在智能家居节能中的关键作用。
|
12月前
|
数据采集 搜索推荐 算法
Java 大视界 -- Java 大数据在智能教育学习社区用户互动分析与社区活跃度提升中的应用(274)
本文系统阐述 Java 大数据技术在智能教育学习社区中的深度应用,涵盖数据采集架构、核心分析算法、活跃度提升策略及前沿技术探索,为教育数字化转型提供完整技术解决方案。
|
Java 数据库连接 API
互联网大厂校招 JAVA 工程师笔试题解析及常见考点分析
本文深入解析互联网大厂校招Java工程师笔试题,涵盖基础知识(数据类型、流程控制)、面向对象编程(类与对象、继承与多态)、数据结构与算法(数组、链表、排序算法)、异常处理、集合框架、Java 8+新特性(Lambda表达式、Stream API)、多线程与并发、IO与NIO、数据库操作(JDBC、ORM框架MyBatis)及Spring框架基础(IoC、DI、AOP)。通过技术方案讲解与实例演示,助你掌握核心考点,提升解题能力。
485 2
|
传感器 分布式计算 安全
Java 大视界 -- Java 大数据在智能安防入侵检测系统中的多源数据融合与分析技术(171)
本文围绕 Java 大数据在智能安防入侵检测系统中的应用展开,剖析系统现状与挑战,阐释多源数据融合及分析技术,结合案例与代码给出实操方案,提升入侵检测效能。
|
缓存 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(3-1):并发共享问题的解决与分析
活锁:多个线程相互影响对方退出同步代码块的条件而导致线程一直运行的情况。例如,线程1的退出条件是count=5,而线程2和线程3在其代码块中不断地是count进行自增自减的操作,导致线程1永远运行。内存一致性问题:由于JIT即时编译器对缓存的优化和指令重排等造成的内存可见性和有序性问题,可以通过synchronized,volatile,并发集合类等机制来解决。这里的线程安全是指,多个线程调用它们同一个实例的方法时,是线程安全的,但仅仅能保证当前调用的方法是线程安全的,不同方法之间是线程不安全的。
224 0