[work] 演算法學習筆記 (爬山法,模擬退火演算法,遺傳演算法)
在優化問題中,有兩個關鍵點
代價函式
確定問題的形式和規模之後,根據不同的問題,選擇要優化的目標。如本文涉及的兩個問題中,一個優化目標是使得航班選擇最優,共計12個航班,要使得總的票價最少且每個人的等待時間之和最小。第二個問題是學生選擇宿舍的問題,每個學生可以實現填報志願,如果安排的宿舍與志願完全一致,則代價為0,與第二志願一致,代價為1,如果沒有和志願一致,代價為3。 故,抽象問題的能力很重要,如何將自己要優化的目標量化的表達出來,是解決優化函式的關鍵。如在普通的數值優化問題中,可選擇當前值與目標值距離的絕對之差,或者使用平方損失函式,均可。
值域
值域是確定搜尋的範圍。一個可行解的範圍是多少,比如學生選宿舍的問題,一共學校有100間宿舍,編號為1-100,那麼這個數值優化問題的每個可行解的範圍均在100之內。不同的問題對應不同的可行解,也要具體問題具體分析。
隨機搜尋演算法
隨機搜尋演算法是最簡單的優化搜尋演算法,它只適用於代價函式在值域範圍內沒有任何變化規律的情況。即找不到任何使得代價下降的梯度和極小值點。 我們只需要在值域範圍內生成足夠多的可行解,然後分別計算每個可行解的代價,根據代價選擇一個最小的可行解作為隨機搜尋的最優解即可。
# 搜尋方法1: 隨機搜尋演算法 # 函式會作1000次隨機猜測,記錄總代價最低的方法. domain為航班號的範圍(0-9),共有5個人,因此共有10項 def randomoptimize(self, domain): best_sol = [] bestcost = 99999 for i in range(1000): sol = [0] * len(domain) for j in range(len(domain) / 2): # 人數 sol[2 * j] = random.randint(domain[2 * j][0], domain[2 * j][1]) sol[2 * j + 1] = random.randint(domain[2 * j + 1][0], domain[2 * j + 1][1]) print sol[:] newcost = self.schedulecost(sol) if newcost < bestcost: bestcost = newcost best_sol = sol else: continue self.printschedule(best_sol) print "隨機搜尋演算法的結果的最小代價是:", bestcost return best_sol
爬山法
爬山法假設當前解和周圍的解是有變化規律的,如,當前解得下方有一個代價較小的解,則我們就認為,沿著這個方向走,解會越來越小。步驟為:首先選擇一個解作為種子解,每次尋找與這個解相近的解,如果相近的解中有代價更小的解,則把這個解作為種子解。而如果周圍的解都比該解的代價大,則表示已經到達了局部極小值點,搜尋停止。
# 搜尋演算法2:爬山法 # 首先隨機選擇一個解作為種子解,每次尋找這個種子相近的解,如果相近的解有代價更小的解,則把這個新的解作為種子 # 依次迴圈進行,當迴圈到某種子附近的解都比該種子的代價大時,說明到達了局部極小值點,搜尋結束 def hillclimb(self, domain): # 隨機產生一個航班序列作為初始種子 seed = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))] while 1: neighbor = [] # 迴圈改變解的每一個值產生一個臨近解的列表 for i in range(len(domain)): # 下列判斷是為了將某一位加減1後不超出domain的範圍 # print seed if seed[i] > domain[i][0]: newneighbor = seed[0:i] + [seed[i] - 1] + seed[i + 1:] # print newneighbor[:] neighbor.append(newneighbor) if seed[i] < domain[i][1]: newneighbor = seed[0:i] + [seed[i] + 1] + seed[i + 1:] # print newneighbor[:] neighbor.append(newneighbor) # 對所有的臨近解計算代價,排序,得到代價最小的解 neighbor_cost = sorted( [(s, self.schedulecost(s)) for s in neighbor], key=lambda x: x[1]) # 如果新的最小代價 > 原種子代價,則跳出迴圈 if neighbor_cost[0][1] > self.schedulecost(seed): break # 新的代價更小的臨近解作為新的種子 seed = neighbor_cost[0][0] print "newseed = ", seed[:], " 代價:", self.schedulecost(seed) # 輸出 self.printschedule(seed) print "爬山法得到的解的最小代價是", self.schedulecost(seed) return seed
模擬退火演算法
從一個問題的原始解開始,用一個變數代表溫度,這一溫度開始時非常高,而後逐步減低。在每一次迭代期間,演算法會隨機選中題解中的某個數字,使其發生細微變化,而後計算該解的代價。關鍵的地方在於計算出該解的代價後,如果決定是否接受該解。 如果新的成本更低,則新的題解就會變成當前題解,這與爬山法類似;如果新的成本更高,則新的題解與概率 P 被接受。這一概率會隨著溫度T的降低而降低。即演算法開始時,可以接受表現較差的解,隨著退火過程中溫度的不斷下降,演算法越來越不可以接受較差的解,知道最後,它只會接受更優的解。 ** 其中P = exp[-(newcost - oldcost)/ T ] 其中newcost是新解的成本,oldcost是當前成本,T為當前溫度。演算法以概率P接受新的解。**
# 搜尋演算法4:模擬退火演算法
# 引數:T代表原始溫度,cool代表冷卻率,step代表每次選擇臨近解的變化範圍
# 原理:退火演算法以一個問題的隨機解開始,用一個變量表示溫度,這一溫度開始時非常高,而後逐步降低
# 在每一次迭代期間,算啊會隨機選中題解中的某個數字,然後朝某個方向變化。如果新的成本值更
# 低,則新的題解將會變成當前題解,這與爬山法類似。不過,如果成本值更高的話,則新的題解仍
# 有可能成為當前題解,這是避免區域性極小值問題的一種嘗試。
# 注意:演算法總會接受一個更優的解,而且在退火的開始階段會接受較差的解,隨著退火的不斷進行,演算法
# 原來越不能接受較差的解,直到最後,它只能接受更優的解。
# 演算法接受較差解的概率 P = exp[-(highcost-lowcost)/temperature]
def annealingoptimize(self, domain, T=10000.0, cool=0.98, step=1):
# 隨機初始化值
vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
# 迴圈
while T > 0.1:
# 選擇一個索引值
i = random.randint(0, len(domain) - 1)
# 選擇一個改變索引值的方向
c = random.randint(-step, step) # -1 or 0 or 1
# 構造新的解
vecb = vec[:]
vecb[i] += c
if vecb[i] < domain[i][0]: # 判斷越界情況
vecb[i] = domain[i][0]
if vecb[i] > domain[i][1]:
vecb[i] = domain[i][1]
# 計算當前成本和新的成本
cost1 = self.schedulecost(vec)
cost2 = self.schedulecost(vecb)
# 判斷新的解是否優於原始解 或者 演算法將以一定概率接受較差的解
if cost2 < cost1 or random.random() < math.exp(-(cost2 - cost1) / T):
vec = vecb
T = T * cool # 溫度冷卻
print vecb[:], "代價:", self.schedulecost(vecb)
self.printschedule(vec)
print "模擬退火演算法得到的最小代價是:", self.schedulecost(vec)
return vec
遺傳演算法
隨機生成一組解,成為一個種群
- 直接遺傳 將當前種群中代價最小的一部分解,如 40% 進行直接遺傳,傳入下一代種群。
- 變異 從題解中隨機選取一個數字,對其進行微小的,簡單的改變。
- 交叉 選取最優解中的兩個解,將他們按照某種方式進行結合
通過上述三種方法,從上一代種群中構建出了下一代種群。而後,這一過程重複進行,知道達到了指定的迭代次數,或者連續數代都沒有改善種群,則整個過程就結束了。
# 搜尋演算法5: 遺傳演算法
# 原理: 首先隨機生成一組解,我們稱之為種群,在優化過程的每一步,演算法會計算整個種群的成本函式,
# 從而得到一個有關題解的有序列表。隨後根據種群構造進化的下一代種群,方法如下:
# 遺傳:從當前種群中選出代價最優的一部分加入下一代種群,稱為“精英選拔”
# 變異:對一個既有解進行微小的、簡單的、隨機的修改
# 交叉:選取最優解中的兩個解,按照某種方式進行交叉。方法有單點交叉,多點交叉和均勻交叉
# 一個種群是通過對最優解進行隨機的變異和配對處理構造出來的,它的大小通常與舊的種群相同,爾後,這一過程會
# 一直重複進行————新的種群經過排序,又一個種群被構造出來,如果達到指定的迭代次數之後題解都沒有得
# 改善,整個過程就結束了
# 引數:
# popsize-種群數量 step-變異改變的大小 mutprob-交叉和變異的比例 elite-直接遺傳的比例 maxiter-最大迭代次數
def geneticoptimize(self, domain, popsize=50, step=1, mutprob=0.2, elite=0.2, maxiter=100):
# 變異操作的函式
def mutate(vec):
i = random.randint(0, len(domain) - 1)
if random.random() < 0.5 and vec[i] > domain[i][0]:
return vec[0:i] + [vec[i] - step] + vec[i + 1:]
elif vec[i] < domain[i][1]:
return vec[0:i] + [vec[i] + step] + vec[i + 1:]
# 交叉操作的函式(單點交叉)
def crossover(r1, r2):
i = random.randint(0, len(domain) - 1)
return r1[0:i] + r2[i:]
# 構造初始種群
pop = []
for i in range(popsize):
vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
pop.append(vec)
# 每一代中有多少勝出者
topelite = int(elite * popsize)
# 主迴圈
for i in range(maxiter):
scores = [(self.schedulecost(v), v) for v in pop]
scores.sort()
ranked = [v for (s, v) in scores] # 解按照代價由小到大的排序
# 優質解遺傳到下一代
pop = ranked[0: topelite]
# 如果當前種群數量小於既定數量,則新增變異和交叉遺傳
while len(pop) < popsize:
# 隨機數小於 mutprob 則變異,否則交叉
if random.random() < mutprob: # mutprob控制交叉和變異的比例
# 選擇一個個體
c = random.randint(0, topelite)
# 變異
pop.append(mutate(ranked[c]))
else:
# 隨機選擇兩個個體進行交叉
c1 = random.randint(0, topelite)
c2 = random.randint(0, topelite)
pop.append(crossover(ranked[c1], ranked[c2]))
# 輸出當前種群中代價最小的解
print scores[0][1], "代價:", scores[0][0]
self.printschedule(scores[0][1])
print "遺傳演算法求得的最小代價:", scores[0][0]
return scores[0][1]
書中給出了關於學生宿舍選擇的一個具體例子,以下是程式碼
# -*- coding: utf-8 -*-
__author__ = 'Bai Chenjia'
import random
import math
"""
宿舍分配問題,屬於搜尋優化問題。優化方法使用optimization.py中使用的
隨機搜尋、爬山法、模擬退火法、遺傳演算法等. 但題解的描述比之前的問題複雜
"""
class dorm:
def __init__(self):
# 代表宿舍,每個宿舍有兩個隔間可用
self.dorms = ['Zeus', 'Athena', 'Hercules', 'Bacchus', 'Pluto']
# 代表選擇,第一列代表人名,第二列和第三列代表該學生的首選和次選
self.prefs = [('Toby', ('Bacchus', 'Hercules')),
('Steve', ('Zeus', 'Pluto')),
('Karen', ('Athena', 'Zeus')),
('Sarah', ('Zeus', 'Pluto')),
('Dave', ('Athena', 'Bacchus')),
('Jeff', ('Hercules', 'Pluto')),
('Fred', ('Pluto', 'Athena')),
('Suzie', ('Bacchus', 'Hercules')),
('Laura', ('Bacchus', 'Hercules')),
('James', ('Hercules', 'Athena'))]
# 題解的表示法:
# 設想每個宿舍有兩個槽,本例中共有5個宿舍,則共有10個槽,我們將每名學生依序安置於各空槽
# 內————則第一名學生有10種選擇,解的取值範圍為0-9;第二名學生有9種選擇,解的取值範圍為
# 0-8,第三名學生解的取值範圍是0-7,以此類推,最後一名學生只有一個可選。
# 按照上述題解的表示法初始化題解範圍:
self.domain = [(0, 2*len(self.dorms)-1-i) for i in range(len(self.prefs))]
# 根據題解序列vec打印出最終宿舍分配方案
# 注意,輸出一個槽後表明該槽已經用過,需將該槽刪除
def printsolution(self, vec):
slots = []
# slots = [0,0,1,1,2,2,3,3,4,4]
for i in range(len(self.dorms)):
slots.append(i)
slots.append(i)
# 迴圈題解
print "選擇方案是:"
for i in range(len(vec)):
index = slots[vec[i]]
print self.prefs[i][0], self.dorms[index]
del slots[vec[i]]
# 代價函式: 如果學生當前安置的宿舍使其首選則代價為0,是其次選則代價為1,否則代價為3
# 注意,輸出一個槽後表明該槽已經用過,需將該槽刪除
def dormcost(self, vec):
cost = 0
# 建立槽
slots = []
for i in range(len(self.dorms)):
slots.append(i)
slots.append(i)
# 迴圈題解
for i in range(len(vec)):
index = slots[vec[i]]
if self.dorms[index] == self.prefs[i][1][0]:
cost += 0
elif self.dorms[index] == self.prefs[i][1][1]:
cost += 1
else:
cost += 3
del slots[vec[i]]
return cost
"""
下列函式與 optimization 中函式相同,只不過代價函式和輸出函式用本問題的輸出函式
"""
# 搜尋方法1: 隨機搜尋演算法
# 函式會作1000次隨機猜測,記錄總代價最低的方法. domain為航班號的範圍(0-9),共有5個人,因此共有10項
def randomoptimize(self, domain):
best_sol = []
bestcost = 99999
for i in range(1000):
sol = [0] * len(domain)
for j in range(len(domain) / 2): # 人數
sol[2 * j] = random.randint(domain[2 * j][0], domain[2 * j][1])
sol[2 * j + 1] = random.randint(domain[2 * j + 1][0], domain[2 * j + 1][1])
print sol[:]
newcost = self.dormcost(sol)
if newcost < bestcost:
bestcost = newcost
best_sol = sol
else:
continue
self.printsolution(best_sol)
print "隨機搜尋演算法的結果的最小代價是:", bestcost
return best_sol
# 搜尋演算法2:爬山法
# 首先隨機選擇一個解作為種子解,每次尋找這個種子相近的解,如果相近的解有代價更小的解,則把這個新的解作為種子
# 依次迴圈進行,當迴圈到某種子附近的解都比該種子的代價大時,說明到達了局部極小值點,搜尋結束
def hillclimb(self, domain):
# 隨機產生一個航班序列作為初始種子
seed = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
while 1:
neighbor = []
# 迴圈改變解的每一個值產生一個臨近解的列表
for i in range(len(domain)):
# 下列判斷是為了將某一位加減1後不超出domain的範圍
# print seed
if seed[i] > domain[i][0]:
newneighbor = seed[0:i] + [seed[i] - 1] + seed[i + 1:]
# print newneighbor[:]
neighbor.append(newneighbor)
if seed[i] < domain[i][1]:
newneighbor = seed[0:i] + [seed[i] + 1] + seed[i + 1:]
# print newneighbor[:]
neighbor.append(newneighbor)
# 對所有的臨近解計算代價,排序,得到代價最小的解
neighbor_cost = sorted(
[(s, self.dormcost(s)) for s in neighbor], key=lambda x: x[1])
# 如果新的最小代價 > 原種子代價,則跳出迴圈
if neighbor_cost[0][1] > self.dormcost(seed):
break
# 新的代價更小的臨近解作為新的種子
seed = neighbor_cost[0][0]
print "newseed = ", seed[:], " 代價:", self.dormcost(seed)
# 輸出
self.printsolution(seed)
print "爬山法得到的解的最小代價是", self.dormcost(seed)
return seed
# 搜尋演算法4:模擬退火演算法
# 引數:T代表原始溫度,cool代表冷卻率,step代表每次選擇臨近解的變化範圍
# 原理:退火演算法以一個問題的隨機解開始,用一個變量表示溫度,這一溫度開始時非常高,而後逐步降低
# 在每一次迭代期間,算啊會隨機選中題解中的某個數字,然後朝某個方向變化。如果新的成本值更
# 低,則新的題解將會變成當前題解,這與爬山法類似。不過,如果成本值更高的話,則新的題解仍
# 有可能成為當前題解,這是避免區域性極小值問題的一種嘗試。
# 注意:演算法總會接受一個更優的解,而且在退火的開始階段會接受較差的解,隨著退火的不斷進行,演算法
# 原來越不能接受較差的解,直到最後,它只能接受更優的解。
# 演算法接受較差解的概率 P = exp[-(highcost-lowcost)/temperature]
def annealingoptimize(self, domain, T=10000.0, cool=0.98, step=1):
# 隨機初始化值
vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
# 迴圈
while T > 0.1:
# 選擇一個索引值
i = random.randint(0, len(domain) - 1)
# 選擇一個改變索引值的方向
c = random.randint(-step, step) # -1 or 0 or 1
# 構造新的解
vecb = vec[:]
vecb[i] += c
if vecb[i] < domain[i][0]: # 判斷越界情況
vecb[i] = domain[i][0]
if vecb[i] > domain[i][1]:
vecb[i] = domain[i][1]
# 計算當前成本和新的成本
cost1 = self.dormcost(vec)
cost2 = self.dormcost(vecb)
# 判斷新的解是否優於原始解 或者 演算法將以一定概率接受較差的解
if cost2 < cost1 or random.random() < math.exp(-(cost2 - cost1) / T):
vec = vecb
T = T * cool # 溫度冷卻
print vecb[:], "代價:", self.dormcost(vecb)
self.printsolution(vec)
print "模擬退火演算法得到的最小代價是:", self.dormcost(vec)
return vec
# 搜尋演算法5: 遺傳演算法
# 原理: 首先隨機生成一組解,我們稱之為種群,在優化過程的每一步,演算法會計算整個種群的成本函式,
# 從而得到一個有關題解的有序列表。隨後根據種群構造進化的下一代種群,方法如下:
# 遺傳:從當前種群中選出代價最優的一部分加入下一代種群,稱為“精英選拔”
# 變異:對一個既有解進行微小的、簡單的、隨機的修改
# 交叉:選取最優解中的兩個解,按照某種方式進行交叉。方法有單點交叉,多點交叉和均勻交叉
# 一個種群是通過對最優解進行隨機的變異和配對處理構造出來的,它的大小通常與舊的種群相同,爾後,這一過程會
# 一直重複進行————新的種群經過排序,又一個種群被構造出來,如果達到指定的迭代次數之後題解都沒有得
# 改善,整個過程就結束了
# 引數:
# popsize-種群數量 step-變異改變的大小 mutprob-交叉和變異的比例 elite-直接遺傳的比例 maxiter-最大迭代次數
def geneticoptimize(self, domain, popsize=50, step=1, mutprob=0.2, elite=0.2, maxiter=100):
# 變異操作的函式
def mutate(vec):
i = random.randint(0, len(domain) - 1)
res = []
if random.random() < 0.5 and vec[i] > domain[i][0]:
res = vec[0:i] + [vec[i] - step] + vec[i + 1:]
elif vec[i] < domain[i][1]:
res = vec[0:i] + [vec[i] + step] + vec[i + 1:]
else:
res = vec
return res
# 交叉操作的函式(單點交叉)
def crossover(r1, r2):
i = random.randint(0, len(domain) - 1)
return r1[0:i] + r2[i:]
# 構造初始種群
pop = []
for i in range(popsize):
vec = [random.randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
pop.append(vec)
# 每一代中有多少勝出者
topelite = int(elite * popsize)
# 主迴圈
for i in range(maxiter):
if [] in pop:
print "***"
try:
scores = [(self.dormcost(v), v) for v in pop]
except:
print "pop!!", pop[:]
scores.sort()
ranked = [v for (s, v) in scores] # 解按照代價由小到大的排序
# 優質解遺傳到下一代
pop = ranked[0: topelite]
# 如果當前種群數量小於既定數量,則新增變異和交叉遺傳
while len(pop) < popsize:
# 隨機數小於 mutprob 則變異,否則交叉
if random.random() < mutprob: # mutprob控制交叉和變異的比例
# 選擇一個個體
c = random.randint(0, topelite)
# 變異
if len(ranked[c]) == len(self.prefs):
temp = mutate(ranked[c])
if temp == []:
print "******", ranked[c]
else:
pop.append(temp)
else:
# 隨機選擇兩個個體進行交叉
c1 = random.randint(0, topelite)
c2 = random.randint(0, topelite)
pop.append(crossover(ranked[c1], ranked[c2]))
# 輸出當前種群中代價最小的解
print scores[0][1], "代價:", scores[0][0]
self.printsolution(scores[0][1])
print "遺傳演算法求得的最小代價:", scores[0][0]
return scores[0][1]
if __name__ == '__main__':
dormsol = dorm()
#sol = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
#dormsol.printsolution(sol)
#dormsol.dormcost(sol)
domain = dormsol.domain
# 方法1:隨機猜測
#dormsol.randomoptimize(domain)
# 方法2:爬山法
#dormsol.hillclimb(domain)
# 方法3:模擬退火法
#dormsol.annealingoptimize(domain)
# 方法4:遺傳演算法
dormsol.geneticoptimize(domain)
三種演算法分析
這三種都是用來求解函式“最大值”問題的演算法,我們可以把函式曲線理解成一個一個山峰和山谷組成的山脈(如圖片所示)。那麼我們可以設想所得到的每一個解就是一隻青蛙,我們希望它們不斷的向著更高處跳去,直到跳到最高的山峰(儘管青蛙本身不見得願意那麼做)。所以求最大值的過程就轉化成一個“青蛙跳”的過程。
下面介紹介紹“青蛙跳”的幾種方式。
爬山演算法(最速上升爬山法):
從搜尋空間中隨機產生鄰近的點,從中選擇對應解最優的個體,替換原來的個體,不斷重複上述過程。因為只對“鄰近”的點作比較,所以目光比較“短淺”,常常只能收斂到離開初始位置比較近的區域性最優解上面。對於存在很多區域性最優點的問題,通過一個簡單的迭代找出全域性最優解的機會非常渺茫。(在爬山法中,青蛙最有希望到達最靠近它出發點的山頂,但不能保證該山頂是珠穆朗瑪峰,或者是一個非常高的山峰。因為一路上它只顧上坡,沒有下坡。)
模擬退火演算法:
這個方法來自金屬熱加工過程的啟發。在金屬熱加工過程中,當金屬的溫度超過它的熔點(Melting Point)時,原子就會激烈地隨機運動。與所有的其它的物理系統相類似,原子的這種運動趨向於尋找其能量的極小狀態。在這個能量的變遷過程中,開始時。溫度非常高,使得原子具有很高的能量。隨著溫度不斷降低,金屬逐漸冷卻,金屬中的原子的能量就越來越小,最後達到所有可能的最低點。利用模擬退火演算法的時候,讓演算法從較大的跳躍開始,使到它有足夠的“能量”逃離可能“路過”的區域性最優解而不至於限制在其中,當它停在全域性最優解附近的時候,逐漸的減小跳躍量,以便使其“落腳”到全域性最優解上。(在模擬退火中,青蛙喝醉了,而且隨機地大跳躍了很長時間。運氣好的話,它從一個山峰跳過山谷,到了另外一個更高的山峰上。但最後,它漸漸清醒了並朝著它所在的峰頂跳去。)
遺傳演算法:
模擬物競天擇的生物進化過程,通過維護一個潛在解的群體執行了多方向的搜尋,並支援這些方向上的資訊構成和交換。以面為單位的搜尋,比以點為單位的搜尋,更能發現全域性最優解。(在遺傳演算法中,有很多青蛙,它們降落到喜瑪拉雅山脈的任意地方。這些青蛙並不知道它們的任務是尋找珠穆朗瑪峰。但每過幾年,就在一些海拔高度較低的地方射殺一些青蛙,並希望存活下來的青蛙是多產的,在它們所處的地方生兒育女。)可以描述成這樣的一個故事:從前,有一大群青蛙,它們被莫名其妙的零散地遺棄於喜馬拉雅山脈。於是只好在那裡艱苦的生活。海拔低的地方瀰漫著一種無色無味的毒氣,海拔越高毒氣越稀薄。可是可憐的青蛙們對此全然不覺,還是習慣於活蹦亂跳。於是,不斷有青蛙死於海拔較低的地方,而越是在海拔高的青蛙越是能活得更久,也越有機會生兒育女。就這樣經過許多年,這些青蛙們竟然都不自覺地聚攏到了一個個的山峰上,可是在所有的青蛙中,只有聚攏到珠穆朗瑪峰的青蛙被帶回了美麗的澳洲。)
作者:卡爾是正太 連結:https://www.jianshu.com/p/46d845442afb 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。