深度优先搜索:暴力出奇迹

阅读之前

请确认您能够理解函数及其递归调用。

快速入门

深度优先搜索(DFS,即Depth First Search)属于图算法的一种,也被氢切的称为回溯算法,本质还是打暴力。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。在非图论的应用中,DFS常用于设计回溯递归算法,本次我们主要围绕DFS的基本应用即回溯搜索为中心展开讲解。

考虑全排列问题,即按照字典序输出自然数 1n 所有不重复的排列的个数与排列的方式,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。要求编写程序遍历所有可能的情况并输出。我们先来看一个简单的情况n=3,此时用脑子很容易想到一共有6种排列,分别是123、132、213、231、312、321

我们是如何想到这6个数字的呢?首先,我们以1作为第1位,之后选择2作为第2位,剩下一个3作为第3位,得到123;之后我们回到第一位为1的情况,再选择刚才没选的3作为第2位,最后选择剩下的2作为第3位,得到132。这样1作为第1位的情况就全部遍历完成,我们回到最开始,再选择2作为第1位,之后选择1作为第2位,以此往复,直到把6种情况全部遍历完毕。

我们以此为以据设计算法,首先考虑基本的连续选择过程,由于对于全排列而言,每个数字只能使用一次,我们使用一个数组vis来存储每一个数组是否已被使用,当选择下一个数字时,我们检查这个数字是否已经被使用,如果没被使用,则选择这个数字并更新状态,如过已经被使用,那么尝试选择下一个数字。

最重要的在于“回溯”过程,我们在上面提到过,当第一次选择完成,选择完成一次回溯,重新跳回到第2位的选择。由于第3位选择的3已经被释放,我们应该将vis数组中的3位置修改为false。由于2已经被选择,接着继续尝试向下选择3,此时3未被访问过,接下来将3作为第2为,生成132。

第一次回溯

从上面的分析我们得出,每次最终回溯前我们的三位数字都完成了取值,可以被记为一个答案,我们可以在调用时增加一个参数layer,表示目前搜索的层数,当层数大于3时,代表3位数字均已搜索完成,此时我们就得到一个新的答案。用一个全局参数ans记录:

#include <iostream>

using namespace std;
void dfs(int layer);

int n,ans;
int vis[14] = {};

int main()
{
    cin >> n;
    dfs(1);
    cout << ans << endl;

    return 0;
}

void dfs(int layer)
{
    if (layer > n)
        ans++;

    for (int i = 1; i <= n; ++i)
    {
        if (!vis[i])
        {
            vis[i] = true;
            dfs(layer + 1);
            vis[i] = false;
        }
    }
}

我们来简单的解读一下这份递归代码中的核心部分——dfs函数。

  1. 生成答案。layer > n时,代表产生了一种新答案,此时累加答案。
if (layer > n)
    ans++;
  1. 遍历搜索树的一层。对于每深入的一位,我们都从起点开始遍历,覆盖每一种情况。
for (int i = 1; i <= n; ++i)
  1. 产生情况。对每次要取的数字,判断它是否被访问过。如果是,则不进入内部的处理,循环继续寻找下一位。如果它没有被访问过,进入更深的一层递归,同时将该数设为已访问。
if (!vis[i])
{
    vis[i] = true;
    dfs(layer + 1);
}
  1. 回溯。当遍历至终点,更深层的dfs函数退出。需要注意的是,相对于上一层dfs函数而言,更深层的dfs函数占用了vis[i],此时要从浅层的dfs,通过下一个数字来访问深层的dfs,也就是回溯过程,将已经被占用的数字重新置否,将被占用的vis[i]设为未占用。
vis[i] = false;

至此完成整个深度优先搜索过程。

接下来我们考虑如何输出答案的排列。我们可以使用一个新数组arrange来记录该数字每一位的情况,在每一次成功占用数字时,我们将arrange[layer](也就是第layer层占用的数字)设置为i,表示第layer层使用了数字i。当每次完成递归也就是layer > n时,我们输出这个数组的全部元素即可,为了美观我们位每个元素保留5个场宽。

#include <iostream>
#include <iomanip>

using namespace std;
void dfs(int layer);

int n;
int arrange[14] = {}, vis[14] = {};

int main()
{
    cin >> n;
    dfs(1);
    return 0;
}

