LeetCode 周赛上分之旅 #41 结合离散化的线性 DP 问题

周赛 359

T1. 判别首字母缩略词(Easy)

  • 标签:模拟、字符串

T2. k-avoiding 数组的最小总和(Medium)

  • 标签:散列表、贪心、数学

T3. 销售利润最大化(Medium)

  • 标签:排序、桶排序、双指针、线性 DP、离散化

T4. 找出最长等值子数组(Medium)

  • 标签:分桶、双指针

T1. 判别首字母缩略词(Easy)

https://leetcode.cn/problems/check-if-a-string-is-an-acronym-of-words/

题解(模拟)

class Solution {
public:
    bool isAcronym(vector<string>& words, string s) {
        if (words.size() != s.length()) return false;
        for (int i = 0; i < words.size(); i++) {
            if (words[i][0] != s[i]) return false;
        }
        return true;
    }
};

复杂度分析:

  • 时间复杂度:O(n) 线性遍历;
  • 空间复杂度:O(1) 仅使用常量级别空间。

T2. k-avoiding 数组的最小总和(Medium)

https://leetcode.cn/problems/determine-the-minimum-sum-of-a-k-avoiding-array/

题解一(散列表 + 贪心)

从 1 开始从小到大枚举,如果当前元素 cur 与已选列表不冲突,则加入结果中。为了验证是否冲突,我们使用散列表在 O(1) 时间复杂度判断。

class Solution {
    fun minimumSum(n: Int, k: Int): Int {
        val set = HashSet<Int>()
        var sum = 0
        var cur = 1
        repeat(n) {
            while (!set.isEmpty() && set.contains(k - cur)) cur++
            sum += cur
            set.add(cur)
            cur++
        }
        return sum
    }
}

复杂度分析:

  • 时间复杂度:O(n) 线性遍历;
  • 空间复杂度:O(n) 散列表空间。

题解二(数学)

这道题还可以继续挖掘数学规律,我们发现当我们从 1 开始从小到大枚举时,每选择一个数的同时必然会使得另一个数 k - x 不可选。例如:

  • 选择 1,则 k - 1 不可选;
  • 选择 2,则 k - 2 不可选;
  • 选择 k / 2,则 k - k / 2 不可选。

可以发现,最终选择的元素被分为两部分:

  • 小于 k 的部分:选择所有和为 k 的配对中的较小值,即 1、2、3 … k / 2;
  • 大于等于 k 的部分:与其他任意正整数相加都不会等于 k,因此大于等于 k 的数必然可以选择,即 k、k + 1、k + 2、…、k + n - m - 1共 n - m 个数。

我们令 m = min(k / 2, n),使用求和公式可以 O(1) 求出两部分的总和:

  • 小于 k 的部分:m(m + 1)/ 2
  • 大于等于 k 的部分:(n - m) * (2*k + n - m - 1) / 2
class Solution {
    fun minimumSum(n: Int, k: Int): Int {
        val m = Math.min(n, k / 2)
        return m * (m + 1) / 2 + (n - m) * (2 * k + n - m - 1) / 2
    }
}

复杂度分析:

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

T3. 销售利润最大化(Medium)

https://leetcode.cn/problems/maximize-the-profit-as-the-salesman/

问题分析

对于区间 [0, n) 的房子,如果我们选择 [i, j, gold] 的 offer,那么原问题的解就变成 gold + [0, i) + (j, n) 的两个子问题的解;

定义 dp[i] 表示到 [i] 为止可以收获的最大销售利润,则对于第 [i] 间房子有卖和不卖两种选择:

  • 不卖,那么 dp[i] = dp[i - 1]
  • 卖,那么 dp[i] = dp[i - 1] + gold。然而题目的销售 offer 是按照区间销售而不是按照单个房子销售,如果第 i 个房子没有处于 offer 的 end 端点的话,我们是不能卖出的。

因此,我们需要找到枚举 start 端点(从后往前遍历)或枚举 end 端点(从前往后遍历)的方法,并使用转移方程 dp[i] = max(dp[i], dp[start] + gold) 更新答案。

题解一(排序 + 线性 DP + 双指针)

第一种方法,我们先对所有 offer 按照 end 端点排序,并使用 j 指针指向当前终止时间最早的 offer。在动态规划的过程中,当 i 指针与 j 指针的 end 端点重合时,可以尝试更新结果。

class Solution {
    fun maximizeTheProfit(n: Int, offers: List<List<Int>>): Int {
        val m = offers.size
        // 排序
        Collections.sort(offers) { e1, e2 ->
            e1[1] - e2[1]
        }
        var j = 0
        // 线性 DP
        val dp = IntArray(n + 1)
        for (i in 1 .. n) {
            // 不卖
            dp[i] = dp[i - 1]
            // 卖
            while (j < m && i - 1 == offers[j][1]) { // while:可能存在终点重叠的区间
                dp[i] = Math.max(dp[i], dp[offers[j][0]] + offers[j][2])
                ++j
            }
        }
        return dp[n]
    }
}

