……

刷刷 Leetcode(2)

接上篇:刷刷 Leetcode(1)

安排活动的方案数

第二类斯特林数,算是联动理论课复习了。设 \(S(n,m)\)\(n\) 个不同的球放入 \(m\) 个相同盒子,并且每个盒子都非空的方案数,递推:\(S(n,m)=S(n-1,m-1)+mS(n-1,m)\)。我们可以枚举有表演者的活动数,有 \(i\) 个活动有表演者时,对应的方案数是 \(y^ii!S(n,m)\dbinom{x}{i}\),预处理组合数、斯特林数、幂次、阶乘,中间不要忘了处处取模,最后枚举相加即可,时间复杂度 \(\mathcal O(n(x+n))\)(精细处理,这里只实现了 \(\mathcal O(n^2+x^2)\) 就足够了):

class Solution:
    def numberOfWays(self, n: int, x: int, y: int) -> int:
        S = [[0 for i in range(n +1)] for j in range(n + 1)]
        C = [[0 for i in range(x +1)] for j in range(x + 1)]
        S[0][0] = 1
        for i in range(x + 1):
            C[i][0] = 1
        mod = 1000000007
        for i in range(1, n + 1):
            for j in range(1, i + 1):
                S[i][j] = (S[i - 1][j] * j % mod+ S[i - 1][j - 1]) % mod

        for i in range(1, x + 1):
            for j in range(1, x + 1):
                C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod

        xc = [1 for i in range(n + 1)]
        mi = [1 for i in range(x + 1)]
        for i in range(1, n + 1):
            xc[i] = (xc[i - 1] * i) %mod
        for i in range(1, x + 1):
            mi[i] = (mi[i - 1] * y) %mod

        ans = 0
        for i in range(1, min(x + 1, n + 1)):
            now = (S[n][i] * xc[i] % mod) * C[x][i] % mod
            ans = (ans + now * mi[i]) % mod

        return ans

7.13

运动员和训练师的最大匹配数

贪心即可,按照能力值从大到小排序,每个训练员都辅导现有运动员中比自己能力值低中的最大者,这样一定是最优的,因为给下面的训练员留下的都是最简单的方案,即存在最优子结构,排序后双指针维护即可,时间复杂度 \(\mathcal O(n\log n+m\log m)\)

from typing import List
class Solution:
    def matchPlayersAndTrainers(self, players: List[int], trainers: List[int]) -> int:
        players.sort(reverse=True)
        trainers.sort(reverse=True)
        n, m = len(players), len(trainers)
        ans = 0

        j = 0
        for i in range(m):
            if j == n:
                break
            while True:
                if j == n:
                    break
                if trainers[i] >= players[j]:
                    j += 1
                    ans += 1
                    break
                else:
                    j += 1

        return ans

最大化城市的最小电量

最大化最小值考虑二分答案,接下来是思考如何线性的判定可行性。实际上贪心即可,从左边开始扫,如果低于设定的阈值,就思考将其作为一个城市供电站影响的最左断点,这个影响会持续 \(1+2r\) 个长度,我们维护一个列表记录所有的影响,由于 \(r\) 始终不变,这个东西是有单调性的,每个城市枚举结束后判断队首是不是到这个城市一段影响刚好结束即可。至于每个城市初始电量,可以预处理维护一下,即随时加新的右端点、减旧的左端点,总的时间复杂度 \(\mathcal O(n\log k)\)

from typing import List
class Solution:
    def maxPower(self, stations: List[int], r: int, k: int) -> int:
        self.n = len(stations)
        self.s = stations
        self.r = r
        self.k = k
        self.sums = [0 for i in range(self.n)]
        for i in range(min(self.n, r + 1)):
            self.sums[0] += self.s[i]
        minx = self.sums[0]
        for i in range(1, self.n):
            self.sums[i] = self.sums[i - 1]
            if r + i < self.n:
                self.sums[i] += self.s[r + i]
            if -r + i > 0:
                self.sums[i] -= self.s[i - r - 1]
            minx = min(minx, self.sums[i])

        l = minx
        r = minx + k
        mid = (l + r + 1) >> 1
        while l < r:
            if self.canmake(mid):
                l = mid
            else:
                r = mid - 1
            mid = (l + r + 1) >> 1

        return mid

    def canmake(self, m):
        ss = 0
        now = 0
        self.rt = []
        top = 0
        for i in range(self.n):
            if self.sums[i] + now < m:
                d = m - (self.sums[i] + now)
                ss += d
                now += d
                self.rt.append([d, i + 2 * self.r])

            if len(self.rt) > top:
                if self.rt[top][1] == i:
                    now -= self.rt[top][0]
                    top += 1

        if ss <= self.k:
            return 1
        else:
            return 0

7.14

二进制链表转整数

今天的每日一题还是简单了一些,线性递推即可,时间复杂度 \(\mathcal O(n)\)

# Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
from typing import Optional
class Solution:
    def getDecimalValue(self, head: Optional[ListNode]) -> int:
        ans = 0
        u = head
        while True:
            ans += u.val
            if u.next != None:
                ans = ans << 1
                u = u.next
            else:
                break
        return ans

餐盘栈

