遗传算法实践(十) VRPTW问题求解

问题描述

车辆配送模型(Vehicle routing problem)是指从配送中心用车辆把物资配送给顾客时,规划调用哪些车辆,按照何种顺序配送货物的问题。该问题通常假定配送中心与顾客的地理位置之间距离、配送用车辆的载重量和可行驶距离(时间)均为已知,以高效配送和减少成本为目标进行决策优化。

带时间窗的车辆配送模型(Vehicle routing problem with time window,VRPTW)是在VRP基础上额外附加配送时间约束条件产生的一个变种。在这类问题中,给定配送车辆须到达顾客点的最早时间和最迟时间,当车辆从一个或者多个配送中心向带时间窗约束的多个顾客点配送物资的时候,求使费用最低的车辆调度方案。

这个问题基于以下假设:

  • 所有距离采用欧几里得表示
  • 每个顾客只接受一辆车的一次配送
  • 从配送中心出发的车辆必须返回配送中心
  • 每辆车载重量相同
  • 一条路径上顾客的总需求量不能超过车辆的最大载重量
  • 每个顾客分别在各自时间窗内接受服务

定义j,k为需要服务的两个顾客编号,e为配送中心的车辆编号,C为顾客和仓库的集合。
参数:

c_{jk}: 从顾客j到顾客k的行驶距离

t_{jk}:从顾客j到顾客k的行驶时间

s_j:分配给顾客j的服务时间

d_j:顾客j的需求量

Y_e:车辆的最大载重量

g_j:允许到达顾客j处的最早时间

h_j:允许到达顾客j处的最晚时间

决策变量:

a_j:车辆到达顾客j处的时间

z_{ejk}:当车辆e被分配从顾客i运行到顾客j时,取1;否则取0

在给定了参数和定义了决策变量之后,VRPTW问题可以用数学模型表示为:
min\sum_{e\in E}\sum_{j\in C}\sum_{k \in C}c_{jk}z_{ejk} \\s.t.\begin{aligned} \sum_{e\in E}\sum_{j\in C}z_{ejk} = 1, \forall j,k \in C \backslash \{0\} \\ \sum_{j\in C\backslash \{0\}}\sum_{k\in C\backslash \{0\}}d_k z_{ejk} \le Y_e, \forall i,e \in E \\ \sum_{k\in C}z_{e0k}=1, \forall e \in E \\ \sum_{j\in C\backslash \{0\}}z_{ejk} - \sum_{k\in C\backslash \{0\}}z_{ejk}=0, \forall e \in E \\ \sum_{j\in C_i}z_{ejk}=1, \forall e\in E \\ \sum_{e=1}^{E}(a_j+s_j+t_{jk}+w_k)z_{ejk} \le a_{k}, \forall k \\ g_j \le (a_j+w_j) < h_j, \forall i,j \\ z_{ejk} = 0\ or\ 1, \forall e \in E, \forall j,k \in C \end{aligned}

VRPTW问题示例

示例为1个配送中心,位于坐标(15,12),30个顾客的问题,为简化问题,设配送中心只提供一种型号的卡车,卡车的最大载重为400,平均行驶速度为30单位距离/单位时间,各顾客的位置、需求量和时间窗约束如下表所示:

j (u_j,v_j) d_j g_j h_j j (u_j,v_j) d_j g_j h_j
1 (3,13) 50 3 4 16 (23,22) 51 8 9
2 (3,17) 50 4 5 17 (24,11) 20 4 6
3 (6,18) 60 5 7 18 (27,21) 20 7 11
4 (8,17) 30 2 9 19 (26,6) 20 10 15
5 (10,14) 90 10 11 20 (26,9) 30 13 14
6 (14,13) 10 12 13 21 (27,2) 30 17 18
7 (15,11) 20 11 13 22 (27,4) 30 16 18
8 (15,15) 10 1 5 23 (27,17) 10 10 12
9 (17,11) 30 11 15 24 (28,7) 60 13 14
10 (17,16) 20 8 9 25 (29,14) 30 5 8
11 (18,19) 30 10 12 26 (29,18) 20 6 8
12 (19,9) 10 15 16 27 (30,1) 30 7 9
13 (19,21) 10 18 20 28 (30,8) 40 3 6
14 (21,22) 10 1 7 29 (30,15) 20 4 5
15 (23,9) 40 6 9 30 (30,17) 20 6 9

遗传算法求解VRPTW问题

个体编码

