最大流

目录

  • 1.流网络及最大流问题
    1.1 流网络
    1.2 最大流问题
    1.3 最大流问题的三种解法对比
  • 2.Ford-Fulkerson方法
    2.1 残存网络
    2.2 增广路径
    2.3 流网络的切割
    2.4 基本的Ford-Fulkerson算法
  • 3.最大二分匹配——归约到最大流问题
    3.1 最大二分匹配问题
    3.2 寻找最大二分匹配
  • 4.推送—重贴标签算法
    4.1 预流
    4.2 推送操作
    4.3 重贴标签操作
    4.4 通用算法——推送-重贴标签方法
    4.5 推送-重贴标签方法的正确性
  • 5.前置重贴标签算法
    5.1 前置重贴标签法与推送重贴标签法的区别
    5.2 许可边和网络
    5.3 邻接链表
    5.4 释放溢出结点
    5.5 前置重贴标签算法
    5.6 正确性证明和算法分析

1.流网络及最大流问题

1.1 流网络

  • 流网络)流网络G=(V, E)是一个有向图,图中每条边(u, v) ∈E有一个非负的容量值c(u, v) ≥ 0。
    并且如果边集合E包含一条边(u, v),则图中不存在反方向的边(v, u)。
    如果(u, v) ∉ E,定义c(u, v) = 0,且图中不允许自循环。
    两个特殊结点:源结点s和汇点t。每个结点都在从源结点到汇点的某条路径上。即对于每个结点v∈V,流网络包含一条路径s -> v -> t。
    因此,流网络是连通的,且由于源节点外每个结点都至少有一条进入的边,则|E| ≥ |V| -1。

  • )设G=(V, E)是一个流网络,其容量函数为c。设s为网络的源结点,t为汇点。G中的流是一个实值函数f: V x V -> R,满足下面两条性质:
    1)容量限制:对于所有的结点u, v ∈ V,要求 0 ≤ f(u, v) ≤ c(u, v)。
    f(u, v)为从结点u到结点v的流。
    2)流量守恒:对于所有的结点u ∈ V-{s, t},要求


    当(u, v) ∉ E时,从结点u到结点v之间没有流,因此f(u, v) = 0.

  • 流的值|f|)一个流f的值|f|定义如下:


    流f的值是从源结点流程的总流量减去流入源结点的总流量。通常,一个流网络不会有任何进入源结点的边。也即后面的项为0.
    这里将其包含进来时为了讨论后面的残存网络。

1.2 最大流问题

  • 最大流问题)在最大流问题中,给定一个流网络G、一个源结点s、一个汇点t,希望找到值最大的一个流。

  • 将带有反平行边的网络转换为不带反平行边的网络)对于反平行边(v1, v2),通过加入一个新结点v'来将其分为两段,并以边(v1, v')和(v', v2)来替换边(v1, v2)。同时将两条新设立的边的内容设置为与原来的边的容量相同。


  • 将具有多个源结点和多个汇点的网络转换为单源点和单汇点网络)转换方法如下:
    1)新增一个超级源结点s,加入有向边(s, si),和容量c(s, si) = ∞。
    2)新增一个超级汇点t,加入边(ti, t),其容量c(ti, t) = ∞。


1.3 最大流问题的四种解法对比

  • Ford-Fulkerson方法——O(E|f*|)


  • Edmonds-Karp算法(Ford-Fulkerson方法的一种实现)——O(VE²)
    这里主要是约定了查找增广路径的方法:寻找s到t的最短路径。
    使用广度优先搜索来改善。
    在残存网络中选择的增广路径是一条从源结点s到汇点t的最短路径,其中每条边的权重为单位距离。

  • 推送—重贴标签算法—— O(V²E)





  • 前置重贴标签算法(推送—重贴标签算法的一种实现)—— O(V³)





2.Ford-Fulkerson方法

  • Ford-Fulkerson方法包含几种运行时间各不相同的具体实现。
  • Ford-Fulkerson方法的主要思想:
    1)循环增加流的值。
    在开始的时候,对于所有的结点u, v ∈V,f(u, v) = 0,给出的初始流值为0。
    在每一次迭代中,将图G的流值进行增加,方法是在一个关联的“残存网络”Gf中寻找一条“增广路径”。
    2)重复对流进行这一过程,知道残存网络中不再存在增广路径为止。
    3)最大流最小切割定理将说明在算法终结时,该算法将获得一个最大流。

