动态规划 - 贪心算法

6/22/2021 Algorithm

一般动态规划

dynamic programming

最大子数组和

力扣 53:最大子数组和 (opens new window)

public class MaxSumSubArray {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] f = new int[n];
        f[0] = nums[0];
        //动态规划
        //当前者和大于0,将其状态转移到后者
        //当前者和小于0,放弃掉前者和,重新计算最大和,即重新进行转移
        //对和的数组排序,返回其最大和
        for(int i = 1; i < n; i++){
            if(f[i-1] > 0){
                f[i] += f[i-1] + nums[i];
            }
            else{
                f[i] = nums[i];
            }
        }
        Arrays.sort(f);
        return f[n-1];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

解码方法

力扣 91:解码方法 (opens new window)

//动态规划:状态转移方程
public class NumDecodings {
    public int decodings(String s) {

        int n = s.length();
        int f[] = new int[n+1];
        f[0] = 1;
        //该在for循环中++i和i++等价
        for(int i = 1; i < n+1; i++) {
            f[i] = 0;
            //状态转移方程1
            //其实在12行不初始化,f[i]也默认为0
            if(s.charAt(i-1) != '0') {
                f[i] += f[i-1];
            }    
            //状态转移方程2
            //转换类型,判断两位数是否 <= 26
            if(i > 1 && s.charAt(i-2) != '0' && (s.charAt(i-2)-'0')*10 + (s.charAt(i-1)-'0') <= 26) {
                f[i] += f[i-2];
            }            
        }
        return f[n];
    }