遗传算法求解VRPTW的文献中有多种编码方式,这里对于个体采用自然数编码,0代表配送中心,1--n代表顾客;不同车辆的配送路线之间用0分隔(即每辆车都从仓库出发);对于有n个顾客,k辆车的VRP问题来说,染色体长度为n+k+1

例如配送中心有3辆车为8个客户服务,一条可能的染色体如下:
0, 7, 0, 1, 2, 3, 5, 0, 8, 4, 6, 0
这条染色体表示的三辆车的行驶路线为:
第一辆车:0-7-0
第二辆车:0-1-2-3-5-0
第三辆车:0-8-4-6-0

惩罚

在交叉和突变产生的子代中,可能会有两种违反约束的形式:

  • 车辆超载
  • 不能在时间窗口约束给出的最晚时间点内到达指定顾客处

对这两种违反约束的情况采用静态惩罚,考虑到时间窗口更容易被违反,对其施加较大的惩罚因子。对这两种约束违反的惩罚因子分别设置为10和500。

交叉

这里参考《基于电动汽车的带时间窗的路径优化问题研究》中给出的交叉操作,细节可以参考上一篇博文

突变

对选中的个体中各条子路线用2-opt算法优化

选择

  • 育种选择:binary锦标赛选择
  • 环境选择:采用精英保留策略,合并子代和父代后,选择数量等同于族群规模的个体

代码示例

完整代码如下:

## 环境设定
import numpy as np
import matplotlib.pyplot as plt
from deap import base, tools, creator, algorithms
import random

params = {
    'font.family': 'serif',
    'figure.dpi': 300,
    'savefig.dpi': 300,
    'font.size': 12,
    'legend.fontsize': 'small'
}
plt.rcParams.update(params)

from copy import deepcopy
#-----------------------------------
## 问题定义
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) # 最小化问题
# 给个体一个routes属性用来记录其表示的路线
creator.create('Individual', list, fitness=creator.FitnessMin) 

#-----------------------------------
## 个体编码
# 用字典存储所有参数 -- 配送中心坐标、顾客坐标、顾客需求、到达时间窗口、服务时间、车型载重量
dataDict = {}
# 节点坐标,节点0是配送中心的坐标
dataDict['NodeCoor'] = [(15,12), (3,13), (3,17), (6,18), (8,17), (10,14),
                           (14,13), (15,11), (15,15), (17,11), (17,16),
                            (18,19), (19,9), (19,21), (21,22), (23,9),
                            (23,22), (24,11), (27,21), (26,6), (26,9),
                            (27,2), (27,4), (27,17), (28,7), (29,14),
                            (29,18), (30,1), (30,8), (30,15), (30,17)
                           ]
# 将配送中心的需求设置为0
dataDict['Demand'] = [0,50,50,60,30,90,10,20,10,30,20,30,10,10,10,
                      40,51,20,20,20,30,30,30,10,60,30,20,30,40,20,20]
# 将配送中心的服务时间设置为0
dataDict['Timewindow'] = [(0,0), (3,4), (4,5), (5,7), (2,9), (10,11),
                         (12,13), (11,13), (1,5), (11,15), (8,9),
                          (10,12), (15,16), (18,20), (1,7), (6,9),
                          (8,9), (4,6), (7,11), (10,15), (13,14),
                          (17,18), (16,18), (10,12), (13,14), (5,8),
                          (6,8), (7,9), (3,6), (4,5), (6,9)
                         ]
dataDict['MaxLoad'] = 400
dataDict['ServiceTime'] = 1
dataDict['Velocity'] = 30 # 车辆的平均行驶速度

def genInd(dataDict = dataDict):
    '''生成个体, 对我们的问题来说,困难之处在于车辆数目是不定的'''
    nCustomer = len(dataDict['NodeCoor']) - 1 # 顾客数量
    perm = np.random.permutation(nCustomer) + 1 # 生成顾客的随机排列,注意顾客编号为1--n
    pointer = 0 # 迭代指针
    lowPointer = 0 # 指针指向下界
    permSlice = []
    # 当指针不指向序列末尾时
    while pointer < nCustomer -1:
        vehicleLoad = 0
        # 当不超载时,继续装载
        while (vehicleLoad < dataDict['MaxLoad']) and (pointer < nCustomer -1):
            vehicleLoad += dataDict['Demand'][perm[pointer]]
            pointer += 1
        if lowPointer+1 < pointer:
            tempPointer = np.random.randint(lowPointer+1, pointer)
            permSlice.append(perm[lowPointer:tempPointer].tolist())
            lowPointer = tempPointer
            pointer = tempPointer
        else:
            permSlice.append(perm[lowPointer::].tolist())
            break
    # 将路线片段合并为染色体
    ind = [0]
    for eachRoute in permSlice:
        ind = ind + eachRoute + [0]
    return ind
