算法与数据结构 之 动态规划

image.png

一、分治,回溯,递归,动态规划

1.1、递归的代码模板

public void recur(int level, int param) {
    // terminator
    if(level > MAX_LEVEL) {
        // process result
        return;
    }
    
    // process current logic
    process(level, param);
    
    // drill down
    recur(level: level + 1, newParam);
    
    // restore current status
}

1.2、分治(Divide & Conquer)的代码模板

def divide_conquer(problem, param1, param2, ...):
    # recursion terminator
    if problem is None:
        print_result
        return
    # prepare data
    data = prepare_data(problem)
    subproblems = split_problem(problem, data)
    # conquer subproblems
    subresult1 = self.divide_conquer(subproblems[0], p1, ...)
    subresult2 = self.divide_conquer(subproblems[1], p1, ...)
    subresult3 = self.divide_conquer(subproblems[2], p1, ...)
    ...
    # process and generate the final result
    result = process_result(subresult1, subresult2, subresult2, ...)

1.3、联系

1、动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)。
2、共性:找到重复子问题。
3、差异性:最优子结构,中途可以淘汰次优解。

1.4、本质

本质都是在寻找重复性,将重复性转化为 计算机指令集if else, for, loop, while ...

1.5、总结

1、人肉递归低效,很累。
2、找到最近最简方法,将其拆解成可重复解决的问题。
3、数学归纳法思维(抵制人肉递归的诱惑),先看n=1,n=2时的情况,然后看f(n)和f(n-1)之间的联系。

二、动态规划

2.1、定义

1、动态规划的英文叫Dynamic programming,翻译过来叫动态规划,翻译很玄学,其实翻译成动态递推更容易理解。
2、动态规划的本质就是将一个复杂问题拆解成若干个子问题进行解决。原文为:Simplifying a complicated problem by breaking it down into simpler sub-problems.
3、动态规划其实就是分治+ 最优子结构( Divide & Conquer + Optimal substructure)。

2.2、关键点

1、最优子结构:公式为,opt[n] = best_of(opt[n-1], opt[n-2], ...)
2、储存中间状态:opt[i]
3、递推公式(美其名曰:状态转移方程或者DP方程):Fib:opt[i] = opt[n-1] + opt[n-2]. 二维路径为 opt[i,j] = opt[i+1][j] + opt[i][j+1] , 其中,需要判断opt[i,j]是否为空。

2.3、小结

1、打破自己的思维惯性
2、理解复杂逻辑的关键
3、职业进阶的要点要领

三、实战例题

例题1:杨辉三角 https://leetcode-cn.com/problems/pascals-triangle/

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

image

在杨辉三角中,每个数是它左上方和右上方的数的和。

示例:

输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]

思路:

1、动态规划,dp存的是结果,和nums产生联系
2、初始方程: dp = [[1] * (i+1) for i in range(num)], 例如 num = 5, 得到[[1], [1, 1], [1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1, 1]]
3、递归方程: dp[i][j] = dp[i-1][j] + dp[i-1][j-1]

时间复杂度:O(mn), m为深度, n为每一层的平均个数
空间复杂度:O(m
n),m为深度, n为每一层的平均个数

代码实现:

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        dp = [[1]*(i+1) for i in range(numRows)]
        for i in range(2, numRows):
            for j in range(1, len(dp[i]) - 1):
                dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
        return dp

例题2: 打家劫舍 II https://leetcode-cn.com/problems/house-robber-ii/

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:

输入:nums = [0]
输出:0

思路:

1、动态规划
2、初始方程: pre, cur = 0, 0
3、递推方程: pre, cur = cur, max(pre + nums[i], cur)

时间复杂度:O(n)
空间复杂度:O(1)

代码实现:

class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return nums[0]
        def myrob(nums):
            pre, cur = 0, 0
            for i in range(len(nums)):
                pre, cur = cur, max(pre + nums[i], cur)
            return cur
        return max(myrob(nums[:-1]), myrob(nums[1:]))

例题3: 62. 不同路径 https://leetcode-cn.com/problems/unique-paths/

image

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28
示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右

思路:
如果沿着边走,即m=0或者n=0时,可以走的选择都是1种,如果不是贴着边走的话,每一步的走法等于上边和左边的走法之和。按着这种思路,可以事先把动态方程的初始方程写成贴着边的都是1,其余都是0。当然也可以在遍历的时候写。具体看下面代码实现。
1、初始方程:dp = [[1] * n] + [[1] + [0] * (n - 1) for _ in range(m - 1)]
2、递推方程:dp[i][j] = dp[i][j-1] + dp[i-1][j]

时间复杂度:O(mn)
空间复杂度:O(m
n)

代码实现:

# 写法一:事先定义好初始方程,贴着边的都是1,不贴着边的都是0,拿m=3,n=7 举例, 得到的初始方程为,[[1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0]]
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[1] * n] + [[1] + [0] * (n - 1) for _ in range(m - 1)]
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i][j-1] + dp[i-1][j]
        return dp[-1][-1]

# 写法二:直接在遍历的时候判断
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0] * n for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if i == 0 or j == 0:
                    dp[i][j] = 1
                else:
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[-1][-1]  

# 写法三:直接在所有格子填写1,在第2行第2列开始做累加(等于上1格+左边1格)
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[1]*n for _ in range(m)]
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[-1][-1]

例题4: 53. 最大子序和https://leetcode-cn.com/problems/maximum-subarray/

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路:

1、初始方程:dp = nums
2、递推方程:dp[i] = max(dp[]i-1] + nums[i], dp[i])

时间复杂度:O(n)
空间复杂度:O(n)

代码实现:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp = nums
        for i in range(1, len(nums)):
            dp[i] = max(dp[i-1] + nums[i], dp[i])
        return max(dp)

例题5: 718. 最长重复子数组 https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。

思路:
思路:动态规划dp,数组元素如果相等,则等于对角线+1, 同时,比较当前值和计数值,返回计数值和当前值的较大者。
1、初始方程:dp = [[0] * (len(B) + 1) for _ in range(len(A) + 1)]
2、递推方程:dp[i][j] = dp[i-1][j-1] + 1, res = max(res, dp[i][j])

时间复杂度:O(mn), m为A的长度,n为B的长度
空间复杂度:O(m
n)

代码实现:

class Solution:
    def findLength(self, A: List[int], B: List[int]) -> int:
        res = 0
        dp = [[0] * (len(B) + 1) for _ in range(len(A) + 1)]
        for i in range(len(A)):
            for j in range(len(B)):
                if A[i] == B[j]:
                    dp[i][j] = dp[i-1][j-1] + 1
                    res = max(res, dp[i][j])
        return res

例题6: 70. 爬楼梯 https://leetcode-cn.com/problems/climbing-stairs/

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

思路:

1、初始方程:dp = [0] * (n+1), dp[0] = dp[1] = 1
2、递推方程:dp[i] = dp[i-1] + dp[i-2]

时间复杂度:O(n)
空间复杂度:O(n)

代码实现:

class Solution:
    def climbStairs(self, n: int) -> int:
        dp = [0] * (n+1)
        dp[0] = dp[1] = 1
        for i in range(2, n+1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[-1]

例题7: 120. 三角形最小路径和 https://leetcode-cn.com/problems/triangle/

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

例如,给定三角形:

[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

思路:

自底向上动态规划,从三角形的底部倒着递推至三角形的顶部,逐层进行递推,递推的规律为,当前元素等于下一层中的相同位置,或者+1位置的最小值中的元素,加上当前元素本身。

1、初始方程:dp = [[0] * (n+1) for _ in range(m+1)]
2、递推方程:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]

时间复杂度:O(mn) m为三角形的深度,n为三角形每行的个数
空间复杂度:O(m
n)

代码实现:

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        m = len(triangle)
        n = len(triangle[-1])
        dp = [[0] * (n+1) for _ in range(m+1)]
        for i in range(m - 1, -1, -1):
            for j in range(len(triangle[i])):
                dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
        return dp[0][0]

2021-01-09 更新思路:构造dp方程的时候,可以直接构造一个三角形
初始方程:dp = [[0]*(i + 1) for i in range(len(triangle) + 1)]
完整的代码为:

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        dp = [[0]*(i + 1) for i in range(len(triangle) + 1)]
        for i in range(len(triangle) -1, -1, -1):
            for j in range(len(triangle[i])):
                dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
        return dp[0][0]

例题8: 1143. 最长公共子序列 https://leetcode-cn.com/problems/longest-common-subsequence/

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

思路:
动态规划,注意dp的初始长度是+1,所以递推方程中用的是i+1开始,而不是i开始, 即dp[i+1][j+1]
1、初始方程:dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)]
2、递推方程:相等:dp[i+1][j+1] = dp[i][j] + 1, 不相等:dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])

时间复杂度: O(mn) m为text1的长度,n为text2的长度
空间复杂度:O(m
n)

代码实现:

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)]
        for i in range(len(text1)):
            for j in range(len(text2)):
                if text1[i] == text2[j]:
                    dp[i+1][j+1] = dp[i][j] + 1
                else:
                    dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
        return dp[-1][-1]

例题9: 第 k 个数 https://leetcode-cn.com/problems/get-kth-magic-number-lcci/

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

示例 1:

输入: k = 5

输出: 9

