LeetCode 动态规划专题 7:面试中的 0-1 背包问题

“动态规划”的本质就是“记忆化递归”,有很多重复子问题,所以必须“记忆”。

例 1 :LeetCode 第 416 题:分割等和子集

传送门:分割等和子集

给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

  1. 每个数组中的元素不会超过 100
  2. 数组的大小不会超过 200

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

分析:如果在前面的几个整数中,可以找到全部整数和一半的整数,这个问题就得到了解决。这是一个典型的 0-1 背包问题,即:要求我们在 n 个物品中选出一定物品,填满容量为 \cfrac{sum}{2} 的背包。

于是我们可以使用“动态规划”来解决,“动态规划”在本质上是“记忆化递归”。关键在于定义“状态”,递推得到“状态转移方程”。

第 1 步:定义“状态”。

F( i , C ):考虑“编号”从 0ii+1 个物品是否能够填满容量为 C 的背包。

第 2 步:找到“状态转移方程”。

分类讨论:分“要当前这个数”和“不要当前这个数”两个情况考虑,则“状态转移方程”为: F ( i , c ) = F( i-1 , c ) or F( i-1 , c - w(i) )

时间复杂度:O( n \times \cfrac{sum}{2} ) = O( n \times sum )。所有数字和为 20000,背包最大为 10000n \times \cfrac{sum}{2} = 100 \times 10000 = 1000000100万)。

写法1:“记忆化递归”,这个写法会超时,应该改用从下到上的动态规划来做,不过理解下面这种写法有助于我们理解“动态规划”的本质。

Python 代码:

class Solution:

    def __init__(self):
        self.cache = None

    def __try_partition(self, nums, index, C):
        """
        给定 nums 表示给出的容量,考虑索引区间 [0, index] ,是否能够得到 sum_
        分类讨论的标准就是要不要 nums[index] 这个物品
        f(i,j) = f(i-1,j) or f(i,j-nums[i])
        :param nums:
        :param index:
        :param C:
        :return:
        """

        # 先写递归终止条件
        if C == 0:
            return True
        if index < 0 or C < 0:
            return False

        if self.cache[index][C]:
            return self.cache[index][C]

        # 情况1:不考虑 nums[index] 这个物品,看看能不能满足条件
        res = self.__try_partition(nums, index - 1, C)
        # 情况2:考虑 nums[index] 这个物品,如果它可以放进背包
        if nums[index] <= C:
            res = res or self.__try_partition(nums, index - 1, C - nums[index])
        self.cache[index][C] = res
        return res

    def canPartition(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """

        l = len(nums)
        if l == 0:
            return False
        # 特判:如果价值为奇数,不可以,直接返回 False
        s = sum(nums)
        if s & 1 == 1:
            return False
        half = s // 2
        self.cache = [[None for _ in range(half + 1)] for _ in range(l)]
        return self.__try_partition(nums, l - 1, half)

写法2:动态规划。

Python 代码:

class Solution:

    def canPartition(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """

        # 物品的数量
        l = len(nums)
        if l == 0:
            return False

        s = sum(nums)
        if s & 1 == 1:
            return False

        # 二维 dp 问题:背包的容量
        half = s // 2

        dp = [[0 for _ in range(half + 1)] for _ in range(l)]

        # 其实使用一维数组就可以了
        # 先写第 1 行:看看第 1 个数是不是能够填满这个背包
        for i in range(half + 1):
            dp[0][i] = False if nums[0] != i else True
        # i 表示物品索引
        for i in range(1, l):
            # j 表示容量
            for j in range(half + 1):
                if j >= nums[i]:
                    dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
                else:
                    dp[i][j] = dp[i - 1][j]
        return dp[-1][-1]

练习 1 :LeetCode 第 322 题:Coin Change

传送门:322. 零钱兑换

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

示例 1:

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

示例 2:

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

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

思路:使用动态规划。

第 1 步:定义状态。

dp[i]:凑齐总价值 i 需要的最少硬币数,状态就是问的问题。

第 2步:思考“状态转移方程”。

假设dp[11] = 3,如何递推得到“状态转移方程”呢?

dp[12] = mindp[11] + (1 这一枚硬币),

                         `dp[10]` +( 2 这一枚硬币),

                         `dp[7]` + (5 这一枚硬币))。