#-----------------------------------
## 评价函数
# 染色体解码
def decodeInd(ind):
    '''从染色体解码回路线片段,每条路径都是以0为开头与结尾'''
    indCopy = np.array(deepcopy(ind)) # 复制ind,防止直接对染色体进行改动
    idxList = list(range(len(indCopy)))
    zeroIdx = np.asarray(idxList)[indCopy == 0]
    routes = []
    for i,j in zip(zeroIdx[0::], zeroIdx[1::]):
        routes.append(ind[i:j]+[0])
    return routes

def calDist(pos1, pos2):
    '''计算距离的辅助函数,根据给出的坐标pos1和pos2,返回两点之间的距离
    输入: pos1, pos2 -- (x,y)元组
    输出: 欧几里得距离'''
    return np.sqrt((pos1[0] - pos2[0])*(pos1[0] - pos2[0]) + (pos1[1] - pos2[1])*(pos1[1] - pos2[1]))

#
def loadPenalty(routes):
    '''辅助函数,因为在交叉和突变中可能会产生不符合负载约束的个体,需要对不合要求的个体进行惩罚'''
    penalty = 0
    # 计算每条路径的负载,取max(0, routeLoad - maxLoad)计入惩罚项
    for eachRoute in routes:
        routeLoad = np.sum([dataDict['Demand'][i] for i in eachRoute])
        penalty += max(0, routeLoad - dataDict['MaxLoad'])
    return penalty

def calcRouteServiceTime(route, dataDict = dataDict):
    '''辅助函数,根据给定路径,计算到达该路径上各顾客的时间'''
    # 初始化serviceTime数组,其长度应该比eachRoute小2
    serviceTime = [0] * (len(route) - 2)
    # 从仓库到第一个客户时不需要服务时间
    arrivalTime = calDist(dataDict['NodeCoor'][0], dataDict['NodeCoor'][route[1]])/dataDict['Velocity']
    arrivalTime = max(arrivalTime, dataDict['Timewindow'][route[1]][0])
    serviceTime[0] = arrivalTime
    arrivalTime += dataDict['ServiceTime'] # 在出发前往下个节点前完成服务
    for i in range(1, len(route)-2):
        # 计算从路径上当前节点[i]到下一个节点[i+1]的花费的时间
        arrivalTime += calDist(dataDict['NodeCoor'][route[i]], dataDict['NodeCoor'][route[i+1]])/dataDict['Velocity']
        arrivalTime = max(arrivalTime, dataDict['Timewindow'][route[i+1]][0])
        serviceTime[i] = arrivalTime
        arrivalTime += dataDict['ServiceTime'] # 在出发前往下个节点前完成服务
    return serviceTime

def timeTable(distributionPlan, dataDict = dataDict):
    '''辅助函数,依照给定配送计划,返回每个顾客受到服务的时间'''
    # 对于每辆车的配送路线,第i个客户受到服务的时间serviceTime[i]是min(TimeWindow[i][0], arrivalTime[i])
    # arrivalTime[i] = serviceTime[i-1] + 服务时间 + distance(i,j)/averageVelocity
    timeArrangement = [] #容器,用于存储每个顾客受到服务的时间
    for eachRoute in distributionPlan:
        serviceTime = calcRouteServiceTime(eachRoute)
        timeArrangement.append(serviceTime)
    # 将数组重新组织为与基因编码一致的排列方式
    realignedTimeArrangement = [0]
    for routeTime in timeArrangement:
        realignedTimeArrangement = realignedTimeArrangement + routeTime + [0]
    return realignedTimeArrangement

def timePenalty(ind, routes):
    '''辅助函数,对不能按服务时间到达顾客的情况进行惩罚'''
    timeArrangement = timeTable(routes) # 对给定路线,计算到达每个客户的时间
    # 索引给定的最迟到达时间
    desiredTime = [dataDict['Timewindow'][ind[i]][1] for i in range(len(ind))]
    # 如果最迟到达时间大于实际到达客户的时间,则延迟为0,否则延迟设为实际到达时间与最迟到达时间之差
    timeDelay = [max(timeArrangement[i]-desiredTime[i],0) for i in range(len(ind))]
    return np.sum(timeDelay)/len(timeDelay)