void dfs(int layer)
{
    if (layer > n)
    {
        for (int i = 1; i <= n; ++i)
            cout << setw(5) << arrange[i];
        cout << endl;
    }

    for (int i = 1; i <= n; ++i)
    {
        if (!vis[i])
        {
            vis[i] = true;
            arrange[layer] = i;
            dfs(layer + 1);
            vis[i] = false;
        }
    }
}

再次梳理执行思路如下:

  • 对于每一层layer
    • 如果layer > n,打印当前排列arrange[1...n]
    • 否则,对每个数字i(1到n):
      • 如果vis[i]为假:
        • 设置vis[i]为真
        • i放入arrange[layer]
        • 递归调用dfs(layer + 1)
        • 递归返回后,设置vis[i]回假

这里无需重置arrange[layer],结论是显然的,这个数组中的元素会在每次数字占用时被覆盖。

排列问题

深度优先搜索在排列问题中非常常见。上文我们正是通过一个基本的全排列问题引入了DFS。下面我们考虑“三连击”问题:将 1, 2,\ldots, 99 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是 A:B:C,试求出所有满足条件的三个三位数,若无解,输出 No!!!。输入,A,B,C;输出若干行,每行 3 个数字。按照每行第一个数字升序排列。

初看这题可能毫无头绪,上文提到过,DFS本质就是打暴力,想想这个题能不能暴力求解?这个题的暴力思路是明显的,我们枚举1-9这9个数字的全排列,然后把它们分成三组,那么这个题就可以被转换为全排列问题。事实上,我们可以把每次递归,都可以不加限制的对搜索树的一层进行枚举的情况都称作全排列。

现在转回到这个问题上来,很容易构建它的搜索树:


搜索树

按照模板构建深度优先搜索,

首先,准备数组vis记录所有访问过的元素,数组arrange记录每一位的情况。

  1. 生成答案。layer > 9时,代表产生了一种新答案,此时我们按照arrange的每三位进行分割,形成三个数字。之后,由于题目要求这三个三位数的比例是 A:B:C,我们通过一个语句num1 * C == num3 * A && num1 * B == num2 * A来验证,如果符合要求,则输出。
if (layer > 9)
{
    auto num1 = arrange[1] * 100 + arrange[2] * 10 + arrange[3];
    auto num2 = arrange[4] * 100 + arrange[5] * 10 + arrange[6];
    auto num3 = arrange[7] * 100 + arrange[8] * 10 + arrange[9];

    if (num1 * C == num3 * A && num1 * B == num2 * A)
    {
        flag = true;
        cout << num1 << " " << num2 << " " << num3 << endl;
    }
}
  1. 遍历搜索树的一层。该树任意一层有9种情况,代表数字1...9
for (int i = 1; i < 10; ++i)
  1. 产生情况。我们遍历每一个数字,如该数字未被使用,我们记录该数字并更新状态为已使用,并加以此状态进入下一层递归。
if (!vis[i])
{
    vis[i] = true;
    arrange[layer] = i;
    dfs(layer + 1);
}
  1. 回溯。将已经被占用的数字重新置否,将被占用的vis[i]设为未占用。
vis[i] = false;
  1. 特判。此题需要注意的是,若无解,输出 No!!!。我们定义一个标志flag。在第1步中,一旦产生合法的答案,flag就会被更新为true。到最后时,我们看一下flag的状态,如果仍为false,说明没有任何解,需要输出 No!!!
if (!flag)
    cout << "No!!!" << endl;
return 0;

本题完整代码如下:

#include <iostream>
using namespace std;

int A, B, C;
int vis[10] = {0},arrange[10] = {-1};
bool flag = false;
void dfs(int layer);

int main()
{
    cin >> A >> B >> C;
    dfs(1);

    if (!flag)
        cout << "No!!!" << endl;
    return 0;
}

void dfs(int layer)
{
    if (layer > 9)
    {
        auto num1 = arrange[1] * 100 + arrange[2] * 10 + arrange[3];
        auto num2 = arrange[4] * 100 + arrange[5] * 10 + arrange[6];
        auto num3 = arrange[7] * 100 + arrange[8] * 10 + arrange[9];

        if (num1 * C == num3 * A && num1 * B == num2 * A)
        {
            flag = true;
            cout << num1 << " " << num2 << " " << num3 << endl;
        }
    }

    for (int i = 1; i < 10; ++i)
    {
        if (!vis[i])
        {
            vis[i] = true;
            arrange[layer] = i;
            dfs(layer + 1);
            vis[i] = false;
        }
    }
}

子集问题