2.1 残存网络

  • 残存容量)假定有一个流网络G=(V, E),其源结点为s,汇点为t。设f为图G的一个流,考虑结点对u, v ∈ V,定义残存容量cf(u, v)如下:


    边(u, v) ∈ E意味着(v, u) ∉ E。
    一个示例:c(u, v) = 16,并且f(u, v) = 11
    1)则对f(u, v)可以增加的量最大为cf(u, v) = 5
    2)运行算法从结点v向结点u最多返回11个单位的流量,cf(v, u) = 11

  • 残存网络)给定一个流网络G=(V, E)和一个流f,则由f所诱导的图G的残存网络为Gf = (V, Ef),其中:


    即残存网络的每条边或残存边,必须运行大于0的流量通过。
    Ef中的边要么是E中原有的边,要么是其反向边,因此有:

    b是a的残存网络,d是c的残存网络

  • 残存网络的流和递增
    1)在残存网络中定义一个流,满足流的定义,但是针对的是残存网络Gf中的容量cf
    2)残存网络中的一个流指出的是一条路线图:如何在原来的流网络中增加流。
    如果f是G的一个流,f'是对应的残存网络Gf中的一个流,定义f↑f'为流f'对流f的递增(augmentation),它是一个从V x V到R的函数,定义如下:


2.2 增广路径

  • 增广路径)给定流网络G = (V, E)和流f,增广路径p是残存网络Gf中一条从源结点s到汇点t的简单路径。
    对于一条增广路径上的边(u, v),可以增加其流量的幅度最大为cf(u, v),而不会违反原始流网络G中对边(u, v)或(v, u)的容量限制。
  • 残存容量)在一条增广路径p上能够为每条边增加的流量的最大值为路径p的残存容量。
  • 引理26.2的证明很显然:
    1)满足容量限制,若(u, v)不在p上,显然满足。若(u, v)在p上cf(p)是路径上最小边的容量,因此肯定满足所有边的容量限制;
    2)流量守恒:对于不在p上的点,流入流出均为0,满足;对于在p上的点,流入流出都为cf(p),所以满足。

2.3 流网络的切割

  • 一个流的最大流当且仅当其残存网络不包含任何增广路径。

  • 切割)流网络G=(V, E)中的一个切割(S, T)将结点集合V划分为S和T = V-S,使得s∈S,t ∈ T。若f是一个流,则定义横跨切割(S, T)的净流量f(S, T)如下:


    切割(S, T)的容量是:

    一个网络的最小切割是整个网络中容量最小的切割。
    一个示例:如下一个切割为({s, v1, v2},{v3, v4, t})

    1)横跨切割的净流量是f(v1, v3)+f(v2, v4)-f(v3, v2) = 12 + 11 - 4 = 19
    2)该切割的容量是c(v1, v3) + c(v2, v4) = 12 + 14 = 26

  • 精髓:最大流最小切割定理



  • 定理26.6最大流最小切割定理是整章的精华,凝结了最大流、残存网络、增广路径和切割之间的关系。

2.4 基本的Ford-Fulkerson算法

2.4.1 Ford-Fulkerson算法


2.4.2 算法分析

  • 算法的运行时间取决于如何寻找增广路径。
    如果选择不好,算法可能不会终止(当边的容量为无理数时);
    如果使用广度优先搜索来寻找增广路径,算法的运行时间将是多项式数量级。


2.4.2.1 一个简单上界——O(E|f*|)

  • while循环次数)一个简单的上界分析:假设所有的容量都是整数(有理数可以通过乘以系数转换成整数)。假设f*是一个最大流,则while循环最多执行|f*|次。因为流量值每次迭代中最少增加一个单位。
    每次循环找到一条增光路径的时间)给定网络G的一个流f,残存网络Gf中的边由网络G'中所有满足条件cf(u, v) > 0的边(u, v)所构成。因此使用深度优先搜索或者广度优先搜索,在一个残存网络中找到一条路径的时间为O(V+E') = O(E)。
    因此整个FORD-FULKERSON算法运行的时间为O(E|f*|)

2.4.2.2 Edmonds-Karp算法——O(VE²)

  • 使用广度优先搜索来改善。
    在残存网络中选择的增广路径是一条从源结点s到汇点t的最短路径,其中每条边的权重为单位距离。

  • δf(u, v)表示残存网络Gf中从结点u到v的最短距离,其中每条边的权重为单位距离。


  • 引理26.7的核心在于:
    在s->t的最短路径上,该路径上的点v也是最短路径,也即s->v也是最短的。(最优子结构)
    当沿该条最短路径(增广路径)增流后,该条最短路径上的关键边就从残存网络中消失了。增广路径上关键边前面的端点具体保持不变,关键边后面的端点递增(至少保持不变)。