看着比较吓人,不过 python 天然的 List 对象可以说十分的好用。首先有多少次操作,就限定了总占用空间的上界,并且 List 是可以长度可变的数据结构,所以插入直接维护一个 top 记录当前栈栈顶的位置即可。为了得到从左往右第一个未满的栈,我们维护一个堆存储未满栈的编号;同理再维护一个堆维护非空栈的编号。比较麻烦的是 popAtStack 操作,因为有可能 pop 出一个栈顶之后栈刚好变空,涉及到堆中删除的操作,这个是很麻烦的。不过好在我们可以懒惰进行这个操作,用一个 lazy 列表代表本来要从未满栈编号堆中删除却未删除的节点编号,如果在 pop 操作中,pop 出来的数 lazy 标签是 \(1\),我们就继续 pop 直到 lazy 不是 \(1\) 或者堆变空为止。实际上 lazy 最多 \(q\) 次变为 \(1\)\(q\) 代表操作次数),这涉及整个过程中所有 pop 操作最多多操作 \(q\) 次,均摊下来是没问题的,而且有机会在 push 阶段中消解掉某些 lazy 标签,操作更为简单。实际上不维护 lazy 也是可以的,直接按 top 来判断是否要继续 pop(pop 操作中)即可,虽然那无法再 push 阶段消解掉一些多余操作,但是仍然是均摊保障的。时间复杂度 \(\mathcal O(q\log q)\)

class DinnerPlates:
    def __init__(self, capacity: int):
        from queue import PriorityQueue
        self.c = capacity
        self.n = -1
        self.noemp = PriorityQueue()
        self.noful = PriorityQueue()
        self.lazy = []
        self.top = []
        self.st = []
    def push(self, val: int) -> None:
        if self.noful.empty():
            self.n += 1
            self.top.append(0)
            self.st.append([val])
            self.lazy.append(0)
            if self.top[self.n] < self.c - 1:
                self.noful.put(self.n)
            self.noemp.put(-self.n)
        else:
            u = self.noful.get()
            self.top[u] += 1
            if len(self.st[u]) > self.top[u]:
                self.st[u][self.top[u]] = val
            else:
                self.st[u].append(val)
            if self.top[u] < self.c - 1:
                self.noful.put(u)
            if self.top[u] == 0:
                if self.lazy[u] == 1:
                    self.lazy[u] = 0
                else:
                    self.noemp.put(-u)
    def pop(self) -> int:
        if self.noemp.empty():
            return -1
        else:
            flg = 0
            u = 0
            while True:
                if self.noemp.empty():
                    flg = 1
                    break
                u = -self.noemp.get()
                if self.lazy[u] == 1:
                    self.lazy[u] = 0
                else:
                    break
            if flg:
                return -1
            val = self.st[u][self.top[u]]
            self.top[u] -= 1
            if self.top[u] >= 0:
                self.noemp.put(-u)
            if self.top[u] == self.c - 2:
                self.noful.put(u)
            return val
    def popAtStack(self, index: int) -> int:
        if index > self.n:
            return -1
        if self.top[index] == -1:
            return -1
        else:
            val = self.st[index][self.top[index]]
            self.top[index] -= 1
            if self.top[index] == self.c - 2:
                self.noful.put(index)
            if self.top[index] == -1:
                self.lazy[index] = 1
            return val

7.15

设计电影租借系统

逆天,其实和上个题基本一样,对于每个电影维护一个未借出的堆,对于所有电影维护一个已解出的堆,在 rent 和 drop 操作中分别对应惰性操作即可,这题难点其实是哈希,不过 python 字典也太好用了/ww。。时间复杂度仍然是 \(O((m+q)\log m)\),怎么会有人写 Treap 啊,还超时了一个点...。代码包含了两种写法,可以在点击此处查看(直接粘在这里观感太差了)。

有效单词

直接扫一遍即可,时间复杂度 \(\mathcal O(n)\)

class Solution:
    def isValid(self, word: str) -> bool:
        yuan = "aeiouAEIOU"
        fu = "bcdfghjklmnpqrstvwxyz" + "BCDFGHJKLMNPQRSTVWXYZ"
        dig = "1234567890"
        if len(word) < 3:
            return False
        else:
            fy, ff = 0, 0
            for c in word:
                if (c not in yuan) and (c not in fu) and (c not in dig):
                    return False
                elif c in yuan:
                    fy = 1
                elif c in fu:
                    ff = 1
            if not fy:
                return False
            if not ff:
                return False
            else:
                return True

找到初始输入字符串 II

看到数据范围想到大概是一个 \(\mathcal O(k^2)\) 的做法,既然以 \(k\) 为主导,我们大概是要考虑减去长度小于 \(k\) 的方案总数的。首先预处理出所有连续区间的长度,设 \(dp_k\) 为长度为 \(k\) 的方案数,这里其实是压掉了一维,因为当前状态之和上一个时间的状态有关。我们发现 \(dp_i\leftarrow \sum_{j=\max\{0, i-a\}}^{i-1}dp_{j}\) 其中 \(a\) 为当前连续区间的长度,这个就是一个求和的形式,我们如果可以在处理完每个时刻后的 \(dp_k\) 后做一个前缀和,就可以 \(\mathcal O(1)\) 地计算出每次转移了。最后从总方案数中减去不合理的方案数总和即可,这样的时间复杂度就是 \(\mathcal O(k^2+n)\)

class Solution:
    def possibleStringCount(self, word: str, k: int) -> int:
        n = len(word)
        mod = 1000000007

        lst = "0"
        now = 1
        S = 1
        u = []
        for i in range(n):
            if word[i] != lst:
                S = S * now % mod
                if i > 0:
                    u.append(now)
                now = 1
            else:
                now += 1
            lst = word[i]

        S = S * now % mod
        u.append(now)
        m = len(u)

        if m >= k:
            return S

        else:
            dp = [0 for i in range(k)]
            dp[0] = 1
            a = [0 for i in range(k)]
            for i in range(m):
                a[0] = dp[0]
                dp[0] = 0
                for j in range(1, k):
                    a[j] = (a[j - 1] + dp[j]) % mod
                    dp[j] = 0
                for j in range(i + 1, k):
                    minx = max(0, j - u[i])
                    if minx == 0:
                        dp[j] = a[j - 1]
                    else:
                        dp[j] = (a[j - 1] - a[minx - 1] + mod) % mod


            ans = 0
            for i in range(m, k):
                ans = (ans + dp[i]) % mod

            return (S - ans + mod) % mod

