LeetCode 周赛上分之旅 #46 经典二分答案与质因数分解

LeetCode 周赛 363

T1. 计算 K 置位下标对应元素的和(Easy)

  • 标签:位运算

T2. 让所有学生保持开心的分组方法数(Medium)

  • 标签:贪心、排序、计数排序

T3. 最大合金数(Medium)

  • 标签:二分查找

T4. 完全子集的最大元素和(Hard)

  • 标签:数学、质因素分解、散列表

T1. 计算 K 置位下标对应元素的和(Easy)

https://leetcode.cn/problems/sum-of-values-at-indices-with-k-set-bits/description/

题解(模拟)

简单模拟题。

写法 1:

class Solution {
    fun sumIndicesWithKSetBits(nums: List<Int>, k: Int): Int {
        var ret = 0
        for (i in nums.indices) {
            if (Integer.bitCount(i) == k) ret += nums[i]
        }
        return ret
    }
}

写法 2:

class Solution {
    fun sumIndicesWithKSetBits(nums: List<Int>, k: Int): Int {
        return nums.indices.fold(0) { acc, it -> if (Integer.bitCount(it) == k) acc + nums[it] else acc}
    }
}

复杂度分析:

  • 时间复杂度:O(n) Java Integer#bitCount 的时间复杂度是 O(1)
  • 空间复杂度:O(1) 仅使用常数级别空间。

T2. 让所有学生保持开心的分组方法数(Medium)

https://leetcode.cn/problems/happy-students/description/

问题分析

思考选哪个:

  • 条件 1: 如果选中的学生 nums[i] 越小,那么越容易满足选中人数 > nums[i]
  • 条件 2: 如果未选中的学生 nums[i] 越大,那么越容易满足选中人数 < nums[i]

因此,在合法的选择方案中,应该优先选择越小的学生。

题解(排序 + 贪心)

先对数组排序,再枚举分割点验证条件 1 与条件 2:

6,0,3,3,6,7,2,7 

排序 => 

0,2,3,3,6,6,7,7
|0,2,3,3,6,6,7,7
0|2,3,3,6,6,7,7
0,2|3,3,6,6,7,7
0,2,3|3,6,6,7,7

对于分割点 i 的要求是:

  • 条件 1:i + 1 > nums[i],利用有序性质只需要判断已选列表的最大值 nums[i]
  • 条件 2:i + 1 < nums[i + 1],利用有序性质只需要判断未选列表的最小值 nums[i + 1]
  • 最后针对全选和都不选的情况特殊判断。