这里要注意的一点是:1、首先硬币的面值应该要小于等于当前要凑出来的面值;2、剩余的那个面值应该不是 -1,满足以上两点,才是“状态转移方程”。

这样就把一个大问题向一个小问题转化。要注意的地方:求最小值,因此我们的初始值应该设置成一个很大的值去和计算出来的值比较,取其中较小的那一个。这里题目中给出了 -1 的定义,因此我们在编码的时候,应该要考虑什么时候我们定义的状态的值为 -1,即已经有的硬币的都不能凑出指定金额的时候。

Java 代码:

public class Solution {

    public int coinChange(int[] coins, int amount) {
        int len = coins.length;
        if (amount == 0) {
            return 0;
        }
        if (coins == null || len == 0) {
            return -1;
        }

        // 要把 0 算进去,这是基本情况,所以开辟 coins 这么多空间的
        // 组成当前的总价值需要的最少硬币数
        int[] dp = new int[amount + 1];
        // dp 问题的套路,把初值先算出来,因为递归到底的时候,可能到这个地方
        dp[0] = 0;

        for (int i = 1; i <= amount; i++) {
            int minCount = Integer.MAX_VALUE;
            for (int j = 0; j < coins.length; j++) {
                if (i - coins[j] >= 0 && dp[i - coins[j]] != -1) {
                    minCount = Integer.min(dp[i - coins[j]] + 1, minCount);
                }
            }
            dp[i] = minCount == Integer.MAX_VALUE ? -1 : minCount;
        }
        return dp[amount];
    }

    public static void main(String[] args) {
        Solution solution = new Solution();

        int[] coins = {1};
        int amount = 0;
        int coinChange = solution.coinChange(coins, amount);
        System.out.println(coinChange);
    }
}

Python 代码:

class Solution:
    def coinChange(self, coins, amount):
        """
        :type coins: List[int]
        :type amount: int
        :rtype: int
        """

        # 特判
        size = len(coins)
        if size == 0:
            return -1

        dp = [0 for _ in range(amount + 1)]

        for i in range(1, amount + 1):
            min_val = 9999
            for j in range(0, size):
                # 注意:能分割,并且有值才进行比较
                if i - coins[j] >= 0 and dp[i - coins[j]] != -1:
                    min_val = min(min_val, dp[i - coins[j]] + 1)
            if min_val == 9999:
                dp[i] = -1
            else:
                dp[i] = min_val

        return dp[-1]


if __name__ == '__main__':
    coins = [1, 2, 5]
    amount = 11
    solution = Solution()

    result = solution.coinChange(coins, amount)
    print(result)

优化思路:dp[x + c] = min(dp[x] + 1, dp[x + c])

Python 代码:

class Solution:
    def coinChange(self, coins, amount):
        """
        :type coins: List[int]
        :type amount: int
        :rtype: int
        """

        dp = [0] + [-1] * amount

        for x in range(amount + 1):

            if dp[x] == -1:
                continue
            for c in coins:
                if x + c > amount:
                    continue
                if dp[x + c] == -1 or dp[x + c] > dp[x] + 1:
                    dp[x + c] = dp[x] + 1
        return dp[-1]


if __name__ == '__main__':
    coins = [1, 2, 5]
    amount = 11
    solution = Solution()

    result = solution.coinChange(coins, amount)
    print(result)

参考资料:https://blog.csdn.net/asd136912/article/details/79080693

总结:这道题其实不难,但是在实现上有一些小的细节要处理。

练习 2 :LeetCode 第 377 题 :Combination Sum IV

传送门:377. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

进阶:
如果给定的数组中含有负数会怎么样?
问题会产生什么变化?
我们需要在题目中添加什么限制来允许负数的出现?

致谢:
特别感谢 @pbrother 添加此问题并创建所有测试用例。

思路:定义状态 dp[i]:和为 i 的正整数的组合个数。