我们来看一道有趣的配料问题:Perket 是一种流行的美食。为了做好 Perket,厨师必须谨慎选择食材,以在保持传统风味的同时尽可能获得最全面的味道。你有 n 种可支配的配料。对于每一种配料,我们知道它们各自的酸度 s 和苦度 b。当我们添加配料时,总的酸度为每一种配料的酸度总乘积;总的苦度为每一种配料的苦度的总和。众所周知,美食应该做到口感适中,所以我们希望选取配料,以使得酸度和苦度的绝对差最小。另外,我们必须添加至少一种配料,因为没有任何食物以水为配料的。输入第一行一个整数 n,表示可供选用的食材种类数。接下来 n 行,每行 2 个整数 s_ib_i,表示第 i 种食材的酸度和苦度。输出一行一个整数,表示可能的总酸度和总苦度的最小绝对差。

这个问题说到底还是这些酸度和苦度的全排列。但这问题相比起上一个有两个明显的不同点,首先,酸度和苦度并不能任意排,我们可以使用一个结构体配对存储,然后对其全排:

struct ingredient
{
    int s;
    int b;
}arr[11];

另一个最大的不同就是这题的“全排列”不是说一定要到最后一层的所有情况,也就是说,给了我们n种材料,我们不是一定要加n种,而是可以加1种,可以加2种...可以加n-1种,当然也可以加n种,这种情况相当于求了集合A = \{x | arr_0 \leq x \leq arr_{n-1}\}的所有子集。我们要找出最优解,可以画图看一下。

搜索树

这时我们就需要对深搜板子做一点小小的改动,本来时直到layer > n时才进行答案的处理,在这里我们进入每一次dfs函数都进行答案处理即可,下面构建深搜。

  1. 生成答案。每次进入深搜时,我们都要尝试此时的答案,即可能的总酸度和总苦度的最小绝对差D = \prod\limits_{i=0}^{n-1}a_i - \sum\limits_{i=0}^{n-1}b_i。并且比较D与目前最小答案ans的大小,如果D < ans,那么更新ansD需要特别注意的是,刚进入时,vis数组全为false,此时未进入求值,而我们给答案赋值时,酸度的初值为1,苦度的初值为0。\prod\limits_{i=0}^{n-1}a_i - \sum\limits_{i=0}^{n-1}b_i \equiv 1-0 =1 ,也就是说求出来的这个最小差为1,我们需要记录一下是否进入了计算D的过程,如果没有进入,那么就不进行赋值。否则进行正常赋值。
if (layer > n) return;

int sour = 1, bitter = 0;
bool changed = false;
for (int i = 0; i < n; ++i)
{
    if (vis[i])
    {
        sour *= arr[i].s;
        bitter += arr[i].b;
        changed = true;
    }
}

if (changed)
    ans = min(ans, abs(sour - bitter));
  1. 遍历搜索树的一层。任意一层有n种情况。代表选择arr_0...arr_n
for (int i = 0; i < n; ++i)
  1. 产生情况。我们遍历arr中的每一种配料,并使用vis进行记录映射,如该调料未被使用,记录该数字并更新状态为已使用并加以此状态进入下一层递归。
if (!vis[i])
{
    vis[i] = true;
    dfs(layer + 1);
}
  1. 回溯。将已经被占用的数字重新置否,将被占用的vis[i]设为未占用。
vis[i] = false;

合并以上思路,便可以使用以下简单的代码解决该问题:

#include<iostream>

using namespace std;

struct ingredient
{
    int s;
    int b;
}arr[11];
bool vis[11];
int n,ans = 2147483647;

void dfs(int layer);

int main()
{
    cin >> n;

    for (int i = 0; i < n; ++i)
        cin >> arr[i].s >> arr[i].b;

    dfs(0);
    cout << ans << endl;
    return 0;
}

void dfs(int layer)
{
    if (layer > n) return;

    int sour = 1, bitter = 0;
    bool changed = false;
    for (int i = 0; i < n; ++i)
    {
        if (vis[i])
        {
            sour *= arr[i].s;
            bitter += arr[i].b;
            changed = true;
        }
    }

    if (changed)
        ans = min(ans, abs(sour - bitter));

    for (int i = 0; i < n; ++i)
    {
        if (!vis[i]) 
        {
            vis[i] = true;
            dfs(layer + 1);
            vis[i] = false;
        }
    }
}

组合问题