class Solution {
    fun countWays(nums: MutableList<Int>): Int {
        nums.sort()
        val n = nums.size
        var ret = 0
        // 都不选
        if (nums[0] > 0) ret += 1
        // 都选
        if (nums[n - 1] < n) ret += 1
        // 选一部分
        for (i in 0 until n - 1) {
            if (nums[i] < i + 1 && nums[i + 1] > i + 1) ret += 1
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:O(nlgn) 瓶颈在排序;
  • 空间复杂度:O(lgn) 排序递归栈空间。

T3. 最大合金数(Medium)

https://leetcode.cn/problems/maximum-number-of-alloys/description/

问题分析

初步分析:

  • 问题目标: 求在预算限制下最大可以制造的合金数量;
  • 关键信息: 所有合金都需要由同一台机器制造,这样难度就降低很多了。

容易发现原问题的单调性:

  • 如果合金数 x 可以制造,那么合金数 x - 1 一定可以制造;
  • 如果合金数 x 不可制造,那么合金数 x + 1 一定不可制造。

因此,可以用二分答案来解决问题:

  • 合金数的下界:0
  • 合金数的上界:2 * 10^8,即金钱和初始金属的最大值;

现在需要思考的问题是: 「如何验证合金数 x 可以构造」

由于所有合金都需要由同一台机器制造,判断很简单,只需要先计算目标数量需要的每种金属的初始金属数是否足够,不足则花金钱购买。如果花费超过限制则不可制造。

题解(二分答案)

基于以上问下,我们枚举机器,使用二分查找寻找可以制造的合金数的上界:

class Solution {
    fun maxNumberOfAlloys(n: Int, k: Int, limit: Int, composition: List<List<Int>>, stock: List<Int>, cost: List<Int>): Int {
        var ret = 0
        // 枚举方案
        for (com in composition) {
            fun check(num: Int): Boolean {
                // 计算需要的金属原料
                var money = 0L
                for (i in 0 until n) {
                    // 原料不足,需要购入
                    money += max(0L, 1L * com[i] * num - stock[i]) * cost[i] // 注意整型溢出
                    if (money > limit.toLong()) return false
                }
                return true
            }

            var left = 0
            var right = 2*1e8.toInt()
            while (left < right) {
                val mid = (left + right + 1) ushr 1
                if (check(mid)) {
                    left = mid
                } else {
                    right = mid - 1
                }
            }
            ret = max(ret, left)
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:O(k·n·lgU) 其中 k 为机器数,n 为金属种类,U 为二分上界;
  • 空间复杂度:O(1) 除结果数组外仅使用常量级别空间。

T4. 完全子集的最大元素和(Hard)

https://leetcode.cn/problems/maximum-element-sum-of-a-complete-subset-of-indices/description/

问题分析

初步分析:

  • 问题目标: 求解满足条件的目标子集的元素最大和;
  • 目标子集: 子集元素的下标两两相乘的乘积是完全平方数,允许仅包含一个元素的子集;

观察测试用例 2:

  • 对于下标 1 和下标 4:两个完全平方数的乘积自然是完全平方数;
  • 对于下标 2 和下标 828 都包含质因子 22 的平方自然是完全平方数;

由此得出结论:

  • 核心思路: 我们消除每个下标中的完全平方数因子,再对剩余的特征分组,能够构造目标子集的方案有且只能出现在相同的特征分组中(否则,子集中一定存在两两相乘不是完全平方数的情况)。
{2 | 6} x 需要相同的因子
{6 | 6} ok 

思考实现:

  • 预处理: 预处理覆盖所有测试用例下标的特征值
  • 质因素分解: 有 2 种基础算法:

朴素算法:枚举 [2, \sqrt{n}] 将出现次数为奇数的质因子记录到特征值中,时间复杂度是 O(\sqrt{n})

private val U = 1e4.toInt()
private val core = IntArray(U + 1)
init {
    for (num in 1 .. U) {
        // 质因素分解
        var prime = 2
        var x = num
        var key = 1
        while (prime * prime <= x) {
            var cnt = 0
            while (x % prime == 0) {
                x /= prime
                cnt ++
            }
            if (cnt % 2 == 1) key *= prime // 记录特征值
            prime ++
        }
        if (x > 1) key *= x // 记录特征值
        core[num] = key
    }
}

筛法:枚举质因子,将记录质因子的整数倍的特征值。

private val U = 1e4.toInt()
private val core = IntArray(U + 1) { 1 }
private val isMark = BooleanArray(U + 1)
init {
    // 质因素分解
    for (i in 2 .. U) {
        // 检查是否为质数,这里不需要调用 isPrime() 函数判断是否质数,因为它没被小于它的数标记过,那么一定不是合数
        if (isMark[i]) continue
        for (num in i .. U step i) {
            isMark[num] = true
            var x = num
            var cnt = 0
            while (x % i == 0) {
                x /= i
                cnt ++
            }
            if (cnt % 2 != 0) core[num] *= i // 记录特征值
        }
    }
}

题解一(质因素分解 + 分桶)

组合以上技巧,枚举下标做质因数分解,将数值累加到分桶中,最后返回最大分桶元素和。

class Solution {

    companion object {
        private val U = 1e4.toInt()
        private val core = IntArray(U + 1)
        init {
            for (num in 1 .. U) {
                // 质因素分解
                var prime = 2
                var x = num
                var key = 1
                while (prime * prime <= x) {
                    var cnt = 0
                    while (x % prime == 0) {
                        x /= prime
                        cnt ++
                    }
                    if (cnt % 2 == 1) key *= prime // 记录特征值
                    prime ++
                }
                if (x > 1) key *= x // 记录特征值
                core[num] = key
            }
        }
    }

    fun maximumSum(nums: List<Int>): Long {
        var ret = 0L
        val buckets = HashMap<Int, Long>()
        for (i in 1 .. nums.size) {
            val key = core[i]
            buckets[key] = buckets.getOrDefault(key, 0) + nums[i - 1]
            ret = max(ret, buckets[key]!!)
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:预处理时间为 O(U\sqrt{U}),单次测试用例时间为 O(n)
  • 空间复杂度:O(U) 预处理空间,单次测试用例空间比较松的上界为 O(n)

题解二(找规律)

题解一的时间复杂度瓶颈在之因素分解。

继续挖掘数据特征,我们观察同一个分桶内的数据规律:

假设分桶中的最小值为 x,那么将分桶的所有元素排序后必然是以下序列的子序列:{x, 4 * x, 9 * x, 16 * x…},由此发现规律:我们可以枚举分桶的最小值,再依次乘以完全平方数序列来计算,既可以快速定位分桶中的元素,而不需要预处理质因数分解。

那怎么度量此算法的时间复杂度呢?

显然,该算法一个比较松上界是 O(n·C),其中 C 为数据范围内的完全平方数个数,C = 100。严格证明参考羊神题解,该算法线性时间复杂度 O(n)

class Solution {

    companion object {
        // 预处理完全平方数序列
        private val s = LinkedList<Int>()
        init {
            for (i in 1 .. 100) {
                s.add(i * i)
            }
        }
    }

    fun maximumSum(nums: List<Int>): Long {
        val n = nums.size
        var ret = 0L
        // 枚举分桶最小值
        for (i in 1 .. n) {
            var sum = 0L
            for (k in s) {
                if (k * i > n) break
                sum += nums[k * i - 1]
            }
            ret = max(ret, sum)
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:O(n) 线性算法;
  • 空间复杂度:O(C) 预处理完全平方数序列空间,可以优化。

推荐阅读

LeetCode 上分之旅系列往期回顾:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容