LeetCode 第 377 题 :Combination Sum IV

要特别注意:在 0 这一点,我们是定义 dp[0] = 1的,因为它表示如果 nums 里有一个数恰好等于 target,它单独成为 1 种可能。

Python 代码:

class Solution:
    def combinationSum4(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        size = len(nums)
        if size == 0 or target <= 0:
            return 0

        dp = [0 for _ in range(target + 1)]
        
        # 这一步很关键,想想为什么 dp[0] 是 1
        # 因为 0 表示空集,空集和它"前面"的元素凑成一种解法,所以是 1
        # 这一步要加深体会
        
        dp[0] = 1

        for i in range(1, target + 1):
            for j in range(size):
                if i >= nums[j]:
                    dp[i] += dp[i - nums[j]]

        return dp[-1]

Java 代码:与上面的写法等价

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        
        dp[0] = 1;
        for (int i = 1; i < target + 1; i++) {
            for (int num : nums) {
                if (i >= num) {
                    dp[i] = dp[i] + dp[i - num];
                }
            }
        }
        return dp[target];
    }
}

下面这种写法从下往上更新。

Java 代码:

public int combinationSum4(int[] nums, int target) {
    int[] memo = new int[target + 1];
    memo[0] = 1;
    for (int i = 0; i < target; i++) {
        for (int num : nums) {
            if (i + num <= target) {
                memo[i + num] += memo[i];
            }
        }
    }
    return memo[target];
}

练习 3 :LeetCode 第 474 题:Ones and Zeroes

传送门:474. 一和零

在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m0n1。另外,还有一个仅包含 01 字符串的数组。

你的任务是使用给定的 m0n1 ,找到能拼出存在于数组中的字符串的最大数量。每个 01 至多被使用一次

注意:

  1. 给定 01 的数量都不会超过 100
  2. 给定字符串数组的长度不会超过 600

示例 1:

输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4

解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。

示例 2:

输入: Array = {"10", "0", "1"}, m = 1, n = 1
输出: 2

解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。

注意:什么叫组成?必须用完?还是有剩余就可以?

练习 4 :LeetCode 第 139 题: Word Break

传送门:139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

  • 拆分时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

思路:动态规划。

LeetCode 第 139 题: Word Break-1
LeetCode 第 139 题: Word Break-2

状态:dp[i] 表示 s[0:i] 可以被空格拆分,拆分以后的单词全落在 wordDict 中。这个状态就是题目要求的。

状态转移方程;dp[j] = dp[i] and s[i + 1,j] in wordDict。注意:编写代码上有技巧,一定是先判断 s[i + 1,j] in wordDict,这样可以很快得出前缀字符串是否可以分割。

写法1:记忆化递归,自上而下。

Python 代码:注意:先看右边在不在单词字典中,注编码的细节,不考虑空字符串,所以 j 从 1 开始,到 ·size -1 结束

class Solution:

    def __init__(self):
        self.memo_ = dict()

    def wordBreak(self, s, wordDict):
        """
        :type s: str
        :type wordDict: List[str]
        :rtype: bool
        """

        # 题目说了 s 非空,wordDict 非空
        word_set = set(wordDict)

        return self.__word_break(s, word_set)

    def __word_break(self, s, word_set):

        # 首先看缓存
        if s in self.memo_:
            return self.memo_[s]

        # 特判
        if s in word_set:
            self.memo_[s] = True
            return True

        res = False

        for j in range(1, len(s)):

            left = s[:j]
            right = s[j:len(s)]

            # 注意:不要把 self.__word_break(left, word_set) 这一个判断放在前面
            # 否则会发生死循环,如果在代码编写过程中出现死循环,不要着急,debug 一下
            # 很快就会找到原因的
            if right in word_set and self.__word_break(left, word_set):
                res = True
                break
        self.memo_[s] = res
        return res

写法2:动态规划,自下而上。

注意1:需要长度为 0 的状态,定义为 True,因为要处理一种特例,即整个字符串恰好就在 wordDict 中;

注意2:注意边界条件:后数组的起始索引,表示了前数组的长度。

