【leetcode系列】【算法】2020春季全國編程大賽-團隊賽(更新第五題解題思路和代碼)

寫在前面:

本次題感覺總體都有ACM的味道了,個人感覺難度極高...

強烈建議自己先考慮一下,然後再看題解

 

題目一:

題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/qi-wang-ge-shu-tong-ji/

 

解題思路:

通過分析題目發現,不同分數的簡歷之間是不會互相影響的,所以原問題等同於n個數字全排列之後,有多少元素還在原位置

設這個隨機變量爲X,對於X_{i},如果第i個元素還在原位,則X_{i} = 1,否則X_{i} = 0

對於每一個元素,隨機排序後還在原位的概率爲\frac {1}{n}。由於期望的可加性,可以得到如下的式子:

E(X) = \sum _{i = 0}^{n - 1}E(x_{i}) = \frac {1}{n} * n = 1

由結果可知,E(X)最終與n無關,所以我們只需要計算有多少個不重複的數字就可以了

利用hash的性質,統計有多少不相同的數字,再返回hash表中key的個數

時間複雜度:O(N)

 

代碼實現:

class Solution:
    def expectNumber(self, scores: List[int]) -> int:
        return len(set(scores))

 

題目二:

題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/xiao-zhang-shua-ti-ji-hua/

 

解題思路:

二分查找

初始的left = 0, right = 所有時間之和,然後開始二分查找

每次取mid之後,mid是每一天的做題時間。然後根據mid,判斷當前被分成幾天完成

如果大於m天,說明需要調大mid,則更新left = mid + 1

如果小於等於m天,說明需要調小mid,則更新right = mid

關於求助功能的體現,在代碼註釋中有詳細解釋

 

代碼實現:

class Solution:
    def minTime(self, time: List[int], m: int) -> int:
        def check(mid, time, m):
            # 根據當前mid天數,計算出需要的總天數
            use_day = 1
            # 當前序列需要使用的天數
            total_time = 0
            # 當前序列中的最大耗時
            max_time = time[0]
            for t in time[1:]:
                # 因爲是可以求助的,所以可以每組中多一道題
                # 所以從1開始遍歷,並且更新當前題組total_time時,排除掉耗時最高的一個
                if total_time + min(max_time, t) <= mid:
                    # 更新當前題組的總耗時,加上當前值和最大值中更小的一個
                    # 最終達到目的:題組中的最大耗時,使用求助功能解答
                    total_time += min(max_time, t)
                    max_time = max(max_time, t)
                else:
                    # 排除掉最大耗時,當前題組也超過mid限制的天數了
                    # 此時更新當前需要天數use_day += 1
                    # 並重置題組所需天數和最高耗時
                    use_day += 1
                    total_time = 0
                    max_time = t

            return use_day <= m

        # 初始化最小值爲0,最大值爲時間總和
        left, right = 0, sum(time)
        while left < right:
            mid = (left + right) >> 1
            if check(mid, time, m):
                # 總耗時天數 <= m,想要增大總耗時,通過調小right調小mid
                right = mid
            else:
                # 總耗時天數 > m,想要減小總耗時,通過增大left增大mid
                left = mid + 1

        return left

 

題目三:

圖片.gif

題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/xun-bao/

 

解題思路:

事實上,我們的走法只有這麼幾種:

  1. 從S走向O,取石頭
  2. 從O走向M,踩機關
  3. 從M走向O,再次取石頭
  4. 從M走向T,所有機關都已經觸發,走向終點

BFS:

對於所有的S、M、O和T,不論我們做什麼操作(觸發機關、搬運石頭),互相之間的連通性是不會變化的

所以在開始的時候,對每一個特殊點都進行一次BFS,搜索當前點到其他點的最短距離,之後就不需要再去搜索了

 

狀態壓縮DP:

在最開始,我們一定是從S點開始,經過一個O點搬一塊石頭,再到達一個M點觸發機關。所以我們先枚舉S通過某個O到達每一個M的最短距離(S -> O -> M),這樣我們就首先得到了按照遊戲規則的S到每一個M的最短距離