复杂度分析:

  • 时间复杂度:O(mlgm + n + m) 排序预处理时间为 O(mlgm),动态规划时间为 O(n + m)
  • 空间复杂度:O(n) 排序递归栈空间 + DP 数组空间。

题解二(桶排序 + 线性 DP)

第二种方法,同样是对所有 offer 按照 end 端点排序,但我们使用桶排序优化。

class Solution {
    fun maximizeTheProfit(n: Int, offers: List<List<Int>>): Int {
        // 分桶
        val buckets = Array(n) { LinkedList<IntArray>() }
        for (offer in offers) {
            buckets[offer[1]].add(intArrayOf(offer[0], offer[2]))
        }
        // 线性 DP
        val dp = IntArray(n + 1)
        for (i in 1 .. n) {
            // 不卖
            dp[i] = dp[i - 1]
            // 卖
            for (e in buckets[i - 1]) {
                dp[i] = Math.max(dp[i], dp[e[0]] + e[1])
            }
        }
        return dp[n]
    }
}

复杂度分析:

  • 时间复杂度:O(n + m) 预处理时间为 O(n + m),其中包含 O(n) 时间的数组创建时间,使用散列表可以优化预处理时间到 O(m)。动态规划中求最值部分每个 offer 最多访问 1 次整体时间,因此动态规划的时间复杂度为 O(n + m)
  • 空间复杂度:O(n) DP 数组和分桶数组空间。

题解三(线性 DP + 离散化)

如果 n 的值域非常大的话,以上两种解法的时间和空间可能无法满足。我们发现由于影响题目的关键点仅在与 offer 的 start 端点和 end 端点,而中间空白的点或者被覆盖的点是无关紧要的。

因此,我们可以使用离散化的技巧,将所有 offer 的 start 端点和 end 端点去重后组合成新的坐标轴 points,将在 [0, n) 上的线性 DP 转换为在 [0, m) 上的线性 DP。

class Solution {
    fun maximizeTheProfit(n: Int, offers: List<List<Int>>): Int {
        // 对 start 和 end 离散化
        val pointSet = HashSet<Int>()
        for (offer in offers) {
            pointSet.add(offer[0])
            pointSet.add(offer[1])
        }
        // 排序
        val points = pointSet.toMutableList()
        points.sort()
        // 端点 -> id
        val m = points.size
        val ids = HashMap<Int, Int>()
        for (id in 0 until m) {
            ids[points[id]] = id
        }
        // 分桶
        val buckets = Array(m) { LinkedList<IntArray>() }
        for (offer in offers) {
            val start = offer[0]
            val end = offer[1]
            val gold = offer[2]
            buckets[ids[end]!!].add(intArrayOf(ids[start]!!, gold))
        }
        // 线性 DP
        val dp = IntArray(m + 1)
        for (i in 1 .. m) {
            // 不卖
            dp[i] = dp[i - 1]
            // 卖
            for (e in buckets[i - 1]) {
                dp[i] = Math.max(dp[i], dp[e[0]] + e[1])
            }
        }
        return dp[m]
    }
}

复杂度分析:

  • 时间复杂度:O(mlgm + m) 预处理时间为 O(mlgm),瓶颈在排序,线性 DP 时间为 O(m)
  • 空间复杂度:O(m) 离散化节点空间、分桶空间和线性 DP 空间都是 O(m) 时间复杂度。

相似题目:


T4. 找出最长等值子数组(Medium)

https://leetcode.cn/problems/find-the-longest-equal-subarray/

题解(分桶 + 同向双指针)

这道题比 T3 还稍微简单一些。

  • 分桶: 我们知道目标子数组一定发生在元素值相等的位置,因此我们可以先把所有元素下标按元素值分桶,再使用滑动窗口来寻找分桶内删除次数不超过 k 可以构造的最大连续子数组。
  • 滑动窗口: 使用同向双指针维护目标滑动窗口,当向右扩展窗口右端点时增加删除量 delete,如果 delete 大于 k 则需要缩小左端点,过程中记录连续相等子数组的最大长度。
class Solution {
    fun longestEqualSubarray(nums: List<Int>, k: Int): Int {
        val n = nums.size
        // 分桶
        val buckets = Array(n + 1) { ArrayList<Int>() }
        for ((i, num) in nums.withIndex()) {
            buckets[num].add(i)
        }
        // 同向双指针
        var ret = 1
        for (bucket in buckets) {
            val m = bucket.size
            var delete = 0
            var i = 0
            for (j in 1 until m) {
                // 增加删除量
                delete += bucket[j] - bucket[j - 1] - 1
                while (delete > k) {
                    // 恢复删除量
                    delete -= bucket[i + 1] - bucket[i] - 1
                    // 收缩左指针
                    i++
                }
                ret = Math.max(ret, j - i + 1)
            }
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:O(n) 分桶时间为 O(n),所有分桶的同向双指针时间总和为 O(n)
  • 空间复杂度:O(n) 分桶空间。

推荐阅读

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

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

推荐阅读更多精彩内容