Python 代码:重点关注这里设置 dp[0] = True 的原因

class Solution:
    def wordBreak(self, s, wordDict):
        """
        :type s: str
        :type wordDict: List[str]
        :rtype: bool
        """

        size = len(s)
        word_set = set(wordDict)

        # dp[i]:长度为 i 的 s 字符串经过空格分隔以后在 wordDict 中
        # 需要长度为 0,因此前面加上一个 True
        dp = [True] + [False for _ in range(size)]

        for i in range(1, size + 1):
            # j 其实就是前缀的长度,注意这个细节
            for j in range(i):
                right = s[j:i]
                if right in word_set and dp[j]:
                    dp[i] = True
                    # 注意,这里 break
                    break
        return dp[-1]


if __name__ == '__main__':
    s = "leetcode"
    wordDict = ["leet", "code"]
    solution = Solution()

    result = solution.wordBreak(s, wordDict)
    print(result)

LeetCode 第 140 题:单词拆分 II

传送门:140. 单词拆分 II

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。

说明:

  • 分隔时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。

示例 1:

输入:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
输出:
[
"cats and dog",
"cat sand dog"
]

示例 2:

输入:
s = "pineapplepenapple"
wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
输出:
[
"pine apple pen apple",
"pineapple pen apple",
"pine applepen apple"
]
解释: 注意你可以重复使用字典中的单词。

示例 3:

输入:
s = "catsandog"
wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:
[]

思路:先用第 139 题做预处理,然后再 DFS,DFS 的时候,后面分割出来的单词先加入。

Python 代码:关键在于使用 dp 数组作为 DFS 的判断条件。

class Solution:
    def wordBreak(self, s, wordDict):
        """
        :type s: str
        :type wordDict: List[str]
        :rtype: List[str]
        """

        size = len(s)

        if size == 0:
            return None

        word_set = set(wordDict)

        # dp[i] 表示长度为 i 的 s,满足题意
        # 0 表示 False ,1 表示 True
        dp = [0 for _ in range(size + 1)]
        dp[0] = 1

        for i in range(1, size + 1):
            # i 表示 s 子串的长度
            for j in range(i):
                # j 表示后子串的起始位置,最多到 i-1
                # j 也正正好表示前子串的长度
                if s[j:i] in word_set and dp[j]:
                    dp[i] = 1

        res = []
        if dp[-1]:
            self.__dfs(s, size, wordDict, res, [], dp)
        return res

    def __dfs(self, s, length, wordDict, res, path, dp):

        if length == 0:
            res.append(' '.join(reversed(path)))
            return

        for i in range(length):
            cur = s[i:length]

            if dp[i] and cur in wordDict:
                path.append(cur)
                self.__dfs(s, i, wordDict, res, path, dp)
                path.pop()

练习5:LeetCode 第 494 题:Target Sum

传送门:494. 目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 +-。对于数组中的任意一个整数,你都可以从 +-中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

注意:

  1. 数组的长度不会超过20,并且数组中的值全为正数。
  2. 初始的数组的和不会超过1000。
  3. 保证返回的最终结果为32位整数。

思路1:搜索,回溯。

Python 代码:提交到 LeetCode 上会超时

class Solution:

    def __init__(self):
        self.res = 0

    def findTargetSumWays(self, nums, S):
        """
        :type nums: List[int]
        :type S: int
        :rtype: int
        """
        size = len(nums)

        self.__dfs(nums, size, 0, 0, S)
        return self.res

    def __dfs(self, nums, size, start, cur_sum, S):
        if start == size:
            # 到尾巴了,看看累积和是不是达到 S 了
            if cur_sum == S:
                self.res += 1
                return
        else:
            self.__dfs(nums, size, start + 1, cur_sum + nums[start], S)
            self.__dfs(nums, size, start + 1, cur_sum - nums[start], S)


if __name__ == '__main__':
    solution = Solution()
    nums = [35, 25, 24, 23, 2, 47, 39, 22, 3, 7, 11, 26, 6, 30, 5, 34, 10, 43, 41, 28]
    S = 49
    result = solution.findTargetSumWays(nums, S)
    print(result)

