出處:http://www.cnblogs.com/bigmonkey
本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途!
掃描二維碼關注公衆號「我是8位的」
在搜索的策略(2)——貪心策略中,小偷撬開了一個保險箱,利用貪心法偷走了裏面的物品並賣了個好價錢。如今小偷又來了,他光顧了同一個保險箱,保險箱中的物品還和以前同樣,有5個物品A,B,C,D,E,它們的體積分別是3,4,7,8,9,價值分別是4,5,10,11,13,只不過每種物品僅有一個。html
此次小偷帶了一個容量是20的揹包,他升級了一下本身的技術,打算使用遺傳算法從新編寫代碼。java
達爾文在1859年出版的《物種起源》中系統地闡述了進化論,他認爲生物是可變的,是不斷進化的,物競天擇,適者生存。進化論很快取代了神創論,成爲生物學研究的基石。python
遺傳算法正是受進化論啓發的一種優化算法。二十世紀六十年代初,密切根大學教授Holland開始研究天然和人工系統的自適應行爲,經過模擬生物進化過程設計了最初的遺傳算法,也稱爲經典遺傳算法(Genetic Algorithms,GA)。算法
遺傳算法和生物進化過程及其類似。算法首先從問題的解空間中隨機選擇一部分解做爲初始種羣,將種羣中的每個解視爲一個生物個體。以後讓種羣中的所有個體朝既定的目標(最優解)前進,而且在前進過程當中展開生存競爭。此時天然法則將產生做用,在競爭中勝出者會被保留下來,失敗者則被淘汰,爲勝出者騰出空間。接下來,勝出者會經過交叉和變異的方式產生出許多新的個體代替失敗者,以保持種羣總量不變。最後,新種羣構成了「下一代」,它們將繼續朝目標前進,並再次展開競爭……不斷重複下去,因爲每一代都是勝出者的子孫,因此咱們有理由相信,新一代在總體上強於上一代,整個種羣將朝着更高級的進化體逐步前進:app
生物遺傳靠的是基因,解讀基因編碼對於絕大多數人來講是個神祕的工做。受生物學啓發的遺傳算法也有基因編碼的概念,並且基因編碼是遺傳算法的首要問題,沒有基因編碼,就沒有遺傳。dom
基因、染色體、嘧啶、嘌呤……這些概念有些唬人了,實際上遺傳算法中的基因編碼遠沒有生物科學中的那麼神祕,它能夠簡單地理解成,以計算機存儲數據的形式表達問題的解。函數
一種經常使用且天然的編碼方式是二進制編碼。對於保險櫃中的每種物品來講,要麼拿走,用1表示;要麼不拿,用0表示,所以能夠用一個5位的二進制數表示一種盜竊方案:post
字符串」10101」表示小偷偷走了A、C、E三個物品,能夠用print_solution方法來解碼:性能
1 GOODS_LIST = [('A', 3, 4), ('B', 4, 5), ('C', 7, 10), ('D', 8, 11), ('E', 9, 13)] 2 3 def print_solution(code): 4 ''' 5 打印解決方案 6 :param code: 二進制基因編碼 7 ''' 8 size_sum, value_sum, = 0, 0 9 for i, x in enumerate(code): 10 if x == '1': 11 gtype, size, value = GOODS_LIST[i] 12 print('\t{0}\t體積:{1}\t價值:{2}'.format(gtype, size, value)) 13 size_sum += size 14 value_sum += value 15 print('\t揹包內的物品整體積:{0},\t總價值:{1}'.format(size_sum, value_sum))
保險櫃中的物品清單用一個簡單的三元組表示,('A', 3, 4)表示物品A的體積是3,價值是4。」10101」的解碼結果:學習
編碼方式還有不少,好比浮點數編碼、整數編碼、順序編碼、格雷編碼、字符編碼、矩陣編碼等,不一樣的編碼方式可能會對配對和變異的設計產生影響,可是不管使用哪一種編碼,都須要保證編碼值可以表示解空間中的全部解,而且可以和這些解一一對應。咱們會在後續看到不一樣於二進制編碼的其它編碼方式。
生物進化是以羣體的方式進行的,這個羣體被稱爲種羣。種羣中的每種生物都是一個個體,有着本身的基因編碼。
盜竊方案的基因編碼共有25=32種,每一中編碼都是一個個體,種羣的大小應當小於解空間中候選解的數量。咱們令種羣的大小是5,並使用隨機挑選的方式產生初始種羣:
1 POPULATION_SIZE = 5 # 種羣數量 2 3 def init_population(): 4 ''' 構造初始種羣 ''' 5 population = [] 6 code_len = len(GOODS_LIST) # 編碼長度 7 for i in range(POPULATION_SIZE): 8 population.append(''.join(random.choices(['0', '1'], k=code_len))) 9 return population
通常來講,種羣的大小一旦肯定就無需改變。
對於小偷來講,並非全部的編碼方式都是有效的解,好比11101,解碼的結果是小偷偷走了A、B、C、E四個物品,其體積總量是23,超過了揹包的容量。一種應對策略是在初始化種羣是加入判斷,排除無效的解,這在後續的配對和變異是也須要相似的判斷;另外一種是讓無效編碼永遠不會被選中,這須要在適應度的取值上作點文章。
種羣中的每一個個體均可以經過適應度函數計算出適應度的值。適應度高的將會有較大機率生存下來,並將基因遺傳給下一代;適應度低的,則有較大的機率被淘汰。適應度函數的另外一個較爲通俗名稱是成本函數,至於應該淘汰成本低的仍是成本高的,徹底取決於你本身。
偷竊方案的適應度評估很容易,只要計算價揹包中物品的總價值就能夠了,若是物品撐爆了揹包,讓總價值返回0:
1 V_BAG = 20 # 揹包的容量 2 3 def fitness_fun(code): 4 ''' 5 適應度評估 6 :param code: 二進制基因編碼 7 :return: 適應度評估值 8 ''' 9 size_sum, sum_value = 0, 0 # 物品的整體積,物品的總價值 10 for i, x in enumerate(code): 11 if x == '1': 12 _, size, value = GOODS_LIST[i] 13 size_sum += size 14 sum_value += value 15 if size_sum > V_BAG: # 揹包被撐爆時,適應度爲0 16 sum_value = 0 17 break 18 return sum_value
有了適應度評估值,就能夠開始殘酷的生存競爭。從種羣中選擇某些個體造成下一代的父羣體,以便將較爲優秀的基因遺傳到下一代。選擇策略有不少,經常使用的有輪盤賭法、精英保留法、錦標賽法等,不一樣的策略對遺傳算法的交叉設計、變異設計和總體性能都將產生影響。
1. 輪盤賭法
輪盤賭是一種依靠運氣的賭博方式。在賭博時,輪盤逆時針轉動,掌盤人把一個小球在微凸的輪盤上以順時針方向滾動。小球速度逐漸降低,最後落入某個對應着號碼和顏色的金屬格中。
輪盤賭算法和賭場中的輪盤賭相似,基本思想是把每一個個體的適應度按比例轉換爲被選中的機率。假設種羣中有 個個體, ,第 個個體的適應度是 ,羣體的總適應度是全部個體適應度之和,用 表示。一個顯而易見的策略是用 表示第 個個體被選中的機率。若是盜竊方案的初始種羣是['10010', '00010', '10100', '01100', '11010'],那麼每一個個體被選中的機率:
根據上表製做一個輪盤,每一個機率都至關於輪盤中的一個方格,機率值越大,方格也越大:
種羣中的適應度較高的精英(適應度較高的個體)理應得到較大的選中機率,但某個種羣的最優解並不必定在全局最優解附近:
輪盤賭策略可讓全部個體均有概率被選中,在重視精英的同時,也給非精英們留下一點生存的機會。如何在計算機上實現這種策略呢?
咱們首先按照個體的序號,順序地計算每一個個體的機率分佈,用qi 表示:
這就進一步獲得了下表:
因爲機率值是四捨五入,所以最後一個個體的機率分佈值不是1,在選擇的過程當中多少會有一點吃虧。
接下來將全部qi映射到一個數軸上,每一段都表示一個個體:
最後讓小球在輪盤上轉動。用一個區間在[0,1]的隨機數r模擬這個小球,r將落在數軸的某一點上。若r≤q1,則第1個個體被選中;若qk-1≤r≤qk (2≤k≤n)則第k個個體選中。很明顯,在兩個相鄰個體間,機率分佈值相差越懸殊,後面的個體被選中的機率越大。圖7.9中,r落在0.74和1.01之間的機率最大,所以5號個體最容易被選中;反之,2號個體最容易落選。反覆迭代n次後,便獲得了下一代種羣,它們中會存在某些重複的個體,種羣大小和上一代一致。
1 def pop_fitness(population): 2 ''' 計算種羣中每一個個體的適應度 ''' 3 return [fitness_fun(code) for code in population] 4 5 def selection_roulette(population): 6 ''' 7 輪盤賭選擇算法 8 :param population: 種羣 9 :return: 下一代種羣 10 ''' 11 f_list = pop_fitness(population) # 每一個個體的適應度 12 f_sum = [] # 第i個元素表示前i個體的適應度之和 13 for i in range(POPULATION_SIZE): 14 if i == 0: 15 f_sum.append(f_list[i]) 16 else: 17 f_sum.append(f_sum[i - 1] + f_list[i]) 18 S = sum(f_list) # 種羣的總適應度 19 q = [f / S for f in f_sum] # 每一個個體的機率分佈 20 21 pop_next = [] # 下一代種羣 22 for i in range(POPULATION_SIZE): 23 r = random.random() # 在[0,1]區間內產生一個均勻分佈的隨機數r 24 if r <= q[0]: # 若 r <= q0,則第1個個體被選中 25 pop_next.append(population[0]) 26 for k in range(1, POPULATION_SIZE): 27 if q[k - 1] < r <= q[k]: # 若q[k - 1] < r <= q[k],則第k個個體被選中 28 pop_next.append(population[k]) 29 break 30 return pop_next
一種可能的新種羣是['11010', '01100', '11010', '00010', '10010'],能夠看到,5號被選中了2次,1號、2號、4號各被選中1次,3號在生存競爭中慘遭淘汰。
2. 精英保留法
輪盤賭選擇易於理解,但在後續的交叉配對中,極可能把較好的基因代碼破壞掉,這就喪失了累積優良基因的目的,從而增長算法的迭代次數,延長算法的收斂時間。精英保留法避免了這個缺點,它會把種羣在進化過程當中出現的精英羣體不進行配對交叉而直接複製到下一代,從而保證了優良的基因代碼不會被破壞:
1 def selection_elitism(population): 2 ''' 精英選擇策略''' 3 # 按適應度從大到小排序 4 pop_parents = sorted(population, key=lambda x: fitness_fun(x), reverse=True) 5 # 選擇種羣中適應度最高的40%做爲精英 6 return pop_parents[0: int(POPULATION_SIZE * 0.4)]
在精英保留法中,經過交叉環節使下一代種羣中的個體數量與上一代保持一致。精英保留法不給平庸的個體留一點活路,極端狀況下,某些精英會從初代一直存活到算法結束,這可能使算法最終陷入局部最優解,能夠說,精英保留法的缺點正是輪盤賭法的優勢。
3. 錦標賽法
錦標賽法從種羣中抽取必定數量的個體,讓它們在錦標賽中進行競爭,其中勝出的個體進入下一代種羣。重複該操做,直到新的種羣規模達到原來的種羣規模。每次參加錦標賽的個體數量稱爲錦標賽的「元」,一般使用二元錦標賽法。
1 def selection_tournament(population): 2 '''錦標賽選擇策略''' 3 pop_next = [] # 下一代種羣 4 for i in range(POPULATION_SIZE): 5 tour_list = random.choices(population, k=2) # 二元錦標賽 6 winner = max(tour_list, key=lambda x: fitness_fun(x)) 7 pop_next.append(winner) 8 return pop_next
在錦標賽法中,每一個個體都有與其它實力至關的個體競爭的機會,即便最差的個體,也可能匹配到本身做爲競爭對手,所以錦標賽法保留了輪盤賭法的優勢,不易陷入局部最優解。同時,因爲錦標賽法還具備較低的複雜度、易並行化處理,而且不須要對全部的適應度值進行排序,所以錦標賽法成爲遺傳算法中最流行的選擇策略
交叉又稱配對或重組,是指把兩個父代個體的部分基因編碼加以交換、重組而生成新個體的操做,其目的是保證種羣的穩定性,使種羣朝着最優解的方向進化。經常使用的交叉策略有單點交叉和兩點交叉等。
1. 單點交叉
單點交叉又稱一點交叉,它在個體基因序列中隨機設置一個交叉點,而後隨機選擇兩個個體作爲父代個體,相互交換它們交叉點後面的那部分基因塊,從而產生兩個新的個體:
1 def crossover_onepoint(population): 2 ''' 單點交叉 ''' 3 pop_new = [] # 新種羣 4 code_len = len(population[0]) # 基因編碼的長度 5 for i in range(POPULATION_SIZE): 6 p = random.randint(1, code_len - 1) # 隨機選擇一個交叉點 7 r_list = random.choices(population, k=2) # 選擇兩個隨機的個體 8 pop_new.append(r_list[0][0:p] + r_list[1][p:]) 9 return pop_new
2. 兩點交叉
兩點交叉又稱局部交叉,它在個體基因序列中隨機設置兩個交叉點,而後隨機選擇兩個個體作爲父代個體,相互交換它們交叉點之間的那部分基因塊,從而產生兩個新的個體:
def crossover_twopoint(population): pop_new = [] # 新種羣 code_len = len(population[0]) # 基因編碼的長度 for i in range(POPULATION_SIZE): # 選擇兩個隨機的交叉點 p1, p2 = random.randint(0, code_len - 1), random.randint(0, code_len - 1) if p1 > p2: p1, p2 = p2, p1 r_list = random.choices(population, k=2) # 選擇兩個隨機的個體 pop_new.append(r_list[0][0:p1] + r_list[1][p1:p2] + r_list[0][p2:]) return pop_new
交叉算法須要與基因編碼配配合,在後續的示例中,咱們將看到一些配合方案,同時也將看到其它的交叉策略。
變異在遺傳算法中只是用來產生新個體的輔助手段,它會改變個體的某一部分基因編碼。和生物中的基因突變同樣,變異算法的變異率應該控制在較低的頻率。做爲交叉運算的補充算法,變異對新種羣的影響應該遠小於交叉。經常使用的變異算法有單點變異和均勻變異。
1. 單點變異
單點變異較爲簡單,它在隨機個體的基因編碼中隨機選取一個進行改變:
1 def mutation_onepoint(population): 2 '''單點變異''' 3 code_len = len(population[0]) # 基因編碼的長度 4 mp = 0.2 # 變異率 5 for i, r in enumerate(population): 6 if random.random() < mp: 7 p = random.randint(0, code_len - 1) # 隨機變異點 8 population[i] = r[0:p] + str(int(r[p]) ^ 1) + r[p + 1:]
變異操做直接修改了種羣中的個體,並用「異或」操做模擬了變異,若是變異點的編碼是1,那麼變異後該點變爲0;若是變異點的編碼是0,那麼變異後該點變爲1。
2. 均勻變異
均勻變異又稱一致性變異,它與單點變異相似,不一樣之處在於,均勻變異中隨機個體的每一位基因代碼都會隨機發生變異。
1 def mutation_uniform(population): 2 ''' 多點變異 ''' 3 code_len = len(population[0]) # 基因編碼的長度 4 mp = 0.2 # 變異率 5 for i, r in enumerate(population): 6 r_list = list(r) # 將個體的基因編碼轉換成列表 7 for p in range(code_len): # 遍歷個體的每一位 8 if random.random() < mp: 9 r_list[p] = str(int(r_list[p]) ^ 1) 10 population[i] = ''.join(r_list)
python對於字符串的操做要比c或java笨拙一些,你能夠用列表代替字符串表示基因編碼。
如今已經完成了全部鋪墊,能夠編寫遺傳算法的主體代碼:
1 def ga(selection_fun=selection_tournament, crossover_fun=crossover_onepoint, 2 mutation_fun=mutation_onepoint): 3 ''' 遺傳算法 4 :param selection_fun: 種羣選擇策略, 默認錦標賽策略 5 :param crossover_fun: 交叉策略,默認單點交叉 6 :param mutation_fun: 變異策略,默認單點變異 7 :return: 最優個體 8 ''' 9 population = init_population() # 構建初始化種羣 10 sum_fitness = sum(pop_fitness(population)) # 種羣的總適應度 11 i = 0 12 while i < 15: # 若是連續15代沒有改進,結束算法 13 pop_next = selection_fun(population) # 選擇種羣 14 pop_new = crossover_fun(pop_next) # 交叉 15 mutation_fun(pop_new) # 變異 16 sum_fitness_new = sum(pop_fitness(pop_new)) # 新種羣的總適應度 17 if sum_fitness < sum_fitness_new: 18 sum_fitness = sum_fitness_new 19 i = 0 20 else: 21 i += 1 22 population = pop_new 23 # 按適應度值從大到小排序 24 population = sorted(population, key=lambda x: fitness_fun(x), reverse=True) 25 # 返回最優的個體 26 return population[0] 27 28 if __name__ == '__main__': 29 best = ga() 30 print_solution(best)
這裏用種羣的總適應度做爲評判標準,若是下一代的總適應度高於上一代,說明進化出了更強的下一代;當連續5代沒有產生更強的後代時,結束算法。一種可能的運行結果:
交叉和變異並不必定都要執行,可讓種羣中的大部分個體經過交叉產生,另外一小部分經過變異產生。
遺傳算法是否可以找到最優解,和初始種羣、適應度評估函數、種羣選擇策略、交叉策略、變異策略、終止條件都有關係,每次運行的結果均可能不一樣,但整體上可以獲得一個較爲滿意的解。在實際應用中,咱們能夠選擇不一樣的參數屢次運行,挑選最好的一個做爲最終結果。
完整代碼:
1 import random 2 3 GOODS_LIST = [('A', 3, 4), ('B', 4, 5), ('C', 7, 10), ('D', 8, 11), ('E', 9, 13)] 4 V_BAG = 20 # 揹包的容量 5 POPULATION_SIZE = 5 # 種羣數量 6 7 def print_solution(code): 8 ''' 9 打印解決方案 10 :param code: 二進制基因編碼 11 ''' 12 size_sum, value_sum, = 0, 0 13 for i, x in enumerate(code): 14 if x == '1': 15 gtype, size, value = GOODS_LIST[i] 16 print('\t{0}\t體積:{1}\t價值:{2}'.format(gtype, size, value)) 17 size_sum += size 18 value_sum += value 19 print('\t揹包內的物品整體積:{0},\t總價值:{1}'.format(size_sum, value_sum)) 20 21 def init_population(): 22 ''' 構造初始種羣 ''' 23 population = [] 24 code_len = len(GOODS_LIST) # 編碼長度 25 for i in range(POPULATION_SIZE): 26 population.append(''.join(random.choices(['0', '1'], k=code_len))) 27 return population 28 29 def fitness_fun(code): 30 ''' 31 適應度評估 32 :param code: 二進制基因編碼 33 :return: 適應度評估值 34 ''' 35 size_sum, sum_value = 0, 0 # 物品的整體積,物品的總價值 36 for i, x in enumerate(code): 37 if x == '1': 38 _, size, value = GOODS_LIST[i] 39 size_sum += size 40 sum_value += value 41 if size_sum > V_BAG: # 揹包被撐爆時,適應度爲0 42 sum_value = 0 43 break 44 return sum_value 45 46 def pop_fitness(population): 47 ''' 計算種羣中每一個個體的適應度 ''' 48 return [fitness_fun(code) for code in population] 49 50 def selection_roulette(population): 51 ''' 52 輪盤賭選擇算法 53 :param population: 種羣 54 :return: 下一代種羣 55 ''' 56 f_list = pop_fitness(population) # 每一個個體的適應度 57 f_sum = [] # 第i個元素表示前i個體的適應度之和 58 for i in range(POPULATION_SIZE): 59 if i == 0: 60 f_sum.append(f_list[i]) 61 else: 62 f_sum.append(f_sum[i - 1] + f_list[i]) 63 S = sum(f_list) # 種羣的總適應度 64 q = [f / S for f in f_sum] # 每一個個體的機率分佈 65 66 pop_next = [] # 下一代種羣 67 for i in range(POPULATION_SIZE): 68 r = random.random() # 在[0,1]區間內產生一個均勻分佈的隨機數r 69 if r <= q[0]: # 若 r <= q0,則第1個個體被選中 70 pop_next.append(population[0]) 71 for k in range(1, POPULATION_SIZE): 72 if q[k - 1] < r <= q[k]: # 若q[k - 1] < r <= q[k],則第k個個體被選中 73 pop_next.append(population[k]) 74 break 75 return pop_next 76 77 def selection_elitism(population): 78 ''' 精英選擇策略''' 79 # 按適應度從大到小排序 80 pop_parents = sorted(population, key=lambda x: fitness_fun(x), reverse=True) 81 # 選擇種羣中適應度最高的40%做爲精英 82 return pop_parents[0: int(POPULATION_SIZE * 0.4)] 83 84 def selection_tournament(population): 85 '''錦標賽選擇策略''' 86 pop_next = [] # 下一代種羣 87 for i in range(POPULATION_SIZE): 88 tour_list = random.choices(population, k=2) # 二元錦標賽 89 winner = max(tour_list, key=lambda x: fitness_fun(x)) 90 pop_next.append(winner) 91 return pop_next 92 93 def crossover_onepoint(population): 94 ''' 單點交叉 ''' 95 pop_new = [] # 新種羣 96 code_len = len(population[0]) # 基因編碼的長度 97 for i in range(POPULATION_SIZE): 98 p = random.randint(1, code_len - 1) # 隨機選擇一個交叉點 99 r_list = random.choices(population, k=2) # 選擇兩個隨機的個體 100 pop_new.append(r_list[0][0:p] + r_list[1][p:]) 101 return pop_new 102 103 def crossover_twopoint(population): 104 pop_new = [] # 新種羣 105 code_len = len(population[0]) # 基因編碼的長度 106 for i in range(POPULATION_SIZE): 107 # 選擇兩個隨機的交叉點 108 p1, p2 = random.randint(0, code_len - 1), random.randint(0, code_len - 1) 109 if p1 > p2: 110 p1, p2 = p2, p1 111 r_list = random.choices(population, k=2) # 選擇兩個隨機的個體 112 pop_new.append(r_list[0][0:p1] + r_list[1][p1:p2] + r_list[0][p2:]) 113 return pop_new 114 115 def mutation_onepoint(population): 116 '''單點變異''' 117 code_len = len(population[0]) # 基因編碼的長度 118 mp = 0.2 # 變異率 119 for i, r in enumerate(population): 120 if random.random() < mp: 121 p = random.randint(0, code_len - 1) # 隨機變異點 122 population[i] = r[0:p] + str(int(r[p]) ^ 1) + r[p + 1:] 123 124 def mutation_uniform(population): 125 ''' 多點變異 ''' 126 code_len = len(population[0]) # 基因編碼的長度 127 mp = 0.2 # 變異率 128 for i, r in enumerate(population): 129 r_list = list(r) # 將個體的基因編碼轉換成列表 130 for p in range(code_len): # 遍歷個體的每一位 131 if random.random() < mp: 132 r_list[p] = str(int(r_list[p]) ^ 1) 133 population[i] = ''.join(r_list) 134 135 def ga(selection_fun=selection_tournament, crossover_fun=crossover_onepoint, 136 mutation_fun=mutation_onepoint): 137 ''' 遺傳算法 138 :param selection_fun: 種羣選擇策略, 默認錦標賽策略 139 :param crossover_fun: 交叉策略,默認單點交叉 140 :param mutation_fun: 變異策略,默認單點變異 141 :return: 最優個體 142 ''' 143 population = init_population() # 構建初始化種羣 144 sum_fitness = sum(pop_fitness(population)) # 種羣的總適應度 145 i = 0 146 while i < 15: # 若是連續15代沒有改進,結束算法 147 pop_next = selection_fun(population) # 選擇種羣 148 pop_new = crossover_fun(pop_next) # 交叉 149 mutation_fun(pop_new) # 變異 150 sum_fitness_new = sum(pop_fitness(pop_new)) # 新種羣的總適應度 151 if sum_fitness < sum_fitness_new: 152 sum_fitness = sum_fitness_new 153 i = 0 154 else: 155 i += 1 156 population = pop_new 157 # 按適應度值從大到小排序 158 population = sorted(population, key=lambda x: fitness_fun(x), reverse=True) 159 # 返回最優的個體 160 return population[0] 161 162 if __name__ == '__main__': 163 best = ga() 164 print_solution(best)
在下一章中,咱們將嘗試用遺傳算法進行上一章的宿舍分配工做,同時展現不一樣於二進制的編碼方案和另外一些可以與之配合的交叉策略。
做者:我是8位的