7.16

找出有效子序列的最大长度 I

考虑可行的串只有三种情况:全是奇数、全是偶数、奇偶严格交替。前两种情况就是扫一遍统计,后面的情况记录一下奇偶变了多少次就行了,时间复杂度 \(\mathcal O(n)\)

from typing import List
class Solution:
    def maximumLength(self, nums: List[int]) -> int:
        n = len(nums)
        n1, n2, n3 = 0, 0, 0
        lst = -1
        for i in range(n):
            if nums[i] % 2 == 0:
                n1 += 1
            else:
                n2 += 1
            if nums[i] % 2 != lst:
                n3 += 1
                lst = nums[i] % 2

        ans = max(n1, max(n2, n3))
        return ans

网格传送门旅游

对于有特殊路径的网格路径问题,不太能直接搜索,还是要考虑最短路。不过发现这个路径都只有 \(0\)\(1\),用堆维护比较亏,使用 deque,\(0\) 路径放队首首先出队,\(1\) 路径放队尾后出队,这样自然就维护了有序关系,时间复杂度 \(\mathcal O(mn)\)

from typing import List
from collections import deque
class Solution:
    def minMoves(self, matrix: List[str]) -> int:
        self.m, self.n = len(matrix), len(matrix[0])
        self.matrix = matrix
        self.g = [[] for i in range(26)]
        self.ab = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        self.abd = dict({(self.ab[i], i) for i in range(26)})

        for i in range(self.m):
            for j in range(self.n):
                if self.matrix[i][j] in self.ab:
                    self.g[self.abd[self.matrix[i][j]]].append([i, j])

        self.used = [0 for i in range(26)]
        self.dis = [[1919810 for i in range(self.n)] for j in range(self.m)]
        self.vis = [[0 for i in range(self.n)] for j in range(self.m)]
        self.dis[0][0] = 0
        from queue import Queue
        self.q = deque()
        self.q.append([0, 0])

        while self.q:
            u = self.q.pop()
            # print(u, self.dis[u[0]][u[1]])
            if self.vis[u[0]][u[1]]:
                continue
            self.vis[u[0]][u[1]] = 1

            Dxy = [(-1, 0), (1, 0), (0, -1), (0, 1)]
            if self.matrix[u[0]][u[1]] in self.ab:
                c = self.matrix[u[0]][u[1]]
                if not self.used[self.abd[c]]:
                    self.used[self.abd[c]] = 1
                    for loc in self.g[self.abd[c]]:
                        if loc != [u[0], u[1]]:
                            if self.dis[u[0]][u[1]] < self.dis[loc[0]][loc[1]]:
                                self.dis[loc[0]][loc[1]] = self.dis[u[0]][u[1]]
                                self.q.append(loc)
            for dxy in Dxy:
                dx, dy = dxy[0], dxy[1]
                x = u[0] + dx
                y = u[1] + dy
                if self.is_legal(x, y):
                    if self.dis[u[0]][u[1]] + 1 < self.dis[x][y]:
                        self.dis[x][y] = self.dis[u[0]][u[1]] + 1
                        self.q.appendleft([x, y])

        if self.dis[self.m - 1][self.n - 1] < 1919810:
            return self.dis[self.m - 1][self.n - 1]

        else:
            return -1

    def  is_legal(self, x, y):
        if x < 0:
            return 0
        elif y < 0:
            return 0
        elif x >= self.m:
            return 0
        elif y >= self.n:
            return 0
        elif self.matrix[x][y] == "#":
            return 0
        return 1

图中边值的最大和

首先其实只有两种情况,环与链。成环时,考虑递推,\(S_{n}\) 为最优值的话,从 \(n-1\)\(n\),断一个边 \((i,j)\),加 \((n, i)\)\((n,j)\),设任意一种 \(n-1\) 环时的方案为 \(x_{n-1}\in X_{n-1}\),那么 \(S_n=\max_{x_{n-1}, (i,j)\in x_{n-1}}\{x_{n-1} + n(i + j) -ij\}\),求导,可知后面这部分最大时 \(i,j\) 应该是 \(n-1\)\(n-2\),而 \(x_{n-1}\) 当然在 \(S_{n-1}\) 处取到,归纳可以知道两个是可以同时成立的,于是 \(S_{n}=S_{n-1} + n^2 - 2\),起始条件 \(S_3 = 11\)

然后就是链的情况,这时候其实就是加了一个值为 \(0\) 为点,又可以看做是一个环,按照这个规律与 \(S_{n+1}\) 可以对应上关系,发现 \(x_{n+1}\) 无论是什么情况,总对应一个链上 \(n\) 个点的情况,他们的差值是固定的 \((n+1)^2\),于是这时候的最大值 \(P_n\) 满足 \(P_n = S_{n+1}-(n+1)^2\)\(S_n\) 的求值可以用到 \(i^2\) 求和公式,综上整个题都可以 \(\mathcal O(1)\) 完成(代码注释了 \(\mathcal O(n)\) 的递推过程,也可以通过):