组合与排列最大的区别就是组合不包含重复的排列。举个例子,对于数字1—5中抽取三个数字,而言,全排列可以包含123、231、321等等,对于组合而言,这三种排列都是数字1、2、3的组合,算作同一种组合。那么如何寻找组合呢?事实上,组合和排列的区别在于,组合需要对重复的排列去重。

首先我们观察一个简单的情况:不加详细考虑的先认为以1为首位的所有组合已经枚举完成,下面以2作为首位枚举,显然213是一个重复的组合(123已被枚举)那么移动第2位的开始位置至第2个元素2,由于元素2已经在首位被使用,这种情况被忽略,接下来第2位移动到3,如果第3位从第1个元素开始,会得到231,与123重复,那么移动第3位的开始位置至第2个元素2,由于2已经被使用,接下来第2位移动到3,又由于3已经被使用,接下来第2位移动到4,产生新的组合234,之后继续以上过程得到新的组合。

回溯过程(应该说是两轮回溯而不是两次)

从上面的分析中,我们得出一个求组合的通用思路,就是在每一层的遍历中,不在从1开始枚举以产生重复的组合(即排列),由于组合里每一个数都要大于前一个数,就从比当前数字大的地方i+1开始遍历到最大值。

解决组合问题:排列与组合是常用的数学方法,其中组合就是从 n 个元素中抽出 r 个元素(不分顺序且 r \le n),我们可以简单地将 n 个元素理解为自然数 1,2,\dots,n,从中任取 r 个数。现要求你输出所有组合。例如 n=5,r=3,所有组合为:123,124,125,134,135,145,234,235,245,345输入一行两个自然数 n,r(1<n<21,0 \le r \le n)输出所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。

我们尽可能地通过修改板子地方式实现,我们修改第2步,遍历搜索树的一层。对于每深入的一位,我们都从比上一位大的地方开始遍历,覆盖每一种情况。

for (int i = start; i <= n; ++i)

之后修改第3步,产生情况。对每次要取的数字,判断它是否被访问过。如果是,则不进入内部的处理,循环继续寻找下一位。如果它没有被访问过,进入更深的一层递归,同时将该数设为已访问。同时向dfs函数中传入当前遍历到的位置,并+1作为下一层的起始值

if (!vis[i])
{
    vis[i] = true;
    dfs(layer + 1,i + 1);
}

最后修改函数签名,增加一个参数start接收这个位置即可。

void dfs(int layer,int start);

完整代码如下所示:

#include<iostream>
#include <iomanip>

using namespace std;

int n, r;
int vis[22], combine[22];
void dfs(int layer, int start);

int main()
{
    cin >> n >> r;
    dfs(1,1);

    return 0;
}

void dfs(int layer,int start)
{
    if (layer > r)
    {
        for (int i = 1; i < r + 1; ++i)
            cout << setw(3) << combine[i];
        cout << endl;
        return;
    }

    for (int i = start; i < n + 1; ++i)
    {
        if (!vis[i])
        {
            vis[i] = true;
            combine[layer] = i;
            dfs(layer + 1, i + 1);
            vis[i] = false;
        }
    }
}

坐标问题

深度优先搜索在二维数组(坐标系)中应用广泛。一方面,可以解决回溯搜索问题;另一方面,衍生出的Flood Filling一类算法可以解决连通性问题。

“过河卒”是一个经典的算法问题:棋盘上 A 点有一个过河卒,需要走到目标 B 点。卒行走的规则:可以向下、或者向右。同时在棋盘上 C 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。棋盘用坐标表示,A(0, 0)B(n, m),同样马的位置坐标是需要给出的。

题目示例

现在要求你计算出卒从 A 点能够到达 B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。输入一行四个正整数,分别表示 B 点坐标和马的坐标。输出一个整数,表示所有的路径条数。

现在问题从一维上升到了二维,我们依然还是先构建搜索树,搜索树有了,思路也就有了。但这时我们构建搜索树时可能遇到难以逾越的天堑:我们无从获知每一次选择的情况是什么。这时候我们可以重新思考搜索树的实质。每一层的选择实质上是相对于上一层,下一层可以如何选择。在本题中,对于上一层的坐标a_{x,y},下一次可以向下移动,此时坐标变为a_{x+1,y},也可以向右移动,此时坐标变为a_{x,y+1}。也就是说我们的搜索树每层有2种情况。

那么这棵树有多少层呢?我们无从获知。但是这棵树的层数真的有这么重要吗?回想树层数的作用,不过是用于确定终止条件而已,而这个题的终止条件并不是选择多少个格子,而是到达终点,所以这棵树的层数根本就不重要。

