寫在前面:
本次題感覺總體都有ACM的味道了,個人感覺難度極高...
強烈建議自己先考慮一下,然後再看題解
題目一:
題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/qi-wang-ge-shu-tong-ji/
解題思路:
通過分析題目發現,不同分數的簡歷之間是不會互相影響的,所以原問題等同於n個數字全排列之後,有多少元素還在原位置
設這個隨機變量爲,對於,如果第i個元素還在原位,則,否則
對於每一個元素,隨機排序後還在原位的概率爲。由於期望的可加性,可以得到如下的式子:
由結果可知,最終與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
題目三:
題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/xun-bao/
解題思路:
事實上,我們的走法只有這麼幾種:
- 從S走向O,取石頭
- 從O走向M,踩機關
- 從M走向O,再次取石頭
- 從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的時候已經計算出來了
這樣,我們就將已知條件轉換爲了:
- 按照遊戲規則,S到達每一個M的最短距離
- 按照遊戲規則,M到達每一個M‘的最短距離
- 每一個M到達T的最短距離
這樣就是一個經典的狀壓DP問題了
令dp[s][i]表示在第i個機關,總觸發狀態爲s的最小步數(s是一個狀態的bitmap),那麼枚舉當前沒有觸發的機關j,狀態轉移公式爲:
其中爲之前預處理出的所有特殊點之間的最小距離
複雜度分析:
- BFS時間複雜度,其中m爲M點的數目,o爲O點的數目,S爲迷宮面積
- dp時間複雜度爲
代碼實現:
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/
解題思路:
假設表示在將i這個質數添加到數組中後,數組的最少分組是幾個
比如對於數組[2, 5, 3, 6],在從前向後遍歷時,對f的更新如下:
- 遍歷到數字2,此時只有1個數字,只能劃分爲1個子數組,所以f[2] = 1
- 遍歷到數字5,此時有2個數字,但是最大公約數爲1,需要劃分爲2個數組,所以f[5] = 2
- 遍歷到數字3,此時有3個數字,但是所有數字之間的最大公約數都爲1,需要劃分爲3個數組,所以f[3] = 3
- 遍歷到數字6,此時有4個數字,先獲取6的最小質因數2,發現與2的最大公約數爲2 > 1,可以與2構成一個符合條件的子數組,所以f[6] = 1,並對6除以最小質因數2進行再次循環處理,處理的數字爲6 / 2 = 3
- 處理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
題目五:
(上圖:A->B->C 右轉; 下圖:D->E->F 左轉)
題目鏈接: https://leetcode-cn.com/contest/season/2020-spring/problems/you-le-yuan-de-mi-gong/
解題思路:
這道題其實比較簡單,不過需要事先了解一下叉積的性質:
- 如果,說明在 的左側
- 如果,說明在的右側
如果要求下個轉向關係爲左轉,那麼本次找到最右邊的一個點,則剩餘的點全部都是左轉;如果要求下個轉向關係爲右轉,那麼本次找到最左邊的一個點,則剩餘的點全部都是右轉
代碼實現:
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/