接下來,按照遊戲規則,我們需要從某個M出發,到達O搬運一塊石頭,再到達其他未出發機關的M點(M -> O -> M'),和計算S -> O -> M的邏輯相同,我們需要枚舉出所有M -> O -> M'的最短距離,就得到了按照遊戲規則的每一個M到達其他M的最短距離

而M到T的距離,之前在BFS的時候已經計算出來了

這樣,我們就將已知條件轉換爲了:

  1. 按照遊戲規則,S到達每一個M的最短距離
  2. 按照遊戲規則,M到達每一個M‘的最短距離
  3. 每一個M到達T的最短距離

這樣就是一個經典的狀壓DP問題了

令dp[s][i]表示在第i個機關,總觸發狀態爲s的最小步數(s是一個狀態的bitmap),那麼枚舉當前沒有觸發的機關j,狀態轉移公式爲:

dp[s|(1<<j)][j]=min(dp[s|(1<<j)][j],dp[s][i]+minDistance(i,j))

其中minDistance爲之前預處理出的所有特殊點之間的最小距離

 

複雜度分析:

  • BFS時間複雜度O(m * o * S),其中m爲M點的數目,o爲O點的數目,S爲迷宮面積
  • dp時間複雜度爲O(2^{m} * m^{2})

 

代碼實現:

import queue
class Solution:
    def bfs(self, maze):
        """
        以迷宮maze中的每個特殊點爲中心,BFS搜索到其他所有特殊點的最近距離
        Args:
            maze : 原始迷宮信息
        Returns:
            total_dis_info : 按照special_point中的順序,保存每個點到其餘點的最近距離.如果要獲取第i個點到第j個點的最近距離,可以直接使用total_dis_info[i][j]獲取
            tag : 保存每個類型的點,在special_point中的索引值,同時也是total_dis_info中的索引值
        """
        # 迷宮的高和寬
        height, width = len(maze), len(maze[0])
        # 特殊點位置信息
        special_point = []
        for i in range(height):
            for j in range(width):
                if maze[i][j] in ['S', 'T', 'M', 'O']:
                    special_point.append((i, j, maze[i][j]))

        # 按照special_point中的順序,保存每個點到其餘點的最近距離
        # 如果要獲取第i個點到第j個點的最近距離,可以直接使用total_dis_info[i][j]獲取
        total_dis_info = []
        # 保存每個類型的點,在special_point中的索引值
        # 這個索引值,同時也是在total_dis_info中的索引值
        tag = collections.defaultdict(list)
        # 以每個特殊點爲中心,開始BFS搜索其他特殊點的最短距離
        for idx, (x, y, point_type) in enumerate(special_point):
            q = queue.Queue()
            dis = [[float('inf') for i in range(width)] for j in range(height)]
            dis[x][y] = 0
            q.put((x, y))
            while not q.empty():
                curr_x, curr_y = q.get()
                # BFS時搜索的的方向
                # 按照數組順序,分別爲:向下、向右、向上、向左
                for x_move, y_move in [[0, 1], [1, 0], [0, -1], [-1, 0]]:
                    nxt_x = curr_x + x_move
                    nxt_y = curr_y + y_move
                    if nxt_x < 0 or nxt_x >= height or nxt_y < 0 or nxt_y >= width:
                        # 超出邊界
                        continue
                    elif maze[nxt_x][nxt_y] == '#':
                        # 不可通行
                        continue

                    if dis[nxt_x][nxt_y] > dis[curr_x][curr_y] + 1:
                        # 如果nxt_x,nxt_y的位置之前沒搜索到,當前距離應該是無窮大
                        # 或者之前搜索到nxt_x,nxt_y的位置,並且距離比本次搜索的距離要大
                        # 則更新nxt_x,nxt_y和原始x,y的最近距離爲當前距離 + 1
                        # 並將nxt_x,nxt_y加入隊列,繼續搜索
                        dis[nxt_x][nxt_y] = dis[curr_x][curr_y] + 1
                        q.put((nxt_x, nxt_y))

            # 當前點到其他所有特殊點,按照保存在special_point中的順序的最小距離
            curr_dis_info = []
            for i, j, _ in special_point:
                curr_dis_info.append(dis[i][j])

            #加入到結果集中
            total_dis_info.append(curr_dis_info)
            tag[point_type].append(idx)

        return total_dis_info, tag

    def state_compression_dp(self, total_dis_info, tag):
        """
        狀態壓縮DP處理
        Args:
            total_dis_info : BFS的距離信息
            tag : 每個特殊點的索引序列
        Returns:
            最終步數結果
        """
        m_num = len(tag['M'])
        o_num = len(tag['O'])
        s_idx = tag['S'][0]
        t_idx = tag['T'][0]
        dp = [[float('inf') for i in range(m_num)] for j in range(1 << m_num)]
        # 處理S -> O -> M的最短距離
        for i in range(m_num):
            m_idx = tag['M'][i]
            # s移位後,dp[s][i]表示的是每個M到自己的距離
            self_idx = 1 << i
            for j in range(o_num):
                o_idx = tag['O'][j]
                # 更新每個M到自己的距離,爲S開始,經過每個O,到自己的最小距離
                dp[self_idx][i] = min(dp[self_idx][i], total_dis_info[s_idx][o_idx] + total_dis_info[o_idx][m_idx])

        # 預處理M -> O -> M的距離
        m_2_m_dis = [[float('inf') for i in range(m_num)] for j in range(m_num)]
        for i in range(m_num):
            m_idx1 = tag['M'][i]
            for j in range(m_num):
                m_idx2 = tag['M'][j]
                for k in range(o_num):
                    o_idx = tag['O'][k]
                    # 獲取每個M,經過O,到達其他M的最短距離
                    m_2_m_dis[i][j] = min(m_2_m_dis[i][j], total_dis_info[m_idx1][o_idx] + total_dis_info[o_idx][m_idx2])

        # 狀態壓縮DP
        for s in range(1 << m_num):
            for j in range(m_num):
                if s & (1 << j) == 0:
                    continue
                for k in range(m_num):
                    if s & (1 << k) != 0:
                        continue
                    ns = s | (1 << k)
                    dp[ns][k] = min(dp[ns][k], dp[s][j] + m_2_m_dis[j][k])

        ans = float('inf')
        fs = (1 << m_num) - 1
        for j in range(m_num):
            m_idx = tag['M'][j]
            ans = min(ans, dp[fs][j] + total_dis_info[m_idx][t_idx])

        return -1 if ans == float('inf') else ans

    def minimalSteps(self, maze: List[str]) -> int:
        """
        根據輸入的迷宮,計算一共需要多少步,才能在觸發所有機關後,從起點走向終點
        Args:
            maze : m * n的迷宮矩陣
        Returns:
            需要的步數
        """
        total_dis_info, tag = self.bfs(maze)
        if 'M' not in tag:
            # 如果沒有機關,則直接返回起點到終點的最近距離
            # 因爲S和T有且只有1個,所以直接獲取相應的第一個
            s_idx = tag['S'][0]
            t_idx = tag['T'][0]
            # 如果起點無法到達終點,則返回-1; 否則返回起點到終點的最近距離total_dis_info[s_idx][t_idx]
            return -1 if float('inf') == total_dis_info[s_idx][t_idx] else total_dis_info[s_idx][t_idx]

        return self.state_compression_dp(total_dis_info, tag)

 

題目四:

題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/qie-fen-shu-zu/

 

解題思路:

假設f[i]表示在將i這個質數添加到數組中後,數組的最少分組是幾個

比如對於數組[2, 5, 3, 6],在從前向後遍歷時,對f的更新如下:

  1. 遍歷到數字2,此時只有1個數字,只能劃分爲1個子數組,所以f[2] = 1
  2. 遍歷到數字5,此時有2個數字,但是最大公約數爲1,需要劃分爲2個數組,所以f[5] = 2
  3. 遍歷到數字3,此時有3個數字,但是所有數字之間的最大公約數都爲1,需要劃分爲3個數組,所以f[3] = 3
  4. 遍歷到數字6,此時有4個數字,先獲取6的最小質因數2,發現與2的最大公約數爲2 > 1,可以與2構成一個符合條件的子數組,所以f[6] = 1,並對6除以最小質因數2進行再次循環處理,處理的數字爲6 / 2 = 3
    1. 處理3時,發現與前面的數字3有最大公約數 = 3 > 1,可以與3構成一個符合條件的子數組,當與3構成子數組時,分爲2個子數組:[2, 5], [3, 6],所以更新f[3] = 2;但是之前使用2爲質因數時,最小子數組個數爲1,所以當前的最小子數組個數仍爲1

 

代碼實現:

class Solution:
    def __init__(self):
        # 事先對小於10^6數字進行預處理,計算所有數字的最小質因子,方便後續處理
        # 放在init中初始化,會導致在leetcode的性能測試中超時
        # 如果爲了通過leetcode測試,需要將init邏輯放在類外,將此部分時間放到import中,不會佔用測試case的耗時
        # 但是爲了代碼結構合理,此處放在了init中進行初始化
        max_num = pow(10, 6)
        self.rec = [1] * (max_num + 1)
        num = 2
        while num <= max_num:
            times = num
            while times * num <= max_num:
                # 這段邏輯的意思,是說從小到大的乘上去
                # num當前的值,就是第一次遍歷到的數字的最小質因數
                # times從當前數字開始,因爲小於當前數字的倍數,已經在之前遍歷過了
                if self.rec[times * num] == 1:
                    self.rec[times * num] = num

                times += 1

            num += 1
            while num <= max_num:
                # 目的爲找到下一個沒有設置最小質因數的數字
                if self.rec[num] == 1:
                    break

                num += 1


    def splitArray(self, nums: List[int]) -> int:
        """
        對輸入的數組nums進行子數組劃分,要求爲每個子數組的第一個和最後一個數字最大公約數大於1
        Args:
            nums : 需要切分的原始數組
        Returns:
            劃分的子數組最小個數
        """
        prime_factor = {}
        n = len(nums)
        curr_num = nums[0]
        # 先對第一個數字進行質因數分解,並把分解結果加入到prime_factor中
        while True:
            if self.rec[curr_num] == 1:
                prime_factor[curr_num] = 1
                break

            prime_factor[self.rec[curr_num]] = 1
            curr_num //= self.rec[curr_num]

        # 初始化最小步數,因爲最少分爲1個數組,所以初始化爲1
        min_step = 1
        for curr_num in nums[1:]:
            # 這段循環的主要邏輯如下:
            # 對每個數字進行質因數分解,並判斷分解出的質因數,是否在前面的數字中出現過
            # 如果沒出現,則說明需要新增一個子數組
            # 如果出現過,說明當前數字可以與前面的某個數字構成符合條件的子數組
            # 這個時候,就與前面的數字進行合併,可能導致總子數組個數不變,也有可能減小
            # 比如對於數組[2, 3, 6]
            # 遍歷到3時,子數組個數爲2; 當遍歷到6時,發現和數字2有大於1的公約數,所以就將子數組個數更新爲1
            curr_min_step = float('inf')
            while True:
                if self.rec[curr_num] == 1:
                    # 如果無法繼續做質因數分解,則更新當前curr_num的質因數個數
                    prime_factor[curr_num] = min(prime_factor.get(curr_num, float('inf')), min_step + 1)
                    curr_min_step = min(curr_min_step, prime_factor[curr_num])
                    break

                # 判斷當前curr_num是否能夠和之前的數字構成符合條件的子數組
                # 如果可以,則使用curr_num對應的之前的子數組
                # 如果不可以,則說明curr_num需要新加子數組,更新curr_num位置的子數組個數爲當前的最小字數字個數 + 1
                prime_factor[self.rec[curr_num]] = min(prime_factor.get(self.rec[curr_num], float('inf')), min_step + 1)
                curr_min_step = min(curr_min_step, prime_factor[self.rec[curr_num]])
                curr_num //= self.rec[curr_num]

            min_step = curr_min_step

        return min_step

 

題目五:

Screenshot 2020-03-20 at 17.04.58.png

(上圖:A->B->C 右轉; 下圖:D->E->F 左轉)

題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/you-le-yuan-de-mi-gong/

 

解題思路:

這道題其實比較簡單,不過需要事先了解一下叉積的性質:

  1. 如果\vec{a} \times \vec {b} > 0,說明\vec {b}\vec {a} 的左側
  2. 如果\vec {a} \times \vec {b} < 0,說明\vec {b}\vec {a}的右側

如果要求下個轉向關係爲左轉,那麼本次找到最右邊的一個點,則剩餘的點全部都是左轉;如果要求下個轉向關係爲右轉,那麼本次找到最左邊的一個點,則剩餘的點全部都是右轉

 

代碼實現:

class Solution:
    def sub(self, a: List[int], b: List[int]):
        """
        根據兩個點,計算其向量表示
        Args:
            a : 向量起點
            b : 向量終點
        Returns:
            起點a到終點b的向量
        """
        return [a[0] - b[0], a[1] - b[1]]

    def cross(self, a: List[int], b: List[int]):
        """
        計算兩個向量的叉積
        Args:
            a : 第一個向量
            b : 第二個向量
        Returns:
            向量叉積
            如果 > 0, 說明b在a的左邊
            如果 < 0,說明b在a的右邊
        """
        return a[0] * b[1] - a[1] * b[0]

    def get_next_point(self, turn_type, point_num, points, used, last_idx):
        """
        根據當前轉向類型和上個點位置,計算下個點的位置
        Args:
            turn_type : 要求的轉向類型
            point_num : 總點數
            points : 點序列
            used : 對應的點是否已被使用
            last_idx : 上一個選中點的索引值
        Returns:
            下個點的索引值
        """
        target_idx = -1
        for i in range(point_num):
            if used[i]:
                # 當前點已經被使用
                continue
            elif target_idx == -1:
                # 找到的第一個可用的點
                target_idx = i
                continue

            # 根據當前選中的點,和上個選中的點,計算其向量標識
            curr_vector = self.sub(points[target_idx], points[last_idx])
            # 根據當前遍歷的點,和上個選中的點,計算其向量標識
            next_vector = self.sub(points[i], points[last_idx])
            # 計算兩個向量的叉積
            curr_cross = self.cross(curr_vector, next_vector)
            if turn_type == 'L' and curr_cross < 0:
                # 說明next_vector在curr_vector右邊
                # 因爲下個轉向類型爲左轉,想要找到當前最右邊的點
                # 所以更新下個點的索引值爲當前遍歷索引
                target_idx = i
            elif turn_type == 'R' and curr_cross > 0:
                # 說明next_vector在curr_vector左邊
                # 因爲下個轉向類型爲右轉,想要找到當前最左邊的點
                # 所以更新下個點的索引值爲當前遍歷索引
                target_idx = i

        return target_idx

    def visitOrder(self, points: List[List[int]], direction: str) -> List[int]:
        """
        根據輸入的points點序列,找到符合direction轉向序列要求的結果集
        Args:
            points : 輸入的點序列
            direction : 輸入的轉向序列
        Returns:
            符合要求的點索引序列
        """
        n = len(points)
        # 記錄已經用過的點
        # 使用額外數組記錄,而不是刪除原先數組中的元素,是因爲刪除後,不管是python的list,還是c++的vector,都需要把後面的元素依次向前移一個
        # 這樣會使得效率下降,最差情況下,每次都是刪除第一個,將後面所有元素都向前移動
        used = [False] * n
        # 結果序列
        res = []

        # 從最左邊的點開始,主要是從某一邊開始,最右邊、最下邊、最上邊的點也可以
        start = 0
        for i in range(n):
            if points[i][0] < points[start][0]:
                start = i

        # 更新起點狀態,並加入到結果集
        used[start] = True
        res.append(start)

        # 開始尋找符合條件的點序列
        for i in direction:
            # 獲取下個點索引值
            next_idx = self.get_next_point(i, n, points, used, start)
            # 更新下個點狀態,並加入到結果集
            used[next_idx] = True
            res.append(next_idx)
            start = next_idx

        # 將最後一個未使用過的點放入到結果集中
        for i in range(n):
            if not used[i]:
                res.append(i)
                break

        return res

 

題目六:

題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/you-le-yuan-de-you-lan-ji-hua/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章