3.最大二分匹配——归约到最大流问题

3.1 最大二分匹配问题

  • 匹配)给定一个无向图G=(V, E),一个匹配是边的一个子集M ⊆ E,使得对于所有结点v ∈V,子集M中最多有一条边与结点v相连。
    如果子集M的某条边与结点v相连,则称结点v由M所匹配;否则,结点v就是没有匹配的。
  • 最大匹配)最大匹配是最大基数的匹配,也即,对于任意匹配M',有|M| ≥ |M'|的匹配M。
  • 二分图)在一个二分图中,结点集合可以划分为V = L ∪ R,其中L和R是不相交的,并且边集合E中所有的边都横跨L和R。进一步假定结点集合V中的每个结点至少有一条边。
  • 最大二分匹配的应用)把一个机器集合L和要同时执行的任务集合R相匹配。E中的边(u, v)就说明一台特定的机器u ∈ L能够完成一项特定的任务v ∈ R。最大匹配能够让尽可能多的机器运行起来。

3.2 寻找最大二分匹配

  • 解决这一问题的关键技巧是构建一个流网络,其中的流对应于匹配。
    将二分图G所对应的流网络G' = (V', E')定义如下:设源结点s和汇点t为不属于结点集V的新结点。
    V' = V ∪ {s, t}
    E' = {(s, u): u ∈ L} ∪ {(u, v):(u, v) ∈E} ∪{(v, t): v∈R }
    给E'中每条边赋予单位容量。
    由于V中的每个结点至少有一条相连的边,|E| ≥ |V|/2,因此|E| ≤ |E'| = |E| + |V| ≤ 3|E|,所以|E'| = Θ(E)。



  • 运行时间O(VE)的分析来自于“2.4.2.1 一个简单上界——O(E|f*|)”

4.推送—重贴标签算法

推送——重贴标签算法不是对整个残存网络进行检查,然后选择一条增广路径,而是一个结点一个结点地进行查看,每一步只检查当前结点的邻接点。

4.1 预流

  • 预流)预流是一个V x V -> R的函数f,该函数满足容量限制和下面弱化了的流量守恒性质:对于所有的结点u ∈ V-{s},

    即进入一个结点的流可以超过流出该结点的流。称下面的量

    为进入结点u的超额流。如果对于结点u ∈ V - {s, t},有e(u) > 0,则称结点u溢出。

4.2 推送操作

  • 高度函数)设G=(S, V)是一个源结点s汇点t的流网络,设f为G的一个预流。如果函数h:V->N满足h(s) = |V|,h(t) = 0,并且对于所有的边(u, v) ∈Ef,有h(u) ≤ h(v) + 1,则h是一个高度函数。

问题:高度函数为什么要如此定义?

  • 推送操作)如果结点u是一个溢出结点,cf(u, v) > 0,并且h(u) = h(v) + 1,则PUSH(u, v)适用于结点u和v。
    只将超额流向高度差为1的下层结点推送。
    u.e——保存结点u的超额流
    u.h——保存结点u的高度
    f(u, v) ——存放可以从结点u推送到结点v的流

    如果推送操作后,残存网络中的边(u, v)达到饱和状态(即操作之后由cf(u, v) = 0),则称该推送操作为饱和推送;否则,成为非饱和推送。

4.3 重贴标签操作

  • 如果结点u溢出,并且对于所有的边(u, v) ∈Ef,有u.h ≤ v.h,则基本操作RELABEL(u)适用于结点u。
    也即,对于每个结点v如果存在从结点u到结点v的残存容量,却因为结点v不在结点u的下方,而不能将流从u推送到v,我们就可以对溢出结点u进行重贴标签操作。(s,t没有资格重贴标签)


    由于所有的流都是非负的,因此必然有一个结点v,使得(v, u).f > 0。因此,当u被重贴标签时,Ef至少有一条从结点u出发的边。因为cf(u ,v) > 0,所以(u, v) ∈Ef。因此,操作RELABEL(u)给结点u所赋予的高度是高度函数所能运行的最大高度。(如果u.h > v.h + 1,则(u, v)不是残存网络中的一条边)

4.4 通用算法——推送-重贴标签方法