def calRouteLen(routes,dataDict=dataDict):
    '''辅助函数,返回给定路径的总长度'''
    totalDistance = 0 # 记录各条路线的总长度
    for eachRoute in routes:
        # 从每条路径中抽取相邻两个节点,计算节点距离并进行累加
        for i,j in zip(eachRoute[0::], eachRoute[1::]):
            totalDistance += calDist(dataDict['NodeCoor'][i], dataDict['NodeCoor'][j])    
    return totalDistance

def evaluate(ind, c1=10.0, c2=500.0):
    '''评价函数,返回解码后路径的总长度,c1, c2分别为车辆超载与不能服从给定时间窗口的惩罚系数'''
    routes = decodeInd(ind) # 将个体解码为路线
    totalDistance = calRouteLen(routes)
    return (totalDistance + c1*loadPenalty(routes) + c2*timePenalty(ind,routes)),
#-----------------------------------
## 交叉操作
def genChild(ind1, ind2, nTrail=5):
    '''参考《基于电动汽车的带时间窗的路径优化问题研究》中给出的交叉操作,生成一个子代'''
    # 在ind1中随机选择一段子路径subroute1,将其前置
    routes1 = decodeInd(ind1) # 将ind1解码成路径
    numSubroute1 = len(routes1) # 子路径数量
    subroute1 = routes1[np.random.randint(0, numSubroute1)]
    # 将subroute1中没有出现的顾客按照其在ind2中的顺序排列成一个序列
    unvisited = set(ind1) - set(subroute1) # 在subroute1中没有出现访问的顾客
    unvisitedPerm = [digit for digit in ind2 if digit in unvisited] # 按照在ind2中的顺序排列
    # 多次重复随机打断,选取适应度最好的个体
    bestRoute = None # 容器
    bestFit = np.inf
    for _ in range(nTrail):
        # 将该序列随机打断为numSubroute1-1条子路径
        breakPos = [0]+random.sample(range(1,len(unvisitedPerm)),numSubroute1-2) # 产生numSubroute1-2个断点
        breakPos.sort()
        breakSubroute = []
        for i,j in zip(breakPos[0::], breakPos[1::]):
            breakSubroute.append([0]+unvisitedPerm[i:j]+[0])
        breakSubroute.append([0]+unvisitedPerm[j:]+[0])
        # 更新适应度最佳的打断方式
        # 将先前取出的subroute1添加入打断结果,得到完整的配送方案
        breakSubroute.append(subroute1)
        # 评价生成的子路径
        routesFit = calRouteLen(breakSubroute) + loadPenalty(breakSubroute)
        if routesFit < bestFit:
            bestRoute = breakSubroute
            bestFit = routesFit
    # 将得到的适应度最佳路径bestRoute合并为一个染色体
    child = []
    for eachRoute in bestRoute:
        child += eachRoute[:-1]
    return child+[0]

def crossover(ind1, ind2):
    '''交叉操作'''
    ind1[:], ind2[:] = genChild(ind1, ind2), genChild(ind2, ind1)
    return ind1, ind2

#-----------------------------------
## 突变操作
def opt(route,dataDict=dataDict, k=2, c1=1.0, c2=500.0):
    # 用2-opt算法优化路径
    # 输入:
    # route -- sequence,记录路径
    # k -- k-opt,这里用2opt
    # c1, c2 -- 寻求最短路径长度和满足时间窗口的相对重要程度
    # 输出: 优化后的路径optimizedRoute及其路径长度
    nCities = len(route) # 城市数
    optimizedRoute = route # 最优路径
    desiredTime = [dataDict['Timewindow'][route[i]][1] for i in range(len(route))]
    serviceTime = calcRouteServiceTime(route)
    timewindowCost = [max(serviceTime[i]-desiredTime[1:-1][i],0) for i in range(len(serviceTime))]
    timewindowCost = np.sum(timewindowCost)/len(timewindowCost)
    minCost = c1*calRouteLen([route]) +  c2*timewindowCost # 最优路径代价
    for i in range(1,nCities-2):
        for j in range(i+k, nCities):
            if j-i == 1:
                continue
            reversedRoute = route[:i]+route[i:j][::-1]+route[j:]# 翻转后的路径
            # 代价函数中需要同时兼顾到达时间和路径长度
            desiredTime = [dataDict['Timewindow'][reversedRoute[i]][1] for i in range(len(reversedRoute))]
            serviceTime = calcRouteServiceTime(reversedRoute)
            timewindowCost = [max(serviceTime[i]-desiredTime[1:-1][i],0) for i in range(len(serviceTime))]
            timewindowCost = np.sum(timewindowCost)/len(timewindowCost)
            reversedRouteCost = c1*calRouteLen([reversedRoute]) + c2*timewindowCost
            # 如果翻转后路径更优,则更新最优解
            if  reversedRouteCost < minCost:
                minCost = reversedRouteCost
                optimizedRoute = reversedRoute
    return optimizedRoute

