【LeetCode】519. Random Flip Matrix 解題報告(Python)
題目描述:
You are given the number of rows n_rows
and number of columns n_cols
of a 2D binary matrix where all values are initially 0. Write a function flip
which chooses a 0 value uniformly at random, changes it to 1, and then returns the position [row.id, col.id]
of that value. Also, write a function reset
Note:
1 <= n_rows, n_cols <= 10000
0 <= row.id < n_rows
and0 <= col.id < n_cols
flip
will not be called when the matrix has no 0 values left.- the total number of calls to
flip
reset
will not exceed 1000.
Example 1:
Input:
["Solution","flip","flip","flip","flip"]
[[2,3],[],[],[],[]]
Output: [null,[0,1],[1,2],[1,0],[1,1]]
Example 2:
Input:
["Solution","flip","flip","reset","flip"]
[[1,2],[],[],[],[]]
Output: [null,[0,0],[0,1],null,[0,0]]
Explanation of Input Syntax:
The input is two lists: the subroutines called and their arguments. Solution’s constructor has two arguments, n_rows and n_cols. flip and reset have no arguments. Arguments are always wrapped with a list, even if there aren’t any.
題目大意
題目是用n_rows, n_cols給出了一個空白的二維陣列,二維陣列每個數字都是0.現在要使用flip函式隨機選擇0的位置翻轉成1.同時還有一個函式reset是把整個二維陣列重置成0.實現這個要求,並儘可能的優化時間和空間,並且減少random()函式的呼叫。
解題方法
方法一:迴圈生成隨機數
這個題的心路歷程:時間消耗比較多的肯定是random()的呼叫次數,首先分析這個函式能呼叫多少次。很激動的是flip函式竟然最多隻呼叫1000次!而行和列的大小竟然到達了10000!所以很明顯這個題需要我們用時間換空間嘛。肯定不能開個很大的二維陣列然後記錄這個過程。
所以我想了類似點陣圖的方法,只需要一個隨機數字,然後把這個數字轉成二維空間的行數和列數就行。所以使用set來儲存已經使用過的數字,然後選隨機數,如果這個隨機數已經出現過,那麼繼續迴圈找到一個沒有出現過的數字。然後計算這個數字在二維列表中的位置就好了。
求一個數字應該排列在二維陣列中的位置方式是[pos / self.N, pos % self.N]
。要記住。
效率怎麼樣呢?很容易想象,當這個二維陣列比較小的時候,那麼衝突肯定很多,所以迴圈的呼叫次數很多。但是,當二維陣列足夠大,比如題目中有10000*10000的空位時候,flip最多才1000次,那麼隨機數碰撞的次數肯定很少了,效率就比較高了。
時間複雜度是O(N),空間複雜度是O(N).N是呼叫次數。超過了52%的提交。
class Solution(object):
def __init__(self, n_rows, n_cols):
"""
:type n_rows: int
:type n_cols: int
"""
self.M = n_rows
self.N = n_cols
self.total = self.M * self.N
self.fliped = set()
def flip(self):
"""
:rtype: List[int]
"""
pos = random.randint(0, self.total - 1)
while pos in self.fliped:
pos = random.randint(0, self.total - 1)
self.fliped.add(pos)
return [pos / self.N, pos % self.N]
def reset(self):
"""
:rtype: void
"""
self.fliped.clear()
# Your Solution object will be instantiated and called as such:
# obj = Solution(n_rows, n_cols)
# param_1 = obj.flip()
# obj.reset()
方法二:Fisher–Yates shuffle 洗牌演算法
看到題目說了儘可能的優化隨機數的呼叫,就知道還有更高效的演算法,果然有啊!著名的Fisher–Yates shuffle 洗牌演算法!但是需要改進一下。關於這個演算法可以看這個視訊,還是挺容易弄懂的。這個演算法對N個數字進行隨機洗牌,只需要呼叫N - 1次隨機函式。
這個洗牌演算法的思想就是,使用一個指標從後向前遍歷,它標記的是洗牌的末尾。即這個指標之後的數字已經全部洗牌了,不用再考慮;前面的數字還沒有洗牌,需要處理;隨機生成一個範圍在前面陣列長度的隨機數,表示選中了哪個,然後和指標標記的位置進行交換,指標前移,重複這個過程。
我用一句更明白的話:每次在前面未洗牌部分隨機選擇一個數字,然後放到已經洗牌了數字裡頭。
至於為什麼需要指標以及交換數字,那是為了在原地in-place操作使用的。
同樣地,在這個題中不能直接使用那麼大的陣列進行這個過程的模擬,記憶體不夠。所以,使用一個字典儲存已經被隨機數選擇過的位置,把這個位置和末尾的total交換的實現方式是使用字典儲存這個位置交換成了末尾的那個數字。每次隨機到一個數字,然後在字典中查,如果這個數字不在字典中,表示這個數字還沒被選中過,那麼就直接返回這個數字,把這個數字和末尾數字交換;如果隨機數已經在字典中出現過,那麼說明這個位置已經被選中過,使用字典裡儲存的交換後的數字返回。
舉個例子吧:
輸入:
["Solution", "flip", "flip", "flip", "flip", "flip", "flip"]
[[2, 3], [], [], [], [], [], []]
程式碼第21行打印出來的r, x, self.total, self.d如下
(0, 0, 5, {0: 5})
(0, 5, 4, {0: 4})
(3, 3, 3, {0: 4, 3: 3})
(2, 2, 2, {0: 4, 2: 2, 3: 3})
(1, 1, 1, {0: 4, 1: 1, 2: 2, 3: 3})
(0, 4, 0, {0: 4, 1: 1, 2: 2, 3: 3})
希望這個例子能幫助理解吧!
時間複雜度是O(N),空間複雜度是O(N).N是呼叫次數。超過了31%的提交。
class Solution(object):
def __init__(self, n_rows, n_cols):
"""
:type n_rows: int
:type n_cols: int
"""
self.M = n_rows
self.N = n_cols
self.total = self.M * self.N
self.d = dict()
def flip(self):
"""
:rtype: List[int]
"""
r = random.randint(0, self.total - 1)
self.total -= 1
x = self.d.get(r, r)
self.d[r] = self.d.get(self.total, self.total)
# print(r, x, self.total, self.d)
return [x / self.N, x % self.N]
def reset(self):
"""
:rtype: void
"""
self.d.clear()
self.total = self.M * self.N
# Your Solution object will be instantiated and called as such:
# obj = Solution(n_rows, n_cols)
# param_1 = obj.flip()
# obj.reset()
日期
2018 年 10 月 19 日 —— 自古逢秋悲寂寥,我言秋日勝春朝