C++ 代码:

class Solution {
    int res=0;
    int sum=0;
    int len=0;
    
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        len=nums.size();
        dfs(nums,0,S);
        return res;
        
    }
    
    void dfs(vector<int>& nums,int p,int s){
        if(p==len){
            if(s==sum)++res;
            return;
        }
        sum+=nums[p];
        dfs(nums,p+1,s);
        sum-=2*nums[p];
        dfs(nums,p+1,s);
        sum+=nums[p];
    }
};

Java 代码:

class Solution {

    public int findTargetSumWays(int[] nums, int S) {
        return findTargetSumWays(nums, 0, S, 0);
    }

    private int findTargetSumWays(int[] nums, int index, int S, int sum) {
        int res = 0;
        if (index == nums.length) {
            return sum == S ? ++res : res;
        }
        res += findTargetSumWays(nums, index + 1, S, sum + nums[index]);
        res += findTargetSumWays(nums, index + 1, S, sum - nums[index]);
        return res;
    }
}

思路2:其实是一个 01 背包问题。原问题等价于: 找到 nums 的两个子集,对这个两个子集分解求和,然后作差等于 target。我们假设 P 是其中一个子集,N 是剩下的元素组成的子集。

例如: 假设 nums = [1, 2, 3, 4, 5]target = 3,一个可能的解决方案是 +1 - 2 + 3 - 4 + 5 = 3,这里子集P = [1, 3, 5],子集 N = [2, 4]

那么让我们看看如何将其转换 0-1 背包问题:

因为 sum(P) - sum(N) = target,等式两边都加上 sum(nums),得:

2 * sum(P) = target + sum(nums)

因此,原来的问题已转化为一个求子集的和问题: 找到 nums 的一个子集 P,使得 sum(P) = (target + sum(nums)) / 2

请注意,上面的公式已经证明 target + sum(nums)必须是偶数,否则输出为 0

Python 代码:

class Solution:
    def findTargetSumWays(self, nums, S):
        """
        :type nums: List[int]
        :type S: int
        :rtype: int
        """
        size = len(nums)
        ss = sum(nums)
        target = ss + S
        if size == 0 or target & 1:
            return 0

        # 除以 2
        target >>= 1
        # 因为题目中给出的是非负整数,因此这一步可以提前判断是否有解
        if target > ss:
            return 0

        dp = [[0 for _ in range(target + 1)] for _ in range(size)]

        # 这一步不要忘记了
        dp[0][0] = 1
        for j in range(target + 1):
            if nums[0] == j:
                dp[0][j] += 1

        for i in range(1, size):
            for j in range(target + 1):
                # 至少是不选这个物品时候的种数
                dp[i][j] += dp[i - 1][j]
                if j >= nums[i]:
                    dp[i][j] += dp[i - 1][j - nums[i]]
        return dp[-1][-1]


if __name__ == '__main__':
    solution = Solution()
    # nums = [1, 1, 1, 1, 1]
    # S = 3
    nums = [35, 25, 24, 23, 2, 47, 39, 22, 3, 7, 11, 26, 6, 30, 5, 34, 10, 43, 41, 28]
    S = 49
    result = solution.findTargetSumWays(nums, S)
    print(result)

Java 代码:

public int findTargetSumWays(int[] nums, int s) {
    int sum = 0;
    for (int n : nums)
        sum += n;
    return sum < s || (s + sum) % 2 > 0 ? 0 : subsetSum(nums, (s + sum) >>> 1); 
}   

public int subsetSum(int[] nums, int s) {
    int[] dp = new int[s + 1]; 
    dp[0] = 1;
    for (int n : nums)
        for (int i = s; i >= n; i--)
            dp[i] += dp[i - n]; 
    return dp[s];
} 

参考资料:https://leetcode.com/problems/target-sum/discuss/97334/Java-(15-ms)-C++-(3-ms)-O(ns)-iterative-DP-solution-using-subset-sum-with-explanation

(本节完)

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

推荐阅读更多精彩内容