from typing import List
class Solution:
    def maxScore(self, n: int, edges: List[List[int]]) -> int:
        m = len(edges)

        # f = [0 for i in range(n + 2)]
        # f[3] = 11
        # for i in range(4, n + 2):
        #     f[i] = f[i - 1] + i * i - 2


        if m == n:
            # return f[n]
            fn = 3 - 2 * n + n * (n + 1) * (2 * n + 1) // 6
            return fn

        else:
            if n == 1:
                return 0
            # return f[n + 1] - (2 * (n + 2) * (n + 1) - 2 * (n + 1)) // 2
            fn = 3 - 2 * (n + 1) + (n + 1) * (n + 2) * (2 * n + 3) // 6
            return fn - (n + 1) ** 2

判断连接可整除性

比较有意思,考虑如何分割问题,比如在一开始,我们可以枚举第一个取多少(作为最前面的),假设说取了 \(x\),后面还有共 \(l\) 位数,那么后面这些数对 \(k\) 取模的结果一定必须是 \((0-x * 10^l ) \bmod k\),然后这样就可以一直往后推下去了,注意到我们思考问题的影响因素只有哪些数还没用以及最后的要求模数,用这两个数作为索引进行记忆化即可。如何保证最小字典序呢?其实先按从大到小排个序,搜索的时候贪心的先试探小的没用的就可以了,一旦返回了 \(True\),那么立刻 return,这样搜到的第一个一定是所有可行解里字典序最小的那个了,时间复杂度 \(\mathcal O(kn2^n)\)

from typing import List
class Solution:
    def concatenatedDivisibility(self, nums: List[int], k: int) -> List[int]:
        self.n = len(nums)
        self.nums = nums
        self.nums.sort()
        self.ll = [0 for i in range(self.n)]
        self.x = [self.nums[i] % k for i in range(self.n)]
        self.suml = 0
        for i in range(self.n):
            self.ll[i] = len(str(self.nums[i]))
            self.suml += self.ll[i]
        self.k = k
        self.dp = [[-1 for i in range(k)] for j in range(1 << self.n)]
        self.vis = [1 for i in range(self.n)]
        self.ans = []
        now = self.dfs(0, self.suml)
        if self.ans == []:
            return []
        else:
            return [self.nums[self.ans[i]] for i in range(self.n - 1, -1, -1)]
        # self.now = []

    def ec(self):
        ans = 0
        for i in range(self.n):
            ans = ans + (self.vis[i] << i)
        return ans

    def dfs(self, p, lefl):
        code = self.ec()
        # if code == 1:
        #     print(p)
        if self.dp[code][p] >= 0:
            return self.dp[code][p]

        if code == 0:
            if p == 0:
                self.dp[code][p] = 1
            else:
                self.dp[code][p] = 0
            return self.dp[code][p]

        ut = 0
        for i in range(self.n):
            if self.vis[i] == 1:
                z = (10 ** (lefl - self.ll[i])) % self.k
                r = (p - z * self.x[i] % self.k + self.k) % self.k
                self.vis[i] = 0
                oi = self.dfs(r, lefl - self.ll[i])
                ut = ut | oi
                self.vis[i] = 1
                if ut:
                    self.ans.append(i)
                    break

        self.dp[code][p] = ut
        return ut

7.17

找出有效子序列的最大长度 II

直接 dp 即可,设 \(dp_{ij}\) 为第 \(i\sim n-1\) 个数中,这个相等于余数值为 \(j\) 时的最大有效子序列长度,那么 \(dp_{is}=\max_{j> i}\{dp_{js}+1\}\),其中 \(s=(a_i + a_j)\bmod k\),直接更新即可,注意所有 \(dp\) 的初始值都是 \(1\),而非有的是 \(0\),这样时间复杂度 \(\mathcal O(n(k+n))\)

from typing import List
class Solution:
    def maximumLength(self, nums: List[int], k: int) -> int:
        n = len(nums)
        a = [nums[i] % k for i in range(n)]
        dp = [[1 for i in range(k)] for j in range(n)]
        ans = 1
        for i in range(k):
            dp[n - 1][i] = 1

        for i in range(n - 2, -1, -1):
            for j in range(i + 1, n):
                dp[i][(a[i] + a[j]) % k] = max(dp[j][(a[i] + a[j]) % k] + 1, dp[i][(a[i] + a[j]) % k])
                ans = max(ans, dp[i][(a[i] + a[j]) % k])

        return ans

统计移除递增子数组的数目 II

最特殊的情况就是输入即严格递增,这时候答案是 \(\frac{n(n+1)}{2}\),而如果输入没有这么好的条件时,我们考虑找到从前缀最大严格递增序列,和后缀严格递增序列,这两个之间的是一定要删的,因为只能删一段连续的。接下来我们把这两个前后缀位置当做双指针,实际上只用保证前面剩的最后一个严格小于后面剩的第一个,这个具有单调性,顺序维护一下即可,时间复杂度 \(\mathcal O(n)\)

from typing import List
class Solution:
    def incremovableSubarrayCount(self, nums: List[int]) -> int:
        n = len(nums)
        l = 0
        for i in range(1, n):
            if nums[i] > nums[i - 1]:
                l = i
            else:
                break

        if l == n - 1:
            return n * (n + 1) // 2
        else:
            r = n - 1
            for i in range(n - 2, -1, -1):
                if nums[i] < nums[i + 1]:
                    r = i
                else:
                    break
            ans = n - r + 1
            for i in range(l + 1):
                while True:
                    if r == n:
                        break
                    if nums[i] < nums[r]:
                        break
                    else:
                        r += 1
                ans = ans + n - r + 1
            return ans

找到最大周长的多边形