def mutate(ind):
    '''用2-opt算法对各条子路径进行局部优化'''
    routes = decodeInd(ind)
    optimizedAssembly = []
    for eachRoute in routes:
        optimizedRoute = opt(eachRoute)
        optimizedAssembly.append(optimizedRoute)
    # 将路径重新组装为染色体
    child = []
    for eachRoute in optimizedAssembly:
        child += eachRoute[:-1]
    ind[:] = child+[0]
    return ind,
#-----------------------------------
## 注册遗传算法操作
toolbox = base.Toolbox()
toolbox.register('individual', tools.initIterate, creator.Individual, genInd)
toolbox.register('population', tools.initRepeat, list, toolbox.individual)
toolbox.register('evaluate', evaluate)
toolbox.register('select', tools.selTournament, tournsize=2)
toolbox.register('mate', crossover)
toolbox.register('mutate', mutate)

## 生成初始族群
toolbox.popSize = 100
pop = toolbox.population(toolbox.popSize)

## 记录迭代数据
stats=tools.Statistics(key=lambda ind: ind.fitness.values)
stats.register('min', np.min)
stats.register('avg', np.mean)
stats.register('std', np.std)
hallOfFame = tools.HallOfFame(maxsize=1)

## 遗传算法参数
toolbox.ngen = 400
toolbox.cxpb = 0.8
toolbox.mutpb = 0.1

## 遗传算法主程序
pop,logbook=algorithms.eaMuPlusLambda(pop, toolbox, mu=toolbox.popSize, 
                                      lambda_=toolbox.popSize,cxpb=toolbox.cxpb, mutpb=toolbox.mutpb,
                   ngen=toolbox.ngen ,stats=stats, halloffame=hallOfFame, verbose=True)

输出结果:

from pprint import pprint

def calLoad(routes):
    loads = []
    for eachRoute in routes:
        routeLoad = np.sum([dataDict['Demand'][i] for i in eachRoute])
        loads.append(routeLoad)
    return loads

bestInd = hallOfFame.items[0]
distributionPlan = decodeInd(bestInd)
bestFit = bestInd.fitness.values
print('最佳运输计划为:')
pprint(distributionPlan)
print('总运输距离为:')
print(evaluate(bestInd,c1=0,c2=0))
print('各辆车上负载为:')
print(calLoad(distributionPlan))

timeArrangement = timeTable(distributionPlan) # 对给定路线,计算到达每个客户的时间
# 索引给定的最迟到达时间
desiredTime = [dataDict['Timewindow'][bestInd[i]][1] for i in range(len(bestInd))]
# 如果最迟到达时间大于实际到达客户的时间,则延迟为0,否则延迟设为实际到达时间与最迟到达时间之差
timeDelay = [max(timeArrangement[i]-desiredTime[i],0) for i in range(len(bestInd))]
print('到达各客户的延迟为:')
print(timeDelay)

# 画出迭代图
minFit = logbook.select('min')
avgFit = logbook.select('avg')
plt.plot(minFit, 'b-', label='Minimum Fitness')
plt.plot(avgFit, 'r-', label='Average Fitness')
plt.xlabel('# Gen')
plt.ylabel('Fitness')
plt.legend(loc='best')

## 计算结果如下:
##最佳运输计划为:
#[[0, 1, 2, 3, 4, 6, 0],
# [0, 9, 0],
# [0, 14, 29, 17, 30, 26, 18, 23, 21, 0],
# [0, 8, 25, 15, 16, 0],
# [0, 10, 11, 24, 12, 0],
# [0, 5, 7, 0],
# [0, 28, 27, 20, 19, 22, 13, 0]]
#总运输距离为:
#(278.62210617851554,)
#各辆车上负载为:
#[200, 30, 150, 131, 120, 110, 160]
#到达各客户的延迟为:
#[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

迭代过程可视化:

GA for VRPTW_iterations

配送路线可视化:

Derived routes
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容