    public static void main(String[] args) {
        NumDecodings nd = new NumDecodings();
        String str = "226712";
        System.out.println(nd.decodings(str));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

最长回文子串

力扣 5:最长回文子串 (opens new window)

//当s[i]==s[j],dp[i][j]是否回文取决于dp[i+1][j-1]是否回文
//所以用boolean数组dp[i][j]记录回文子串s[i]到s[j]的状态
//第一步初始化dp[i][i]=true,即单个字符均回文
//Len为子串长度,i为左边界,j为右边界

class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if(n<2){
            return s;
        }
        boolean[][] dp = new boolean[n][n];
        for(int i = 0; i < n; i++){
            dp[i][i] = true;
        }
        int maxLength = 1;
        int begin = 0;
        for(int Len = 2; Len <= n; Len++){
            for(int i = 0; i < n; i++){
                int j = i+Len-1;            
                if(j>=n){
                    break;
                }
                if(s.charAt(i)==s.charAt(j)){
                    if(j-i<3){
                        dp[i][j] = true;
                    }else{
                        dp[i][j] = dp[i+1][j-1];
                    }
                }else{
                    dp[i][j] = false;
                }
                if(dp[i][j] && j-i+1>maxLength){
                    begin = i;
                    maxLength = j-i+1;
                }
            }
        }
        return s.substring(begin, begin+maxLength);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

回文子串

力扣 647:回文子串 (opens new window)

class Solution {
    public int countSubstrings(String s) {
        int n = s.length(), count = 0;
        boolean dp[][] = new boolean[n][n];
        for(int i = 0; i < n; i++){
            dp[i][i] = true;
        }
        for(int Len = 2; Len <= n; Len++){
            for(int i = 0; i < n; i++){
                int j = i+Len-1;
                if(j>=n){
                    break;
                }
                if(s.charAt(i)==s.charAt(j)){
                    if(Len <= 3){
                        dp[i][j] = true;
                    }else{
                        dp[i][j] = dp[i+1][j-1];
                    }
                }else{
                    dp[i][j] = false;
                }
            }
        }
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                if(dp[i][j]){
                    count++;
                }
            }
        }
        return count;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

最长递增子序列

力扣 300:最长递增子序列 (opens new window)

用 dp[i] 记录第 i 个元素能构成的递增子序列大小:初始化为 1,对小于 i 的元素遍历,若当前元素大于 nums[j],则 dp[i] = dp[j]+1,此为状态转移方程

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if(n == 0){
            return 0;
        }
        int maxLength = 1;
        int[] dp = new int[n];
        dp[0] = 1;
        for(int i = 1; i < n; i++){
            dp[i] = 1;
            for(int j = 0; j < i; j++){               
                if(nums[i] > nums[j]){
                    dp[i] = Math.max(dp[i], dp[j]+1);
                }
            }
            maxLength = Math.max(dp[i], maxLength);
        }
        return maxLength;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

消除游戏

力扣 390:消除游戏 (opens new window)

1 2 3 4 5 6 7 8 9

消除一轮:2 4 6 8(从头开始消除单数序号的数)

消除二轮:2 6(从尾开始消除单数序号的数)

消除三轮:6 ——> 最终结果(从头消除单数序号)

先从最简单的情况入手:n=1时,答案为1。n=2时,答案为2。

可以发现,答案一定不会是奇数,因为第一轮操作一定会将所有的奇数删除。这就提示出一个规律: 如果n为奇数,那么以n结尾和以n-1结尾是完全一样的。 例如1,2,3,4,5,6,7,8,9,操作一轮后剩2,4,6,8,下一轮从8开始; 而1,2,3,4,5,6,7,8,操作一轮后也剩2,4,6,8,下一轮也从8开始。 从而得到第一个递推公式:当n为奇数时,dp[n] = dp[n-1];

2.3 接下来就只剩n为偶数的情况了。 仍以1,2,3,4,5,6,7,8为例,操作一轮后剩2,4,6,8, 下一轮从8开始。 那么2,4,6,8从8开始,和2,4,6,8从2开始有什么区别呢? 很明显就是轴对称的关系,前者剩6,后者就剩(8+2 - 6);

那么2,4,6,8从2开始,和1,2,3,4从1开始有什么区别呢? 这个关系更明显,就是2倍的关系。

写到这里,大家应该明白了,1,2,3,4从1开始不就是dp[4]吗! 也就是说,dp[8] = 2*(1+4-dp[4]) 这里的1+4-dp[4]起的就是轴对称的作用。 推而广之,n为偶数时,dp[n] = 2*(1+n/2-dp[n/2]) 这样完整的递推公式就完成了: n为奇数时,dp[n] = dp[n-1] n为偶数时,dp[n] = 2(1+n/2-dp[n/2])

由于dp[1000000]超出内存限制,采用递归的写法,思路一样,只是未构造dp数组

class Solution {

    public int dp(int n){
        if(n == 1){
            return 1;
        }
        if(n == 2){
            return 2;
        }
        if(n % 2 == 1){
            return dp(n-1);
        }
        if(n % 2 == 0){
            return 2*(1+n/2-dp(n/2));
        }
        return -1;
    }

    public int lastRemaining(int n) {
        return dp(n);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

猫和老鼠

力扣 913:猫和老鼠 (opens new window)

数据结构:无向图

算法:动态规划、深度优先搜索、递归

算尽从初始点穷尽努力的结果,没有变数

package com.solution;

import java.util.Arrays;

public class MouseCatGame {

    private static final int catWin = 2;
    private static final int draw = 0;
    private static final int mouseWin = 1;

    private int n;
    private int[][][] dp;
    private int[][] graph;

    public int mouseCatGame(int[][] graph){
        n = graph.length;
        dp = new int[n][n][2*n];
        for(int[][] i: dp){
            for(int[] j: i){
                Arrays.fill(j, -1);
            }
        }
        this.graph = graph;
        return getRes(1, 2, 0);
    }

    public int getRes(int mouse, int cat, int steps){
        if(steps >= 2*n){
            return draw;
        }
        if(dp[mouse][cat][steps] < 0){
            if(mouse == 0){
                dp[mouse][cat][steps] = mouseWin;
            } else if(mouse == cat){
                dp[mouse][cat][steps] = catWin;
            } else{
                getNextRes(mouse, cat, steps);
            }
        }
        return dp[mouse][cat][steps];
    }

    public void getNextRes(int mouse, int cat, int steps){
        int curMove = steps%2 == 0 ? mouse:cat;
        int defaultRes = curMove==mouse ? catWin:mouseWin;
        int res = defaultRes;
        for(int nextStep: graph[curMove]){
            if(curMove == cat && nextStep == 0){
                continue;
            }
            int mouseNextStep = curMove==mouse ? nextStep:mouse;
            int catNextStep = curMove==cat ? nextStep:cat;
            int nextRes = getRes(mouseNextStep, catNextStep, steps+1);
            if(nextRes != defaultRes){
                res = nextRes;
                if(res != draw){
                    break;
                }
            }
        }
        dp[mouse][cat][steps] = res;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

最长递增子序列的个数

力扣 673:最长递增子序列的个数 (opens new window)

dp[i]记录下标i元素能构成的最长递增子序列的长度

count[i]记录下标i元素构成最长递增子序列的道路总数

package com.solution;

import java.util.Arrays;

//1073741824
public class FindNumberOfLIS {
    private int[] dp;
    private int[] count;

    public int buildDp(int[] nums){
        int n = nums.length;
        dp = new int[n];
        count = new int[n];
        int maxLength = 1;
        for(int i = 0; i < n; i++){
            dp[i] = 1;
            count[i] = 1;
            for(int j = 0; j < n; j++){
                if(nums[j] < nums[i]){
                    if(dp[j] >= dp[i]){
                        dp[i] = dp[j]+1;
                        //重置计数
                        count[i] = count[j];
                    } else if(dp[i] == dp[j]+1){
                        count[i] += count[j];
                    }
                }
            }
            maxLength = Math.max(maxLength, dp[i]);
        }
        return maxLength;
    }

    public int findNumberOfLIS(int[] nums){
        int res = 0;
        int n = nums.length;
        int maxLength = buildDp(nums);
        if(maxLength == 1){
            return n;
        }
        for(int i = 1; i < n; i++){
            if(dp[i] == maxLength){
                res += count[i];
            }
        }
        return res;
    }

    public static void main(String[] args) {
        FindNumberOfLIS findNumberOfLIS = new FindNumberOfLIS();
        int[] nums = {0,2,1,4,3,6,5,8,7,10,9,12,11,14,13,16,15,18,17,20,19,22,21,24,23,26,25,28,27,30,29,32,31,34,33,36,35,38,37,40,39,42,41,44,43,46,45,48,47,50,49,52,51,54,53,56,55,58,57,60,59,61};
        System.out.println(findNumberOfLIS.findNumberOfLIS(nums));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

统计元音字母序列的数目

力扣 1220:统计元音字母序列的数目 (opens new window)

字符串中的每个字符都应当是小写元音字母(a, e, i, o, u) 每个元音a后面都只能跟着e 每个元音e后面只能跟着a或者是i 每个元音i后面 不能 再跟着另一个i 每个元音o后面只能跟着i或者是u 每个元音u后面只能跟着a

动态规划,dp[i][j]表示长度为i、以j结尾的元音字母序列的数目

其中j=0—>a, j=1->e, j=2->i, j=3->o, j=4->u

class Solution {
    public int countVowelPermutation(int n) {
        int mod = 1000000007;
        long[] dp = new long[5];
        long[] next = new long[5];
        //初始化dp[1][i]
        for(int i = 0; i < 5; i++){
            dp[i] = 1;
        }
        //通过dp[cur][i]逐步构造dp[next][i]
        for(int i = 2; i <= n; i++){
            next[0] = (dp[1]+dp[2]+dp[4]) % mod;
            next[1] = (dp[0]+dp[2]) % mod;
            next[2] = (dp[1]+dp[3]) % mod;
            next[3] = dp[2];
            next[4] = (dp[2]+dp[3]) % mod;
            System.arraycopy(next, 0, dp, 0, 5);
        }
        //至此dp[n][i]构造完毕,直接将五种序列(根据末尾区分)数目相加即可
        long ans = 0;
        for(int i = 0; i < 5; i++){
            ans = (dp[i]+ans) % mod;
        }
        return (int)ans;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

猫和老鼠

913. 猫和老鼠 - 力扣(LeetCode) (opens new window)

维护一个三维数组dp[mouse_pos][cat_pos][result],储存鼠、猫位置对应的游戏结果

  • 初始化:当猫鼠位置重合,猫获胜;当鼠在节点0,猫在任意节点,鼠获胜
  • 对于每个状态进行深度优先搜索,递归完善这个三维数组
  • 最后返回dp[1][2],即为游戏结果
package com.solution;

import java.util.Arrays;

public class MouseCatGame {

    // 结果状态
    //猫获胜
    private static final int catWin = 2;
    //平局
    private static final int draw = 0;
    //鼠获胜
    private static final int mouseWin = 1;

    //节点数
    private int n;
    //储存猫鼠节点以及对应结果
    private int[][][] dp;
    // 储存地图
    private int[][] graph;
    
    //开始游戏
    public int mouseCatGame(int[][] graph){
        n = graph.length;
        dp = new int[n][n][2*n];
        //初始化dp数组,令结果全为-1,即没有结果
        for(int[][] i: dp){
            for(int[] j: i){
                Arrays.fill(j, -1);
            }
        }
        this.graph = graph;
        return getRes(1, 2, 0);
    }
	
    //获取当前结果
    public int getRes(int mouse, int cat, int steps){
        //当移动步数大于2*n时,说明绝对有重复的步骤,直接返回平局0
        if(steps >= 2*n){
            return draw;
        }
        //当当前节点为-1,即没有结果
        if(dp[mouse][cat][steps] < 0){
            //若老鼠在洞里,返回老鼠获胜1
            if(mouse == 0){
                dp[mouse][cat][steps] = mouseWin;
            } else if(mouse == cat){//若猫鼠位置重合,返回猫获胜2
                dp[mouse][cat][steps] = catWin;
            } else{//否则进入深度优先搜索
                getNextRes(mouse, cat, steps);
            }
        }
        return dp[mouse][cat][steps];
    }

    public void getNextRes(int mouse, int cat, int steps){
        //判断当前是谁在移动
        int curMove = steps%2 == 0 ? mouse:cat;
        int defaultRes = curMove==mouse ? catWin:mouseWin;
        int res = defaultRes;
        //遍历可以移动的节点
        for(int nextStep: graph[curMove]){
            if(curMove == cat && nextStep == 0){
                continue;
            }
            int mouseNextStep = curMove==mouse ? nextStep:mouse;
            int catNextStep = curMove==cat ? nextStep:cat;
            //进入下一层dfs
            int nextRes = getRes(mouseNextStep, catNextStep, steps+1);
            if(nextRes != defaultRes){
                res = nextRes;
                if(res != draw){
                    break;
                }
            }
        }
        dp[mouse][cat][steps] = res;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

数组总和Ⅳ(377)

矩阵区域不超过k的最大数值和(363)

最小操作次数使数组元素相等(453)

KMP

Knuth-Morris-Pratt

已知 next 数组匹配字符串

#include <iostream>
#include <string.h>
#include <algorithm> 
using namespace std;


int main(){

    char str[1000];
    char dist[1000];
    cin >> str >> dist;

    //cout << str << " " << dist << endl;
    int count = 0, m = strlen(str), n = strlen(dist);
    int pos[m];


    int next[n];
    for(int i = 0; i < n; i++){
        cin >> next[i];
    }



    //cout << m << " " << n << endl;
    int i = 0, j = 0;
    while(i < m && j < n){
        if(j == -1 || tolower(str[i])==tolower(dist[j])){
            i++;
            j++;
        } else {
            j = next[j];
        }
        if(j == n){
            pos[count++] = i-j+1;
            i = i-1-next[j-1];
            j = 0;
        }        
    }


    cout << count << endl;
    for(i = 0; i < count; i++){
        cout << pos[i] << endl;
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

实现strStr()(28)

重复叠加字符串匹配(686)

重复的子字符串(459)

最短回文串(214)

前缀和

ProfixSum

区域和检索-数组不可变

力扣 303:区域和检索 - 数组不可变 (opens new window)

//优化解法:前缀和
class NumArray {

    private int[] nums;

    public NumArray(int[] nums) {
        int n = nums.length;
        this.nums = new int[n+1];
        for(int i = 0; i < n; i++){
            //储存前 i 个的和在数组 this.nums 中,用 nums[j+1]-nums[i] 可以得到第i个数到第j个数的和
            this.nums[i+1] = this.nums[i] + nums[i];
        }
    }

    public int sumRange(int left, int right) {
        int res = nums[right + 1] - nums[left]; 
        return res;
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(left,right);
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

二维区域和检索-矩阵不可变

力扣 304:二维区域和检索 - 矩阵不可变 (opens new window)

//采用303的优化解法:一维前缀和
class NumMatrix {

    private int[][] matrix;

    public NumMatrix(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        this.matrix = new int[m][n+1];
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                this.matrix[i][j+1] = this.matrix[i][j] + matrix[i][j];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        int res = 0;
        for(int i = row1; i <= row2; i++){
            res += matrix[i][col2+1] - matrix[i][col1];
        }
        return res;
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

和为 k 的子数组

力扣 560:和为 K 的子数组 (opens new window)

map<int,int>记录前缀和及其出现次数

  • cur 为当前和,即 sum(0, i)

  • map 中记录了各个位置 j 的前缀和,即 sum(0, j)

  • sum(0, i) - sum(0, j) = sum(j, i),若 sum(j, i) == target,则存在和为 target 的连续子数组(下标从 j 到 i)

  • 同时用 map->second 记录该前缀和出现次数,存在则直接加上 map[pre]

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        int n = nums.size();
        map<int, int> m;
        m[0] = 1;
        int count = 0, cur = 0;
        for(int i = 0; i < n; i++){
            cur += nums[i];
            if(m.count(cur-k)){
                count += m[cur-k];
            }
            m[cur]++;
        }
        return count;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

环绕字符串中唯一的子字符串(467)

区间子数组个数(795)

水果成篮(904)

K个不同的整数的子数组(992)

航班预订统计(1109)

连续数组(525)

零钱兑换 (opens new window)

整数拆分 (opens new window)

编辑距离 (opens new window)

贪婪算法

贪心算法,Greedy

买卖股票的最佳时机

力扣 121:买卖股票的最佳时机 (opens new window)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int pre = prices[0];
        int res = 0;
        for(int i = 1; i < prices.size(); i++){
            int cur = prices[i];
            if(cur > pre){
                res = max(res, cur-pre);
            } else {
                pre = cur;
            }
        }
        return res;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

递增的三元子序列

力扣 334:递增的三元子序列 (opens new window)

class Solution {
public:
    bool increasingTriplet(vector<int>& nums) {
        int n = nums.size();
        int first = nums[0], second = INT_MAX;
        for(int i = 1; i < n; i++){
            if(nums[i] > second){
                return true;
            } else if(nums[i] > first){
                second = nums[i];
            } else if(nums[i] < first){
                first = nums[i];
            }
        }
        return false;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

合并区间

力扣 56:合并区间 (opens new window)

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> res;
        sort(intervals.begin(), intervals.end());
        for(int i = 0; i < intervals.size(); i++){
            vector<int> cur = intervals[i];
            if(res.empty() || cur[0] > res.back()[1]){
                res.push_back(cur);
            }
            if(cur[1] > res.back()[1]){
                res.back()[1] = cur[1];
            }
        }
        return res;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

无重叠区间

力扣 435:无重叠区间 (opens new window)

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.empty()){
            return 0;
        }
        int n = intervals.size();
        sort(intervals.begin(), intervals.end(), [](const auto& u, const auto& v){
            return u[1]<v[1];
        });
        int ans = 1;
        int right = intervals[0][1];
        for(int i = 1; i < n; i++){
            if(intervals[i][0] >= right){
                ans++;
                right = intervals[i][1];
            }
        }
        return n-ans;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

划分字母区间

力扣 763:划分字母区间 (opens new window)

class Solution {
public:
    vector<int> partitionLabels(string s) {
        map<char,vector<int>> m;
        for(int i = 0; i < s.length(); i++){
            char cur = s[i];
            if(m.count(cur)){
                m[cur][1] = i;
            } else {
                m[cur] = {i, i};
            }
        }
        vector<vector<int>> vec;
        for(auto v: m){
            vec.push_back(v.second);
        }
        sort(vec.begin(), vec.end());
        vector<vector<int>> merged;
        for(int i = 0; i < vec.size(); i++){
            vector<int> cur = vec[i];
            if(merged.empty() || cur[0] > merged.back()[1]){
                merged.push_back(cur);
            }
            if(cur[1] > merged.back()[1]){
                merged.back()[1] = cur[1];
            }
        }
        vector<int> res;
        for(int i = 0; i < merged.size(); i++){
            res.push_back(merged[i][1]-merged[i][0]+1);
        }
        return res;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

文本左右对齐(68)

Last Updated: 7/2/2024, 4:26:50 PM
妖风过海
刘森