思路:
动态规划
1、初始方程:dp = [0] * k, dp[0] = 1
2、递推方程:dp[i] = min(dp[num3] * 3, min(dp[num5] * 5, dp[num7] * 7))

时间复杂度: O(n)
空间复杂度:O(n)

代码实现:

class Solution:
    def getKthMagicNumber(self, k: int) -> int:
        num3, num5, num7 = 0, 0, 0
        dp = [0] * k
        dp[0] = 1
        for i in range(1, k):
            dp[i] = min(dp[num3] * 3, min(dp[num5] * 5, dp[num7] * 7))
            if dp[i] == dp[num3] * 3:
                num3 += 1
            if dp[i] == dp[num5] * 5:
                num5 += 1
            if dp[i] == dp[num7] * 7:
                num7 += 1
        return dp[-1]

例题11: 64. 最小路径和 https://leetcode-cn.com/problems/minimum-path-sum/
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1:


image

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

思路:
思路:dp动态规划
初始方程:dp = grid ,因为相等,所以可以不显示写出来,直接用grid; (说明:但是在工业级代码中还是建议用dp表示,因为不写dp,直接用grid的话,会改变原来的入参,把原来的参数改的面目全非,可能工程中并不希望改变原来的入参。)
递推方程:i == 0:grid[i][j] += grid[i][j-1]; j == 0: grid[i][j] += grid[i-1][j]; grid[i][j] += min(grid[i-1][j], grid[i][j-1])

时间复杂度: O(m*n)
空间复杂度:O(1)

代码实现:

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if i == 0 and j == 0:
                    continue
                elif i == 0:
                    grid[i][j] += grid[i][j-1]
                elif j == 0:
                    grid[i][j] += grid[i-1][j]
                else:
                    grid[i][j] += min(grid[i-1][j], grid[i][j-1])
        return grid[-1][-1]

例题12: 322. 零钱兑换 https://leetcode-cn.com/problems/coin-change/

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:

输入:coins = [2], amount = 3
输出:-1
示例 3:

输入:coins = [1], amount = 0
输出:0
示例 4:

输入:coins = [1], amount = 1
输出:1
示例 5:

输入:coins = [1], amount = 2
输出:2

思路:
思路:将总数amount进行拆解,每拆解一个硬币,则计数+1
初始方程:dp = [(amount + 1) for _ in range(amount + 1)]
递推方程:dp[i] = min(dp[i], dp[i-coin] + 1)

时间复杂度: O(m*n),m为硬币总数amount, n为不同硬币的个数
空间复杂度:O(m)

代码实现:

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [(amount + 1) for _ in range(amount + 1)]
        dp[0] = 0
        for i in range(amount + 1):
            for coin in coins:
                if coin <= i:
                    dp[i] = min(dp[i], dp[i-coin] + 1)
        if dp[amount] == amount + 1:
            return -1
        return dp[-1]

例题13: 198. 打家劫舍 https://leetcode-cn.com/problems/house-robber/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

思路:
思路:动态规划
初始方程:pre, cur = 0, 0
递推方程:pre, cur = cur, max(pre + nums[i], cur)
T:O(n), S:O(1)

时间复杂度: O(n)
空间复杂度:O(1)

代码实现:

class Solution:
    def rob(self, nums: List[int]) -> int:
        pre, cur = 0, 0
        for i in range(len(nums)):
            pre, cur = cur, max(pre + nums[i], cur)
        return cur

例题14: 第k个数 https://leetcode-cn.com/problems/get-kth-magic-number-lcci/

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

示例 1:

输入: k = 5

输出: 9

思路:
思路:动态规划dp
初始方程:dp = [0] * k,dp[0] = 1
递推方程:dp[i] = min(dp[num3]3, min(dp[num5]5, dp[num7]*7))

时间复杂度: O(k)
空间复杂度:O(k)

代码实现:

class Solution:
    def getKthMagicNumber(self, k: int) -> int:
        num3, num5, num7 = 0, 0, 0
        dp = [0] * k
        dp[0] = 1
        for i in range(1, k):
            dp[i] = min(dp[num3]*3, min(dp[num5]*5, dp[num7]*7))
            if dp[i] == dp[num3]*3:
                num3 += 1
            if dp[i] == dp[num5]*5:
                num5 += 1
            if dp[i] == dp[num7]*7:
                num7 += 1
        return dp[-1]

数据下载:

撰写记录
2020.10.10-10:57:05-第一次撰写
2020.12.14-06:55:17-第二次撰写
2020.12.19-04:18:10-第三次撰写
2020.12.22-06:12:18-第四次撰写
2020.12.23-07:18:00-第五次撰写
2020.12.25-07:32:10-第六次撰写
2020.12.27-08:59:10-第七次撰写
2021.01.04-06:58:00-第八次撰写
2021.01.10-07:39:01-第九次撰写

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