贪心即可,先进行一下排序,贪心地尽可能取最大的值作为最大边,剩下的比他小的边的和如果大于它就是最优,否则取次大的即可,时间复杂度 \(\mathcal O(n\log n)\)

from typing import List
class Solution:
    def largestPerimeter(self, nums: List[int]) -> int:
        n = len(nums)
        nums.sort()
        s = 0
        for i in range(n - 1):
            s += nums[i]

        for i in range(n - 1, 1, -1):
            if s > nums[i]:
                return s + nums[i]
            else:
                s -= nums[i - 1]

        return -1

树中每个节点放置的金币数目

DFS 即可,维护最大的最多三个正数和最小的最多两个负数即可,这个直接在搜索的时候拼一下儿子的然后排序即可,当然可以不排序,毕竟只用取两三个,但这就是比较精细的处理了,实现起来比较复杂,不如直接排序,注意几个点,需要同时维护三个正数以及一正两负两种情况,而不是 else 的关系,第二个就是即使继承了所有儿子的最大正数或最小负数,也要记得排序,否则在后面选的时候不知道孰大孰小了,最后注意树要加双向边,太久没写这都忘了可还行,真是要死了。时间复杂度 \(\mathcal O(n\log n)\),精细处理可以做到 \(\mathcal O(n)\),当然这里就只用实现简单的前者了:

from typing import List
class Edges:
    def __init__(self, v, nxt):
        self.v = v
        self.nxt = nxt
class Solution:
    def placedCoins(self, edges: List[List[int]], cost: List[int]) -> List[int]:
        n = len(edges) + 1
        self.head = [-1 for i in range(n)]
        self.cnt = 0
        self.e = []
        self.cost = cost
        for i in range(n - 1):
            a, b = edges[i][0], edges[i][1]
            self.add(a, b)
            self.add(b, a)
        self.u_min = [[] for i in range(n)]
        self.vis = [0 for i in range(n)]
        self.u_max = [[] for i in range(n)]
        self.siz = [1 for i in range(n)]
        self.coin = [0 for i in range(n)]

        self.dfs(0)
        return self.coin

    def add(self, u, v):
        ed = Edges(v, self.head[u])
        self.e.append(ed)
        self.head[u] = self.cnt
        self.cnt += 1

    def dfs(self, u):
        e = self.head[u]
        self.vis[u] = 1
        while True:
            if e == -1:
                break
            v, nxt = self.e[e].v, self.e[e].nxt
            if self.vis[v] == 0:
                self.dfs(v)
                self.siz[u] += self.siz[v]
                for x in self.u_min[v]:
                    self.u_min[u].append(x)
                for x in self.u_max[v]:
                    self.u_max[u].append(x)
            e = nxt

        if self.cost[u] < 0:
            self.u_min[u].append(self.cost[u])
        if self.cost[u] >= 0:
            self.u_max[u].append(self.cost[u])

        self.u_min[u].sort()
        self.u_max[u].sort(reverse=True)
        if len(self.u_min[u]) > 2:
            self.u_min[u] = self.u_min[u][:2]
        if len(self.u_max[u]) > 3:
            self.u_max[u] = self.u_max[u][:3]

        if self.siz[u] < 3:
            self.coin[u] = 1
        else:
            maxn = -1
            if len(self.u_max[u]) >= 3:
                maxn = max(maxn, self.u_max[u][0] *  self.u_max[u][1] * self.u_max[u][2])

            if (len(self.u_max[u]) >= 1) and (len(self.u_min[u]) >= 2):
                maxn = max(maxn, self.u_max[u][0] * self.u_min[u][0] * self.u_min[u][1])

            if maxn > 0:
                self.coin[u] = maxn

T 秒后青蛙的位置

又做了一道树的题目,这个题的关键在于特判:能不能到,到了如果还有富余时间并且还能往更深处走那概率直接是 \(0\)。具体实现就直接 DFS,由于树是无向的,所以我们直接指定 \(1\) 号节点是根就可以了,搜索的时候,除了根,回溯的时候记录一下路径上的点的(出入)度减 \(1\),其实就是青蛙在该点的选择数,时间复杂度 \(\mathcal O(n)\)

from typing import List
class Node:
    def __init__(self, to, nxt):
        self.v = to
        self.nxt = nxt
class Solution:
    def frogPosition(self, n: int, edges: List[List[int]], t: int, target: int) -> float:
        self.target = target
        self.e = []
        self.head = [-1 for i in range(n + 1)]
        self.cnt = 0
        self.deg = [0 for i in range(n + 1)]
        for e in edges:
            a, b = e[0], e[1]
            self.add(a, b)
            self.add(b, a)
            self.deg[a] += 1
            self.deg[b] += 1
        self.now = []
        self.dfs(1, 0)
        self.now.reverse()
        self.now[0] += 1
        ans = 1.0
        if t < len(self.now) - 1:
            return 0
        elif t == len(self.now) - 1:
            for i in range(len(self.now) - 1):
                ans = ans / self.now[i]
        else:
            if self.now[-1] > 0:
                return 0
            else:
                for i in range(len(self.now) - 1):
                    ans = ans / self.now[i]

        return ans

    def add(self, u, v):
        node = Node(v, self.head[u])
        self.e.append(node)
        self.head[u] = self.cnt
        self.cnt += 1

    def dfs(self, u, fa):
        if self.target == u:
            self.now.append(self.deg[u] - 1)
            return 1
        e = self.head[u]
        while True:
            if e == -1:
                break
            v, nxt = self.e[e].v, self.e[e].nxt
            if v == fa:
                e = nxt
                continue
            if self.dfs(v, u) == 1:
                self.now.append(self.deg[u] - 1)
                return 1
            e = nxt

        return 0

