iwehdio的博客園:https://www.cnblogs.com/iwehdio/
初賽賽題題目、數據、源代碼、提交的文檔、復賽答辯PPT和決賽題目見:https://github.com/iwehdio/2020ZTE_FourierGroup
初賽使用Python語言,初賽第二階段運行時間在6~7s。初賽第一,決賽參與獎。
1、解題思路
- 賽題可以被抽象為求二向圖中指定長度的環的數量。
- 剛開始對題意有一個初步的了解。要求的是最大的數量,所以跟每人准備的數量無關,而是只跟朋友關系的復雜程度有關。
- 將部落中的每個人進行編號,按正負區分部落。用數組記錄禮物傳遞的路徑。
- 用深度優先搜索來對禮物傳遞路徑進行搜索。但是搜索的效率很低,只能對環長為4和6的路徑進行搜索。
- 在深度優先搜索的同時進行去重,包括路徑內部不重復的內去重和路徑之間不重復的外去重。
- 嘗試剪枝策略,比如記錄路徑中的第一個和第二個節點的編號,並對其在之后的路徑中進行處理。可以搜索到環長為10的路徑。
- 將搜索一整個路徑分解為搜索兩個半路徑,然后拼接起來。可以對14環進行搜索了。去重策略又多了一個半路徑的內去重。
- 尋找數據中的規律,發現了鄰接矩陣是由多個等間隔的分塊矩陣構成的,而且每個分塊矩陣都可以通過矩陣中第一個點的平移得到。
- 利用規律,對每個分塊矩陣,先對第一個節點進行搜索,在此基礎上對該分塊矩陣中的每個節點進行去重。
- 優化規律的使用,利用有序等特點,更換數據結構等對細節進行優化。
2、代碼解讀
-
讀入數據,定義全局變量。
# 記錄程序開始的時間,讀取csv數據 time0 = time.time() file_path = r"../data/part2/5.8/Example.csv" table = [] with open(file_path, 'r') as f: reader = csv.reader(f) for row in reader: table.append(row) # 定義全局變量 global number global head global interval global l global s aims = [2, 3, 4, 5, 6, 7] countall = [] l, s = {}, {} # 將鄰接矩陣轉換為鄰接表 source = [[] for i in range(len(table) + len(table[0]) + 1)] for i in range(len(table)): for j, v in enumerate(table[i]): if v == '1': source[i + 1].append(-j - 1) source[-j - 1].append(i + 1) def get(num): return source[num]
- number:半路徑的長度,環長的一半。
- head:頭結點的編號,A部落為正、B部落為負,從1/-1開始。
- l:字典,存儲頭結點開始找到的環,鍵為升序排列的元組。
- s:字典,存儲頭結點開始找到的半路徑,鍵為路徑的結束點編號,值為升序排列的數組。
-
根據不同的數據大小,判斷數據間隔。
if len(table) == 1344: interval = 192 elif len(table) == 256: interval = 64 index = [1] for i in range(1, int(len(table) / interval)): index.append(interval * i + 1)
-
數據規律:無論橫豎,數據集都可以看作是長度為一個間隔的分塊矩陣。以將矩陣橫向按間隔划分為例,矩陣中的排列僅由每個分塊中的第一個點確定。矩陣中其他點的位置取決於其上一個點的y值加1,如果前一個點的y值是間隔的倍數,則得到的下一個點的實際y值為加1后再減間隔值。(以間隔為64為例,點(5,-64)的下一個點為(6,-64-1+64)(部落A、B中的鄰接表中加減相反))
-
利用這個規律,我們只需計算出每個分塊矩陣中第一個點為頭結點時的環數,就可以計得該分塊矩陣中的點為頭結點時的所有有效的環數。
-
從圖中可以看出,每個區域中,都有兩個半斜線可以拼接為一根完整的斜線。
-
-
循環進行所有環長的搜索。
for x in aims: count = 0 # 初始化不同長度的環的數量 for p1 in index: # 不同區域的起始點 p2 = p1 + interval - 1 # 不同區域的終點 number = x # 設置環長度、頭節點,並進行傳遞 head = p1 count += convert(p1, p2) countall.append(count)
- 只需計算每隔一個間隔的點為頭結點的情況。
- 傳入
convert()
函數的值為該分塊矩陣的起點和終點。
-
調用的函數。
-
convert()
函數。# 對編號為 num 的村民的禮物進行傳遞 def convert(num, p2): # A:1開始的正數 B:-1開始的負數 thiscount = 0 # 遍歷該節點所有的長度為環長一半的路徑,稱半路徑 path = [num] # 初始化路徑 DFS(num, number, 0, path) # 開始搜索 for k in s.keys(): # 遍歷存儲半路徑的 s 字典 for i, m in enumerate(s[k]): # 字典的鍵為半路徑的最后一個節點編號 for n in s[k][i + 1:]: # 在同一個鍵內部的數組進行匹配 if repeat(m, n): # 二者的交集只能為半路徑的頭尾 c = m + n + [head, k[0]] # 拼接 c.sort() # 排序標准化 thiscount += checkIn(tuple(c), num, p2) # 校驗該環是否已經找到 s.clear() # 遍歷完從一個節點出發的半路徑后清空 s,更換頭節點 l.clear() return thiscount
- 按照傳入的頭結點編號,傳入函數
DFS()
進行深度優先搜索,搜索半路徑。 - 搜索出的半路徑,存儲在字典 s 中。按照 s 的鍵為尾節點進行拼接。
- 清空 s 和 l ,返回該分塊矩陣中計得的環數。
- 按照傳入的頭結點編號,傳入函數
-
函數
DFS()
。def DFS(num, length, depth, path): if depth == length: # 如果達到期望的路徑數組,將其排序標准化后, # print(number*2, " ", path) # 存入 s 字典,鍵為半路徑的尾節點(如果該半路徑未被找到) tr0 = tuple([path[-1]]) temppath = path[1:-1] + [] temppath.sort() if tr0 in s.keys(): # 判斷該半路徑是否已經被找到 if temppath not in s[tr0]: s[tr0].append(temppath) else: s[tr0] = [temppath] return for j in get(num): # 向下搜索 if (j < 0 or j > head) and (j not in path): # 去除半路徑中已有的和編號大於頭節點的數據 DFS(j, length, depth + 1, path + [j])
- 對半路徑進行深度優先搜索,傳入頭結點、當前路徑長度、期望長度和當前路徑。
- 如果長度符合要求,則存入字典 s 。字典 s 的鍵為尾節點。
- 存入字典前 s ,先進行排序,並判斷該尾節點和該路徑是否已經搜到。
- 深度優先搜索,排除小於頭結點和已經在現有路徑中的編號。
-
函數
repeat()
。def repeat(m, n): for i in m: if i in n: return False return True
- 判斷兩個數組中是否有重復元素。
-
函數
checkIn()
。def checkIn(c, p1, p2): if c not in l.keys(): l[c] = '' return checkdiff(c, p1, p2) + 1 return 0
- 將所有環按其升序排列的元組為鍵,存儲到字典並進行校驗。
- 如果已經存在,則不計數。
- 如果還未搜到,則進入
checkdiff()
函數計算該環在該分塊矩陣能衍生出多少個環。
-
函數
checkdiff()
。def checkdiff(c, p1, p2): # 判斷這個環能衍生出多少有效環 max_num = p1 - 1 for i in c[number:]: # 排序好的數組,前半部分是負的,不需要比較 if i > max_num: # 判斷是否在范圍中,越過右邊界則不再比較 if i > p2: break max_num = i return p2 - max_num
-
傳入的是一個未被搜到過的環,分塊矩陣的起始點。
-
這個函數的依據為:
-
在深度優先搜索中,我們剪枝了編號小於頭結點的點。這意味着,后邊搜出的環,如果在以同一個頭結點開始搜索的環字典中沒有重復,那么也不會與之前較小編號的頭結點開始搜到的環重復。
-
只搜索分塊矩陣起點為頭結點的環,就可以知道這個分塊矩陣中從起點到終點為頭結點可以生成的所有環數。
-
得出2的理由,是基於間隔的分塊矩陣的規律。從1中可知,在起點后的點為頭結點時,可能出現重復的唯一情況是:因為加1之前為間隔的倍數,加1后被減去一個間隔,導致路徑中存在小於頭結點的編號的點。
-
例:
對於一個頭結點為1的路徑: [1, -56, 64, -98, 1] 則頭結點2中必有一個路徑會搜索到的前三個點編號為: [2, -57, 1...] 在路徑中存在小於頭結點2的編號1,該路徑已經重復
-
-
基於以上依據,我們只需要找到,該路徑中,值位於該分塊矩陣中起點編號和終點編號之間最大的數,用終點編號減去該數,即為起點為頭結點時該路徑可以衍生(平移)出的環數。
-
例:
對於一個頭結點為1的路徑: [1, -56, 63, -98, 1] 則只能衍生出:64 - 63 = 1 個不重復的環
-
-
同時,由於傳入的路徑是排序好的數組,前半部分編號為負,不需要比較。同樣的,大於終點編號后,也不需要再比較。
-
-
-
優化:搜索和去重的效率較低,應該有更為快速的方法。
iwehdio的博客園:https://www.cnblogs.com/iwehdio/