Leetcode【406、763、861、1094】

问题描述:【Sort】406. Queue Reconstruction by Height
解题思路:

这道题是假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对 (h, k) 表示,其中 h 是这个人的身高,k 是排在这个人前面且身高大于或等于 h 的人数。编写一个算法来重建这个队列。

这道题很明显先要进行排序,关键是按照 h 还是 k 来排序呢?

  • 先考虑一种简单的情况:队列中没有身高相同的人,如 people = [[6,1], [7,0], [5,2]],很容易看出按照身高 h 降序排列,而 k 表示的就是人所在队列的位置,即重构后的队列为 [[7,0], [6,1], [5,2]]。
  • 再考虑可能有身高相同的情况,如 people = [[6,1], [7,0], [5,2], [7,1]],我们按照上述思路先对身高 h 降序排列,相同的身高按照 k 进行升序排列(因为不难看出相同身高,k 小的肯定排在前面),得到 [[7,0], [7,1], [6,1], [5,2]],还是按照上述思路,我们依次将 h 插入到 k 位置:7 插入到位置 0 得到 [[7,0]];7 插入到位置 1 得到 [[7,0], [7,1]];6 插入到位置 1 得到 [[7,0], [6,1], [7,1]](可以发现 [6,1] 的插入不会对 [7,1] 造成影响,因为 6 本来就比 7 小,前面放一个小的数字不影响 [7,1] 中的 k 值);5 插入到位置 2 即可得到重构后的队列为 [[7,0], [6,1], [5,2], [7,1]](同理,5 的插入不会对后面的 6 和 7 的 k 造成影响)。
  • 最后考虑例子中的 people = [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]],按照 h 升序 k 降序,得到 [[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]],依次将 h 插入到 k 位置上,即可得到重构后的队列为 [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]],理由同上。

总之,一句话,就是先按照身高 h 降序排列,如果有相同身高,按照 k 值升序排列。然后,对于排列后的每一个 (hi, ki),依次把 (hi, ki) 插入到 ki 位置上就是最后的答案。用 Python 可以一句代码实现排序任务:
people.sort(key=lambda x: (-x[0], x[1])) # 按照第一维降序,第一维相同按第二维升序

这是一种贪婪的策略,即先考虑高个子,身高相同的考虑位置在前的。时间复杂度来自于排序的开销 O(n*logn)。

Python3 实现:
class Solution:
    def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
        people.sort(key=lambda x: (-x[0], x[1]))  # 按照第一维降序,第一维相同按第二维升序
        ans = []
        for p in people:
            ans.insert(p[1], p)  # 将(hi,ki)插入到ki位置上
        return ans

问题描述:【Greedy】763. Partition Labels
解题思路:

这道题是给一个字符串,把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。

这道题其实可以从给的例子中就能发现解题技巧。S = "ababcbacadefegdehijhklij" 最终划分成三段,"ababcbaca", "defegde", "hijhklij",可以发现每个字母处于哪一段取决于它最后一个相同字母的位置。如字母 'a' 的最后一个字母在 S[8] 处,'e' 的最后一个字母在 S[15] 处等等。

  • 因此,我们先对 S 进行遍历,得到每一个字母的最后一个位置,并把它保存到字典中。
  • 然后,我们遍历 S 中的每一个字母。第一段 "ababcbaca",保存 a 的最后一个位置 pos,b 和 c 的最后一个位置都没有超过 pos 且遍历到了 a 的最后一个位置 pos,那么就划分出了一个段;第二段 "defegde",保存 d 的最后一个位置 pos,e 的最后一个位置超过了 d,就更新 pos 为 e 的最后一个位置;后面的 f、g 没有超过 pos 且遍历到 e 的最后一个位置,那么就划分出了一个段;第三段 "hijhklij",保存 h 的最后一个位置 pos,i 的最后一个位置超过 pos,就更新 pos 为 i 的最后一个位置;j 的最后一个位置也超过 pos,就更新 pos 为 j 的最后一个位置;后面的 k、l 没有超过 pos 且遍历到了最后 j 的最后一个位置,那么就划分出了一个段。