7.18

删除元素后和的最小差值

看到数据范围,能想到的方法也就只有枚举分界点了,我们可以在 \([n-1, 2n-1]\) 区间枚举分界处(分界包含右端点,不包含左端点),然后对于前后两个部分,前面要减去最大的若干个,后面要减去最小的若干个,这个可以考虑用堆来维护动态的前 \(k\) 个最值。比如说在 \(n+i\)\(i\geq -1\)) 作为分界点时,左边要删去前 \(i + 1\) 大的数,而在 \(n+i+1\) 作为分界点时,有两种情况,如果第 \(n+i+1\) 个数是要删去的,那么只需要在前 \(n+i\) 个数中删去前 \(i+1\) 大的;否则只需要在前 \(n+i\) 个数中删去前 \(i+1\) 大的之后再删一个,这样我们用堆来维护当前最大的出堆即可。后面也一样,实际上不需要维护具体哪些数是要删去的,只需要记录删去数的和,辅助前缀和就可以计算出前后部分的剩余数和值,时间复杂度 \(\mathcal O(n\log n)\)

from typing import List
from queue import PriorityQueue
class Solution:
    def minimumDifference(self, nums: List[int]) -> int:
        n = len(nums) // 3
        f = [0 for i in range(3 * n)]
        f[0] = nums[0]
        pre, suf = [0 for i in range(n + 1)], [0 for i in range(n + 1)]
        for i in range(1, 3 * n):
            f[i] = f[i - 1] + nums[i]

        pq1 = PriorityQueue()
        for i in range(n):
            pq1.put(-nums[i])

        sum1 = 0
        pre[0] = f[n - 1]
        for i in range(n, 2 * n):
            pq1.put(-nums[i])
            u = -pq1.get()
            sum1 += u
            pre[i - n + 1] = f[i] - sum1

        pq2 = PriorityQueue()
        for i in range(2 * n, 3 * n):
            pq2.put(nums[i])

        sum2 = 0
        suf[n] = f[3 * n - 1] - f[2 * n - 1]
        for i in range(2 * n - 2, n - 2, -1):
            pq2.put(nums[i + 1])
            u = pq2.get()
            sum2 += u
            suf[i - n + 1] = f[3 * n - 1] - f[i] - sum2

        ans = 3 * (10 ** 10)
        for i in range(n + 1):
            ans = min(ans, pre[i] - suf[i])

        return ans

图中的最长回文路径

有意思,还得状压。我们考虑枚举一个回文串最外层是哪两个节点,然后枚举这两个节点的出边,也就是次外层又是哪两个节点,这是一个递归的过程。
然后直接递归 + vis 会爆掉,但是如果只记录现在讨论的是哪两个节点作为最外层,是不够的,因为一个点只能访问一次,那只能拿出状压的方法了,记录一个三维数组 \(dp_{cij}\) 分别代表是否访问的状态压缩码、两个最外层节点。
题目数据量对 python 不友好,注意常数:一个是下层如果两边是一样的,不要再递归一层进去特判了,直接在枚举的时候特判。另一个是枚举第一个出边时可以用邻接表,第二个出边最好用同字母位置表+邻接矩阵判断连通性。注意判断两个出边节点相同放置的位置,在这里错了一次,投降了qwq。时间复杂度应该是 \(\mathcal O(n^4 2^n)\),不过应该很不紧,就算是完全图,不同标签点的分流也会使常数小一些:

from typing import List
class Edge:
    def __init__(self, to, nxt):
        self.to = to
        self.nxt = nxt
class Solution:
    def maxLen(self, n: int, edges: List[List[int]], label: str) -> int:
        self.n = n
        self.edges = edges
        self.label = label
        self.e = []
        self.head = [-1 for i in range(n)]
        self.cnt = 0
        self.em = [[0 for i in range(n)] for j in range(n)]
        self.ab = "abcdefghijklmnopqrstuvwxyz"
        self.abd = dict([(self.ab[i], i) for i in range(26)])
        self.abs = [[] for i in range(26)]
        for i in range(n):
            self.abs[self.abd[label[i]]].append(i)
        m = len(edges)
        for i in range(m):
            a, b = edges[i][0], edges[i][1]
            self.add(a, b)
            self.add(b, a)
        self.vis = [0 for i in range(n)]
        ans = 1
        self.dp = [[[-1 for i in range(n)] for j in range(n)] for k in range(1 << n)]
        # for j in range(1 << n):
        #     for i in range(n):
        #         self.dp[j][i][i] = 1

        for i in range(n - 1):
            for j in range(i + 1, n):
                if self.label[i] == self.label[j]:
                    self.vis[i], self.vis[j] = 1, 1
                    ans = max(ans, self.dfs(i, j))
                    self.vis[i], self.vis[j] = 0, 0

        return ans

    def add(self, u, v):
        ee = Edge(v, self.head[u])
        self.e.append(ee)
        self.head[u] = self.cnt
        self.cnt += 1
        self.em[u][v] = 1
        self.em[v][u] = 1

    def ec(self):
        ans = 0
        for i in range(self.n):
            ans = ans + (self.vis[i] << i)
        return ans

    def dfs(self, u, v):
        code = self.ec()
        if self.dp[code][u][v] >= 0:
            return self.dp[code][u][v]
        # if u == v:
        #     return 1
        now = 0
        if self.em[u][v]:
            now = max(now, 2)

        e1= self.head[u]
        while True:
            if e1 == -1:
                break
            v1, nxt1 = self.e[e1].to, self.e[e1].nxt
            if self.vis[v1]:
                e1 = nxt1
                continue

            c = self.abd[self.label[v1]]
            for v2 in self.abs[c]:
                if self.vis[v2]:
                    continue
                if not self.em[v2][v]:
                    continue
                if v1 == v2:
                    now = max(now, 3)
                    continue
                self.vis[v1] = 1
                self.vis[v2] = 1
                now = max(now, self.dfs(v1, v2) + 2)
                self.vis[v1] = 0
                self.vis[v2] = 0

            e1 = nxt1

        self.dp[code][u][v], self.dp[code][v][u] = now, now
        return now