1)创建初始预流
将从源结点s发出的所有边都充满流。



因为满足条件u.h > v.h + 1的边(u, v)全都是那些满足条件u = s的边,并且这些边都已经达到饱和状态,这就意味着这些边不在残存网络中。

2)通用的算法
按非特定次序执行一个序列的推送和重贴标签操作,就能得出GENERIC-PUSH-RELABEL算法。

4.5 推送-重贴标签方法的正确性

step1.证明如果该算法终止,预流f就是一个最大流


  • 定理26.6(最大流最小切割定理)三个条件是等价的:
    1)f是G的一个最大流
    2)残存网络Gf不包括任何增广路径
    3)|f| = c(S, T),其中(S, T)是流网络G的某个切割

step2.证明该算法必将终止
分别对如下三种操作求界:重贴标签操作、饱和推送操作和非饱和推送操作。


  • 初始的预流INITIALIZE-PREFLOW(G, s)中,将源结点s的预流初始化为其他与预流之和的相反数,只有s.e为负数,其他节点的预流都是非负数。

  • 这里的h定义跟Edmonds-Karp算法(总是选择残存网络中一条从源结点s到汇点t的最短路径)s到结点u的最短路径δf(s, v)有异曲同工之妙。
    边(u, v)成为关键边到下一次再次成为关键边,从源结点s到结点u的距离至少增加2个单位。
  • 这里给出了高度函数定义的缘由:饱和推送操作和重贴标签操作储存的势能是为了给非饱和操作减少的势做准备。
  • 练习26.4-2 说明如何实现通用-重贴标签算法,使得每个重贴标签的操作成本为O(V),每个推送操作的成本为O(1),并且可以在O(1)时间内选择一个合适的操作,从而使得整个算法的运行时间为O(V²E)。

  • 解答
    1)推送操作PUSH(u, v)
    u是一个溢出结点,cf(u, v) > 0,并且u.h = v.h +1
    饱和推送操作次数的上界:O(VE)
    非饱和推送操作次数的上界:O(V³+V²E) = O(V²E)
    每个推送操作的成本为O(1)


    2)重贴标签操作RELABEL(u)
    u是一个溢出结点,并且对于所有的的边(u, v) ∈Ef,有u.h ≤ v.h
    重贴标签操作次数的上界:O(V²)
    每个重贴标签操作成本为O(V)

    根据26.14,可以对溢出结点执行推送操作或重贴标签操作。

  • 一个思路:将溢出结点放在队列中,然后,将队首的h加1,然后,将该结点的超额流向所有满足条件的边推送,然后删除该结点;并将推送操作所产生的新的超额流结点加入到队列。
    根据如下示例,这个思路有个错误,尤其是到了最后的t结点时,v3又加入队列,此时v3的高度不能加1。

    一个示例

  • 一个合适的思路:队列的首元素必然满足重贴标签操作,并且重贴标签之后对min进行推送操作。
    队列里面按照h高度按照由低到高排列,而且队列里面都是超额流,如果一个超额流结点两个操作都不满足,则从队列中删除。
    注意点:
    1)如下的图画的可能有点错误,比如v1,因为最后其邻接点是s,可以直接将其h提高到|V|+1,将其多余的超额流回流到s,后面就不用出现v1
    2)s和t不加入到队列中
    3)具体操作为,对队列首元素执行重贴标签操作,找到min,并将超额流推送到min。并将min加入到队首,然后将队首元素放到队尾。




5.前置重贴标签算法

5.1 前置重贴标签法与推送重贴标签法的区别

  • 推送重贴标签方法允许以任意次序执行基本操作。
  • 前置重贴标签算法在执行过程中维持一个网络中的结点链表。算法从头到尾对链表进行扫描,每次选择一个溢出结点u,然后对所选结点进行“释放”,即对所选结点执行推送操作和重贴标签操作,直到该结点不再拥有正值的超额流量为止。
    每次在算法对一个结点进行重贴标签操作时,都将该结点移动到链表的最前面(“前置重贴标签算法”的由来),而算法则开始一次新的扫描。

5.2 许可边和网络

  • 许可边)图G=(V, E)是一个源结点为s汇点为t的流网络,f是G的一个预流,h是一个高度函数。对于边(u, v),如果cf(u, v) > 0且h(u) = h(v) + 1,则边(u, v)是一条许可边。否则,边(u, v)是非许可边。
  • 许可网络)指的是图Gf,h = (V, Ef,h),其中Ef,h是许可边的集合。