这是一种贪婪的策略,每次考虑最后一个字符所在的位置。时间复杂度为 O(n),空间复杂度为 O(26),即保存 26 个字符所用的空间。

Python3 实现:
class Solution:
    def partitionLabels(self, S: str) -> List[int]:
        dic = dict() 
        for i in range(len(S)):
            dic[S[i]] = i
        ans = []
        ch, pos, cnt = "", -1, 0
        for i in range(len(S)):
            cnt += 1
            if ch != S[i] and dic[S[i]] > pos:  # 更新最后一个字符的位置 pos
                ch, pos = S[i], dic[S[i]]
            if i == pos:  # 遍历到最后一个字符,划分出一个段
                ans.append(cnt)
                ch, pos, cnt = "", -1, 0
        return ans

问题描述:【Greedy】861. Score After Flipping Matrix
解题思路:

这道题是给一个 m*n 的01矩阵,通过多次翻转行或列(0变成1、1变成0),最后将该矩阵的每一行都按照二进制数来解释,矩阵的得分就是这些数字的总和,返回尽可能高的分数。

方法1(时间复杂度 O((mn)^2)):

很明显,我们要获得尽可能高的分数,第一列要保证全 1。所以,先通过行变换把第一列为 0 的那一行进行翻转。因为保证了第一列全为 1,后面就不能进行行变换了,不然第一列有 1 又会变成 0,所以之后只能进行列变换。

对于第二列之后的每一列 j,有翻转 j 和不翻转 j 两种情况,分别计算两种情况下的分数,并更新最大分数。这样时间复杂度为 O((mn)^4),由于 n <= 20,是可以 AC 的。

Python3 实现:

class Solution:
    def matrixScore(self, A: List[List[int]]) -> int:
        
        def calculate():  # 计算得分
            score = 0
            for p in range(m):
                base = 1
                for q in range(n-1, -1, -1):
                    score += A[p][q] * base
                    base *= 2
            return score
            
        m = len(A)
        n = len(A[0])
        for i in range(m):  # 将第一列改为全1
            if A[i][0] == 0:
                for j in range(n):
                    if A[i][j] == 0: A[i][j] = 1
                    else: A[i][j] = 0
        max_score = calculate()  # 初始分数
        for j in range(1, n):  # 翻转第j列
            for i in range(m):
                if A[i][j] == 0: A[i][j] = 1
                else: A[i][j] = 0   
            change_score = calculate()  # 计算翻转第j列的得分
            if change_score > max_score:
                max_score = change_score
            else:  # 不翻转第j列,还原
                for i in range(m):
                    if A[i][j] == 0: A[i][j] = 1
                    else: A[i][j] = 0
        return max_score

方法2(时间复杂度 O(n^2)):

实际上,我们对于第二列之后的每一列 j,没必要计算翻转 j 和不翻转 j 的得分,而是只需要统计第 j 列中 0 与 1 的个数即可。如果 0 的个数超过 1,就进行翻转,这样的话最后的分数肯定比不翻转要高。如果 0 和 1 的个数相同,那么翻不翻转得分是一样的(因为第 j 列中 1 对结果的贡献是 m*(2^(n-j-1)),翻转后 1 变成 0,但 0 也变成了 1,对结果的贡献还是那么多)。

这样,我们只需要统计第二列后的每一列 0 与 1 的个数,累加该列对总和的贡献 max(cnt0, cnt1)*(2^(n-j-1)),除了第一列其他列不用真正地翻转,时间复杂度就可以降为 O(mn)。

Python3 实现:

class Solution:
    def matrixScore(self, A: List[List[int]]) -> int:
        m = len(A)
        n = len(A[0])
        for i in range(m):  # 将第一列改为全1
            if A[i][0] == 0:
                for j in range(n):
                    if A[i][j] == 0: A[i][j] = 1
                    else: A[i][j] = 0
                        
        score = m * (2 ** (n-1))  # 第一列对该位的贡献
        for j in range(1, n):  # 统计每一列0和1的个数
            cnt = [0, 0]
            for i in range(m):
                if A[i][j] == 0: cnt[0] += 1
                else: cnt[1] += 1
            score += (max(cnt) * (2 ** (n - j - 1)))  # 最多的0/1对该位的贡献
        return score

问题描述:【Greedy、DP、DP2】1094. Car Pooling
解题思路:

拼车问题。给一个数组和容量,数组每一项包括人数、上车时间、下车时间,判断是否可以在不超过容量的情况下顺利接送所有乘客(乘客遵循先下后上的原则)

方法1(Greedy,时间复杂度 O(n^2)):

这道题有点像课程表安排,首先先按照上车时间对数组从小到大排序,并用一个变量 cur 记录当前车上的人数。然后对数组的每一项 trips[i],依循先下后上原则,先判断 trips[i] 的上车时间是否大于等于之前项的下车时间,如果是,标记之前的项为已经结束,同时减少 cur;都判断完成后,再把 trips[i] 的人数加到 cur 上,并和 capacity 比较。

由于对于每一个 trips[i],还要和前面的 i 项比较,这样时间复杂度为 O(n^2)。

Python3 实现:

class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        trips.sort(key=lambda x: x[1])  # 按照上车时间从小到大排序
        end = [False] * len(trips)  # 标记哪个时间段已经结束
        cur = 0
        for i in range(len(trips)):
            for j in range(i): # 先下
                if end[j] == False and trips[i][1] >= trips[j][2]:  # 当前上车时间大于等于前面的下车时间
                    end[j] = True
                    cur -= trips[j][0]  # 减去人数
            cur += trips[i][0]  # 后上
            if cur > capacity:
                return False
        return True

print(Solution().carPooling([[8,2,3],[4,1,3],[1,3,6],[8,4,6],[4,4,8]], 12))  # False

方法2(DP,时间复杂度 O(n^2)):

因为发现 0 <= trips[i][1] < trips[i][2] <= 1000,所以可以使用动态规划的思想,建立一个 dp[1001] 的数组,其中 dp[i] 表示 i 时刻车上的人数。因此我们对于每一个时间段 [begin, end],让 dp[i] 累加每个时刻上的人数。最后,dp 中的最大值不大于 capacity,即满足。

因为对于每一项 [cnt, begin, end],dp[i] 都要累加 [begin, cnt] 中每一时刻的 cnt,如果 begin 和 end 间隔很大,这样时间复杂度最坏还是 O(n^2)。

Python3 实现:

class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        dp = [0] * 1001
        for (cnt, begin, end) in trips:
            for i in range(begin, end):
                dp[i] += cnt
        return max(dp) <= capacity

方法3(DP2,时间复杂度 O(n)):

当然方法 2 可以进一步优化,我们可以对每个区间的两个端点标记,左边端点使用加法(表示有人上车),右边端点使用减法(表示有人下车)。最后遍历标记后的数组,遍历的过程中累加标记值,如果超过了capacity,那么就表示不满足。

Python3 实现:

class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        dp = [0] * 1001
        for (cnt, begin, end) in trips:
            dp[begin] += cnt
            dp[end] -= cnt 
        cur = 0
        for t in dp:
            cur += t
            if cur > capacity:
                return False
        return True   
拓展:

如果起始时刻和终止时刻无限大,大到无法用上述方法 2 和方法 3 中的 dp 存储,怎么做呢?实际上,这种情况就和 Leetcode 253:Meeting Rooms II(超详细的解法!!!) 类似了,仿照其直接可以写出如下代码:

class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        time_people = sorted(x for (n, start, end) in trips for x in [[start, n], [end, -n]])  # 上车时间为正人数,下车时间为负人数,然后从小到大排序
        cnt = 0
        for tp in time_people:
            cnt += tp[1]
            if cnt > capacity:
                return False
        return True
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269