用特殊操作处理字符串 II

倒退回去即可,注意每种操作下 \(k\) 是怎么变化的,然后注意倒数第一次正好字符在当前的 \(k\) 位置上,则说明是最终答案,时间复杂度 \(\mathcal O(n)\)

class Solution:
    def processStr(self, s: str, k: int) -> str:
        now = 0
        n = len(s)
        for c in s:
            if c == "*":
                if now:
                    now -= 1
            elif c == "#":
                now = now * 2
            elif c == "%":
                pass
            else:
                now += 1

        if k >= now:
            return "."

        p = k
        for i in range(n - 1, -1, -1):
            if s[i] == "*":
                now += 1

            elif s[i] == "#":
                now = now // 2
                if p >= now:
                    p = p - now

            elif s[i] == "%":
                p = now - 1 - p

            else:
                if now - 1 == p:
                    return s[i]
                now -= 1

数组的最小稳定性因子

叹为观止了,差点成为第一个没头绪的力扣题(悲)。不过要是考试考到应该是要 GG 了,想了很久然后错了三遍。

首先是一个二分答案的问题,重点在于如何判定,因为区间 gcd 这个不好递推,我们想到了一种及其抽象的方法,用于计算每个数最多往后多少个的区间不是 \(\text{gcd}=1\) 的。因为我们不会求具体地最大公因数,所以就只能退而求其次,只要一个区间内的 GCD 值不是为 \(1\),那么就是一个稳定的子数组。
我们考虑先从开头扫一段,直到这一段的区间 \(\text{gcd}=1\),然后再往后扫一段,得到下一个 \(\text{gcd}=1\) 的区间。前面的区间求后缀 GCD,后面的区间求后缀 GCD,后面这个后缀是一个嵌套整除并且递减的区域,所以我们可以二分,即枚举前面区间的每一个值,在后面区间二分第一个使得前者对应后缀与后者对应前缀的 \(gcd=1\) 的位置,两端长度和即为有前面区间枚举的节点向右,最长的 \(\text{gcd}>1\) 的区间。这样实际上虽然做了很多次,但是每段只被算了一次最长区间的位置,为了能保证最后一段能被正常处理(不过我们代码是史山,所以有一部分还是处理了这部分),我们可以在数组后面加一个 \(1\),辗转相除是 \(\mathcal O(\log a)\) 的,因此这部分复杂度是 \(\mathcal O(n\log n\log\max a_i)\)
而二分部分,我们只需要从 \(0\) 开始,发现一个违反的,就把右端点改成 \(1\),进而使用一次修改机会,这是贪心保证的最优解,然后调到 \(1\) 后面的点开始作为新的右端点枚举即可。
因此总的时间复杂度是 \(\mathcal O(n\log n\log\max a_i)\)

from typing import List
class Solution:
    def minStable(self, nums: List[int], maxC: int) -> int:
        anums = nums.copy()
        anums.append(1)
        self.n = len(anums)
        n = self.n
        self.maxt = [0 for i in range(n)]
        self.now_pre = [0 for i in range(n)]
        self.now_suf = [0 for i in range(n)]
        n0 = 0
        for i in range(n):
            if anums[i] > 1:
                n0 +=1
        if n0 <= maxC:
            return 0

        lst_l, lst_r = -1, 0
        while True:
            # 终止判断?
            if lst_r == n - 1:
                now_gcd = anums[lst_r]
                for i in range(lst_r, lst_l - 1, -1):
                    if now_gcd >= anums[i]:
                        now_gcd = self.gcd(now_gcd, anums[i])
                    else:
                        now_gcd = self.gcd(anums[i], now_gcd)
                    if now_gcd == 1:
                        self.maxt[i] = lst_r - i + 1
                    else:
                        self.maxt[i] = 1
                break
            l, r = lst_r, lst_r
            if lst_l == lst_r:
                l, r = lst_r + 1, lst_r + 1
            if r >= n - 1:
                pass
            else:
                now_gcd = anums[l]
                while True:
                    if anums[r] <= now_gcd:
                        now_gcd = self.gcd(now_gcd, anums[r])
                    else:
                        now_gcd = self.gcd(anums[r], now_gcd)
                    if now_gcd == 1:
                        break
                    if r == n - 1:
                        break
                    r += 1

            if lst_l >= 0:
                now_gcd = anums[lst_r]
                for i in range(lst_r - 1, lst_l - 1, -1):
                    if anums[i] <= now_gcd:
                        now_gcd = self.gcd(now_gcd, anums[i])
                    else:
                        now_gcd = self.gcd(anums[i], now_gcd)

                    self.now_suf[i] = now_gcd

                now_gcd = anums[l]
                for i in range(l, r + 1):
                    if anums[i] <= now_gcd:
                        now_gcd = self.gcd(now_gcd, anums[i])
                    else:
                        now_gcd = self.gcd(anums[i], now_gcd)
                    self.now_pre[i] = now_gcd

                for i in range(lst_l, lst_r):
                    if self.now_suf[i] == 1:
                        self.maxt[i] = lst_r - i + 1
                    else:
                        if self.now_pre[r] >= self.now_suf[i]:
                            if self.gcd(self.now_pre[r], self.now_suf[i]) > 1:
                                self.maxt[i] = 1
                                continue
                        else:
                            if self.gcd(self.now_suf[i], self.now_pre[r]) > 1:
                                self.maxt[i] = 1
                                continue

                        self.maxt[i] = self.find(i, l + 1, r) - i + 1
            lst_l, lst_r = l, r

        l, r = 1, n
        mid = (l + r) >> 1
        while l < r:
            if self.finda(mid, maxC):
                r = mid
            else:
                l = mid + 1
            mid = (l + r) >> 1

        return mid

    def find(self, i, l, r):
        mid = (l + r) >> 1
        while l < r:
            if self.now_pre[mid] >= self.now_suf[i]:
                g = self.gcd(self.now_pre[mid], self.now_suf[i])
            else:
                g = self.gcd(self.now_suf[i], self.now_pre[mid])

            if g == 1:
                r = mid
            else:
                l = mid + 1
            mid = (l + r) >> 1

        return mid

    def gcd(self, n, m):
        if m == 0:
            return n
        return self.gcd(m, n % m)

    def finda(self, m, c):
        k = c
        l = 0
        while True:
            if l > self.n - 1 - m:
                break
            if self.maxt[l] > m + 1:
                k -= 1
                if k < 0:
                    return 0
                l += (m + 1)
            else:
                l += 1
        return 1