搜索树

完成了思考,接下来,我们古法炮制dfs代码。

  1. 生成答案。当此时搜索的坐标达到终点的坐标时,将答案累计1。我们存储马及其控制点的坐标至数组forbid中,如果传入的坐标在forbid中,函数立即返回;并且,如果坐标超出了坐标系的界限,函数也立即返回。
if (x < 0 || y < 0 || x > dest[0] || y > dest[1])
    return;

for (int i = 0; i < 8; ++i)
    if (forbid[x][y])
        return;

if (x == dest[0] && y == dest[1])
{
    ans++;
    return;
}

  1. 遍历搜索树的一层。任意一层有2种情况。也就是两个dfs函数的调用。
  2. 产生情况。如果当前位置未访问,就访问接下来的两个临近位置,并且完成对vis数组的更新如上,不再赘述。
if (!vis[x][y])
{
    vis[x][y] = true;
    dfs(x + 1, y);
    dfs(x, y + 1);
}
  1. 回溯。将已经被扫过的数字接触占用,将被占用的vis[i]设为未占用。
vis[x][y] = false;

整理上述思路,容易构建深搜如下:

#include<iostream>
#include <iomanip>

using namespace std;

int dest[2],ans = 0;
bool vis[21][21] = {0},forbid[21][21] = {0};
void dfs(int x, int y);

int main()
{
    int hx, hy;
    cin >> dest[0] >> dest[1];
    cin >> hx >> hy;

    int dx[] = { -2,-1,1,2,2,1,-1,-2 };
    int dy[] = { 1,2,2,1,-1,-2,-2,-1 };

    forbid[hx][hy] = true;
    for (int i = 0; i < 8; ++i)
    {
        if (hx + dx[i] <= dest[0] && hy + dy[i] <= dest[1])
            forbid[hx + dx[i]][hy + dy[i]] = true;
    }

    dfs(0, 0);
    cout << ans << endl;
    return 0;
}

void dfs(int x,int y)
{
    if (x < 0 || y < 0 || x > dest[0] || y > dest[1])
        return;

    for (int i = 0; i < 8; ++i)
        if (forbid[x][y])
            return;

    if (x == dest[0] && y == dest[1])
    {
        ans++;
        return;
    }

    if (!vis[x][y])
    {
        vis[x][y] = true;
        dfs(x + 1, y);
        dfs(x, y + 1);
        vis[x][y] = false;
    }
},

巩固提高

坐标系中的问题不一定非得要深搜每一个格子,例如“八皇后”问题:
一个如下的 6 \times 6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

棋盘图片

上面的布局可以用序列 2\ 4\ 6\ 1\ 3\ 5 来描述,第 i 个数字表示在第 i 行的相应位置有一个棋子:行号 1\ 2\ 3\ 4\ 5\ 6列号 2\ 4\ 6\ 1\ 3\ 5。这只是棋子放置的一个解,请编一个程序找出所有棋子放置的解,并把它们以上面的序列方法输出,解按字典顺序排列。 请输出前 3 个解。最后一行是解的总个数。输入一行一个正整数 n,表示棋盘是 n \times n 大小的。输出前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。

不难发现,每一行/每一列都是只能放置一个棋子的。我们可以以行构建搜索树。每一行中,我们都有n(在示例中为6)种放置方法,很容易构建搜索树。

搜索树

之后进行深搜即可。这里的深搜终止条件是同一列或者对角线已经被其它棋子占用。我们可以用三个数组col[n]diagp[n]diagn[n]存储同一列、斜率为正和负的对角线的占用情况。列可以用直接用x坐标表示,如何表示一条对角线呢?观察可以发现,两条对角线的其中一条x+y为定值,另外一条x-y为定值。根据这些推断直接套上dfs板子即可,不再赘述:

#include<iostream>

using namespace std;
void dfs(int layer);
int col[30], diagp[30], diagn[30], status[15],ans = 0, n = 0;

int main()
{
    cin >> n;
    dfs(1);

    cout << ans << endl;
    return 0;
}

void setval(int l, int i, int val)
{
    status[l] = i;
    col[i] = val;
    diagp[l + i] = val;
    diagn[l - i + n] = val;
}

void dfs(int layer)
{
    if (layer > n)
    {
        ans++;
        if (ans <= 3)
        {
            for (int i = 1; i <= n; ++i)
                cout << status[i] << " ";
            cout << endl;
        }
    }

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

推荐阅读更多精彩内容