5.3 邻接链表

  • 给定流网络G=(V, E),对于结点u ∈ V,其邻接链表u.N是结点u在图G中的邻接结点所构成的一个单链表。
    如果边(u, v) ∈E或则(v, u) ∈ E,则结点v将出现在链表u.N中。
    邻接链表u.N包含的结点恰好是那些可能存在残存边(u, v)的结点v。
  • 前置重贴标签算法以任意次序遍历每个邻接链表,该次序在算法的整个执行过程中维持不变。

5.4 释放溢出结点

  • 释放)对于溢出结点,将其所有多余的流通过许可边推送到相邻的结点上,则称该结点得到释放。
    在释放过程中,需要对结点u进行重贴标签操作,这使得从结点u发出的边称为许可边。
  • 算法终止的唯一条件是当u.e为0,即推送操作是非饱和推送(一般情况下,除非正好u.e等于cf(u, v));
    推送操作不创建许可边;
    u.current向前推进的唯一前提就是,(u, v)是非许可边;
    并且DISCHARGE(v)不会创建进入v的许可边(因此即使DISCHARGE(u)中间终端,调用DISCHARGE(v),然后再回来继续调用DISCHARGE(u),这个过程中DISCHARGE(v)不会创建许可边(u, v))。

  • 一个示例:对接点y进行释放操作


5.5 前置重贴标签算法

  • 维持一个链表L,该表由V-{s, t}中的所有结点构成。
    关键性质是,链表L中的结点均按照许可网络里面的拓扑排序次序存放。(许可网络是一个有向无环图)



  • 如果u在DISCHARGE过程中执行了重贴标签操作,则将其移到到链表L的最前面。

  • 一个示例


5.6 正确性证明和算法分析

5.6.1 算法的正确性证明

  • 为了证明前置重贴标签算法确实计算出了一个最大流,只要证明它是推送——重贴标签算法的一种实现。
    1)该算法仅在推送操作和重贴标签适用的时候才执行这些操作
    2)算法终止时,没有任何基本的操作的操作可以适用。

  • 循环不变式:在前置重贴标签算法第6行的测试中,链表L是许可网络Gf,h = (V, Ef,h)中结点的一个拓扑排序,并且在链表L中位于结点u前面的结点没有任何超额流量。

  • 这个算法的核心在于推送操作不产生许可边;重贴标签操作可能产生许可边,因此算法将其移动到链表头。
    为什么移动到链表头是正确的?因为根据26.28可知,重贴标签操作不会存在进入u的许可边,因此移动到链表头可以保证从u发出的许可边都满足拓扑排序性质。这里唯一改变的只有u,如果u满足了(进入的没有、发出的满足),则整个都满足。

5.6.2 前置重贴标签算法运行时间分析


  • 这里着重对DISCHARGE操作中的u.current指针更新操作进行说明
    1)对一个结点u进行重贴标签操作时,更新操作发生了O(degree(u))次,对于所有的结点而言,总共发生的次数,可以参考握手引理:



    2)对每个结点最多可进行O(V)次重贴标签,也即对1)乘以O(V)即可,因为1)表示的所有结点进行一次重贴标签操作,更新操作发生的总次数。因此为O(VE)。



  • 相关结论1.重贴标签的次数为O(V²)


  • 相关结论2. (练习26.4-3)通用推送-重贴标签算法只用了总共O(VE)的时间来执行所有O(V²)个重贴标签操作。
    1)当每次调用RELABEL(u)时,需要检查所有的边(u, v) ∈ Ef
    2)因为每个结点的重贴标签操作次数不超过2|V| - 1,因此边(u, v)最多会被检查4|V| - 2 = O(V)次(RELABEL(u)的2|V| - 1次,和RELABEL(v)的2|V| - 1次)。
    3)对所有可能的残存边累加求和,所有可能的残存边的总数是2|E| = O(E)。
    因此总共为O(V) * O(E) = O(VE)。

  • 相关结论3.饱和推送的总次数为O(VE)。



  • 相关结论4.(练习B.4-1)在一个教职工聚会中,与会者互相握手问候彼此,每位教授会记住他/她握手的次数。在聚会的最后,系主任将所有教授的握手次数相加。通过证明下面的握手定理来说明系主任得到的结果是偶数:如果G=(V, E)是无向图,则有


    度degree)无向图中顶点的度是指关联于该顶点的边的数目。

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

推荐阅读更多精彩内容