所以怎么做区间 \(gcd\) 呢?—— ST 表,这个题也可以做到 \(\mathcal O(n\log n\log \max a_i)\),不过常数应该比咱们的大,而且空间也是一绝。

后续我又想了一下,实际上二分求最长区间这个过程是具有单调性的,双指针即可,时间复杂度可以优化到 \(\mathcal O(n\log n+n\log \max a_i)\)

7.19

删除子文件夹

差点要写字典树了,后来一想,子文件夹对于父文件夹来说,一定字典序更大,更特殊的,我们在所有文件夹名称后面加一个 '/',或者其他文件名不会有的字符,这样排序后,就是良序的了,记录当前的最上层的文件夹即可,如果发现匹配不上了,就更换最上层文件夹记录即可,时间复杂度 \(\mathcal O(\sum \text{len}\log n)\)

from typing import List

class Solution:
    def removeSubfolders(self, folder: List[str]) -> List[str]:
        n = len(folder)
        ss = []
        for s in folder:
            ss.append(s + "/")

        ss.sort()

        lst = "ppo"
        ans = []
        for i in range(n):
            if self.judge(lst, ss[i]):
                continue
            else:
                ans.append(ss[i][: len(ss[i]) - 1])
                lst = ss[i]

        return ans

    def judge(self, s, t):
        lens = len(s)
        if len(t) < lens:
            return 0
        if t[:lens] == s:
            return 1
        else:
            return 0

统计极差最大为 K 的分割方式数

这下一败涂地了/kk
考虑 dp,从 \(n\) 序列到 \(n + 1\) 序列,我们只需要枚举最后一个数能和哪些数作为一个划分子段即可,这里容易忽略掉前面的数也必须满足要求,这就说明这个东西有单调性,可以线性处理(双指针,如果使用二分,则会 TLE)。关键是判断是否有太大或者太小的数,我傻了,写了个 ST 表(实际上使用单调队列即可),然后转移用前缀和处理即可,时间复杂度是 \(\mathcal O(n\log n)\),如果使用单调队列则可以做到 \(\mathcal O(n)\)(怎么有人忘了取模啊):

from typing import List
from math import floor, ceil, log
class Solution:
    def countPartitions(self, nums: List[int], k: int) -> int:
        n = len(nums)
        mod = 1000000007
        m = ceil(log(n) / log(2)) + 1
        self.f_min, self.f_max = [[0 for i in range(m)] for j in range(n)], [[0 for i in range(m)] for j in range(n)]
        for i in range(n):
            self.f_min[i][0] = nums[i]
            self.f_max[i][0] = nums[i]
        j = 1
        while True:
            if (1 << j) > n:
                break
            l = 1 << j
            for i in range(n - l + 1):
                self.f_min[i][j] = min(self.f_min[i][j - 1], self.f_min[i + (1 << (j - 1))][j - 1])
                self.f_max[i][j] = max(self.f_max[i][j - 1], self.f_max[i + (1 << (j - 1))][j - 1])

            j += 1
        # print(self.f_min)

        dp = [0 for i in range(n)]
        f_dp = [0 for i in range(n)]
        f_dp[0] = 1
        dp[0] = 1
        r = -1
        for i in range(1, n):
            while True:
                mino, maxo = self.q(r + 1, i)
                if (nums[i] - k <= mino) and(nums[i] + k >= maxo):
                    if r >= 1:
                        dp[i] = (f_dp[i - 1] - f_dp[r - 1] + mod) % mod
                    elif r == 0:
                        dp[i] = f_dp[i - 1]
                    else:
                        dp[i] = (f_dp[i - 1] + 1) % mod
                    break

                else:
                    r += 1

            f_dp[i] = (f_dp[i - 1] + dp[i]) % mod

        return dp[n - 1]

    def q(self, l, r):
        h = floor(log(r - l + 1) / log(2))
        return min(self.f_min[l][h], self.f_min[r - (1 << h) + 1][h]), max(self.f_max[l][h], self.f_max[r - (1 << h) + 1][h])

又太长卡了,再开个新的吧:刷刷 Leetcode(3)

posted @ 2025-07-12 18:55  童话镇里的星河  阅读(9)  评论(0)    收藏  举报