zoukankan      html  css  js  c++  java
  • [算法]死磕递归和动态规划专题算法

    最近在忙着找实习,因而做了大量的笔试算法题,阿里,网易,腾讯,华为,发现各大厂商都喜欢出递归和动态规划题,而且出的特别多,这种题以前一直没有搞懂,总是半懂状态,现在感觉有必要好好整理一下。

    1. 斐波那契数列

    谈到递归问题,我们不妨先从斐波那契数列开始,这个大家应该都不陌生吧,1,1,2,3,5,8......除了第一项和第二项为1外,对于第N项,有F(N) = F(N - 1) + F(N - 2)。

    我们先看一下暴力求解,其时间复杂度为O(2^N):

    public static int f1(int n) {
            if(n < 1){
                return 0;
            }
            if(n == 1 || n == 2){
                return 1;
            }
            return f1(n - 1) + f1(n - 2);
    }

    当然我们可以优化成时间复杂度为O(N),如下:a,b=b,a+b

    public static int f2(int n){
            if(n < 1){
                return 0;
            }
            if(n == 1 || n == 2){
                return 1;
            }
            int pre = 1;//第一个
            int res = 1;//第二个
            int temp = 0;
            for (int i = 3; i <= n; i++) {
                temp = res;
                res += pre;
                pre = temp;
            }
            return res;
    }

    当然这道题还可以进一步优化成时间复杂度O(logN),采用矩阵乘法,这里就不说了,一般O(N)足够了。我们通过这道题总结规律,递归问题,进入一个方法,先写出一个终止条件,然后根据题目,找出递推关系,进行递归。

    同类型的题目有台阶问题和生兔子问题。

    2. 台阶问题

    有n级台阶,一个人每次上一级或者两级,问有多少种走完N级台阶的方法。为了防止溢出,请将结果Mod 1000000007。

    给定一个正整数int N,请返回一个数,代表上楼的方式数。保证N小于等于100000。

    这道题类似于斐波那契数列,跳上N级台阶的情况,要么是从N-2级台阶直接跨2级台阶,要么是从N-1级台阶跨1级台阶,即转移方程是f(N) = f(N - 1) + f(N - 2),状态方程为f(1) = 1,f(2) = 2。

    类比上一道题,得到两种求解方法如下:

    时间复杂度为O(2^N):

    public static int f1(int n) {
            if(n < 1){
                return 0;
            }
            if(n == 1 || n == 2){
                return n;
            }
            return f1(n - 1) + f1(n - 2);
    }

    时间复杂度为O(N):a,b=b,a+b

    public static int f2(int n){
            if(n < 1){
                return 0;
            }
            if(n == 1 || n == 2){
                return n;
            }
            
            int pre = 1;//第一个数
            int res = 2;//第二个数
            int temp = 0;
            for (int i = 3; i <= n; i++) {
                temp = res;
                res += pre;
                pre = temp;
            }
            
            return res;
    }

    变态跳台阶

    一次可以跳上1级台阶,也可以跳上2级……也可以跳上n级。求跳上一个n级的台阶总共有多少种跳法。

    public class Solution {
        public int JumpFloorII(int target) {
            if(target == 1){
                return 1;
            }
            return JumpFloorII(target - 1) * 2;
        }
    }

    3. 生兔子问题

    假设成熟的兔子每年生1只兔子,并且永远不会死,第一年有1只成熟的兔子,从第二年开始,开始生兔子,每只小兔子3年之后成熟又可以继续生。给出整数N,求出N年后兔子的数量。

    时间复杂度为O(2^N):

    public static int f1(int n) {
            if(n < 1){
                return 0;
            }
            if(n == 1 || n == 2 || n == 3){
                return n;
            }
            return f1(n - 1) + f1(n - 3);
    }

    时间复杂度为O(N):a,b,c=b,c,a+c

    public static int f2(int n){
            if(n < 1){
                return 0;
            }
            if(n == 1 || n == 2 || n == 3){
                return n;
            }
            
            int prepre = 1;//第一个数
            int pre = 2;//第二个数
            int res = 3;//第三个数 
            int temp1 = 0;
            int temp2 = 0;
            for (int i = 4; i <= n; i++) {
                temp1 = pre;
                temp2 = res;
                res += prepre;
                prepre = temp1;
                pre = temp2;
            }
            return res;
    }

    4. 找零钱问题

    有数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。

    给定数组arr及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。

    测试样例:
    [1,2,4],3
    返回:2

    所有的动态规划题本质都是优化后的暴力求解,一般动态规划题是构造一个dp矩阵,第一行和第一列赋初值,然后根据递推关系,由一个个子问题求出整个问题,即把剩余位置的值填满,说白了就是空间换时间。因为暴力求解会有大量的重复计算,动态规划可以有效地避免重复计算。

    比如找零钱问题,我们可以看成0个arr[0],让剩余的组成aim,1个arr[0],让剩余的组成aim - 1 * arr[0],2个arr[0],让剩余的组成aim - 2 * arr[0],以此类推。为什么会产生重复计算,是因为比方我用了1个10元,0个5元,然后让剩下的组成aim - 10和我用0个10元,2个5元,让剩下的组成aim - 10本质是一样的。

    暴力求解法:

    public static int process1(int[] arr, int index, int aim){
            int res = 0;
            if(index == arr.length){
                res = aim == 0 ? 1 : 0;
            }else{
                for (int i = 0; i * arr[index] <= aim; i++) {
                    res += process1(arr, index + 1, aim - i * arr[index]);
                }
            }
            return res;
    }

    动态规划法:

    首先思考如何设计dp矩阵,这里我们把行设置成arr下标,代表的就是利用[0...i]区间内组成aim的值的方法数,列代表的是aim值,从0取到aim。

    我们先给第一列赋值,因为aim是0,所以只有一种组合方式,就是每个价值的纸币都取0个,所以第一列全取1。

    接下来看第一行,就是求arr[0]能够凑成的钱的方案,只要是其倍数的都能凑成,所以相应位置应该填写1。

    最后我们确定其他位置,完全不用arr[i]货币,只用剩下的,则方法数dp[i - 1][j].

    用1个arr[i],方法数是dp[i - 1][j - 1 * arr[i]]。

    用2个arr[i],方法数是dp[i - 1][j - 2 * arr[i]]。

    以此类推,是上面那一行,经过化简,可以简化成dp[i][j] = dp[i - 1][j] + dp[i][j - arr[i]]。这就是状态转移方程。

    public static int process2(int[] arr, int aim){
            int[][] dp = new int[arr.length][aim + 1];
            
            //先赋值第一列,全是1
            for (int i = 0; i < dp.length; i++) {
                dp[i][0] = 1;
            }
            //再赋值第一行
            for (int i = 1; i * arr[0] <= aim; i++) {
                dp[0][ i * arr[0]] = 1;
            }
            
            //给所有元素赋值
            for (int i = 1; i < dp.length; i++) {
                for (int j = 1; j < dp[i].length; j++) {
                    dp[i][j] = dp[i - 1][j];
                    dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
                }
            }
            
            return dp[arr.length - 1][aim];
    }

    5. 矩阵最小路径

    有一个矩阵map,它每个格子有一个权值。从左上角的格子开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。

    给定一个矩阵map及它的行数n和列数m,请返回最小路径和。保证行列数均小于等于100.

    测试样例:
    [[1,2,3],[1,1,1]],2,3
    返回:4
    public int minPathSum(int[][] m){
            int row = m.length;
            int col = m[0].length;
            
            int[][] dp = new int[row][col];
            dp[0][0] = m[0][0];
            
            //给行初始化
            for (int i = 1; i < row; i++) {
                dp[i][0] = dp[i - 1][0] + m[i][0];
            }
            
            //给列初始化
            for (int i = 1; i < col; i++) {
                dp[0][i] = dp[0][i - 1] + m[0][i];
            }
            
            //给剩余元素初始化
            for (int i = 1; i < row; i++) {
                for (int j = 1; j < col; j++) {
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];
                }
            }
            return dp[row - 1][col - 1];
    }

    6. 最长递增子序列

    这是一个经典的LIS(即最长上升子序列)问题,请设计一个尽量优的解法求出序列的最长上升子序列的长度。

    给定一个序列A及它的长度n(长度小于等于500),请返回LIS的长度。

    测试样例:
    [1,4,2,5,3],5
    返回:3
    public static int[] getLIS(int[] A) {
            // write code here
            List<Integer> list = new ArrayList<>();
            
            int[] dp = new int[A.length];
            dp[0] = 1;
            
            for (int i = 1; i < dp.length; i++) {
                dp[i] = 1;
                for(int j = 0; j < i; j++){
                    if(A[j] < A[i]){
                        dp[i] = Math.max(dp[i], dp[j] + 1);
                    }
                }
            }
            
            int maxIndex = dp.length - 1;
            for (int i = dp.length - 2; i >= 0; i--) {
                if(dp[i] > dp[maxIndex]){
                    maxIndex = i;    
                }
            }
            
            list.add(A[maxIndex]);
            for (int i = maxIndex - 1; i >= 0; i--) {
                if(A[maxIndex] > A[i] && dp[maxIndex] == dp[i] + 1){
                    list.add(A[i]);
                    maxIndex = i;
                }
            }
            
            int[] nums = new int[list.size()];
            for(int i = 0; i < nums.length; i++){
                nums[nums.length - 1 - i] = list.get(i);
            }
            return nums;
        }

    7. 最长公共子序列

    给定两个字符串A和B,返回两个字符串的最长公共子序列的长度。例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最长公共子序列。

    给定两个字符串AB,同时给定两个串的长度nm,请返回最长公共子序列的长度。保证两串长度均小于等于300。

    测试样例:
    "1A2C3D4B56",10,"B1D23CA45B6A",12
    返回:6
    public static String getLCS(String A, String B) {
            int dp[][] = new int[A.length()][B.length()];
            
            dp[0][0] = A.charAt(0) == B.charAt(0) ? 1 : 0;
            
            for (int i = 1; i < B.length(); i++) {
                dp[0][i] = Math.max(dp[0][i - 1], A.charAt(0) == B.charAt(i) ? 1 : 0);
            }
            
            for (int i = 1; i < A.length(); i++) {
                dp[i][0] = Math.max(dp[i - 1][0], A.charAt(i) == B.charAt(0) ? 1 : 0);
            }
            
            for (int i = 1; i < A.length(); i++) {
                for (int j = 1; j < B.length(); j++) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                    if(A.charAt(i) == B.charAt(j)){
                        dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
                    }
                }
            }
            
            int num = dp[A.length() - 1][B.length() - 1];//最长公共子序列的长度
            
            System.out.println(num);
            StringBuilder sb = new StringBuilder();
            
            int m = A.length() - 1;
            int n = B.length() - 1;
            while(num > 0){
                if(m > 0 && dp[m - 1][n] == dp[m][n]){
                    m--;
                }else if(n > 0 && dp[m][n - 1] == dp[m][n]){
                    n--;
                }else{
                    sb.insert(0, A.charAt(m));//因为此时A.charAt(m) == B.charAt(n),所以选哪一个均可
                    m--;
                    n--;
                    num--;
                }
            }
            
            return sb.toString();
    }

    8. 最长公共子串

    注意和上一道题进行区分,公共子串必须连续。

    dp[i][j]表示以两个字符串分别以第i和第j个字符结尾所能达到的公共子串的长度,

    状态转移方程为

    if(str[i-1]=str[j-1])

    dp[i][j]=dp[i-1][j-1]+1;

    if(str[i-1]!=str[j-1])

    dp[i][j]=0;

    public static String getLCS(String A, String B) {
            int dp[][] = new int[A.length()][B.length()];
            
            dp[0][0] = A.charAt(0) == B.charAt(0) ? 1 : 0;
            
            for (int i = 1; i < A.length(); i++) {
                if(A.charAt(i) == B.charAt(0)){
                    dp[i][0] = 1;
                }
            }
            
            for (int i = 1; i < B.length(); i++) {
                if(B.charAt(i) == A.charAt(0)){
                    dp[0][i] = 1;
                }
            }
            
            for (int i = 1; i < A.length(); i++) {
                for (int j = 1; j < B.length(); j++) {
                    if(A.charAt(i) == B.charAt(j)){
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    }
                }
            }
            
            
            //找出最大值,即为最长公共子串
            int max = 0;
            int index = 0;//记录A字符串最长公共子字符串最后一个位置
            for (int i = 0; i < A.length(); i++) {
                for (int j = 0; j < B.length(); j++) {
                    if(dp[i][j] > max){
                        max = dp[i][j];
                        index = i;
                    }
                }
            }
            
            return A.substring(index - max + 1, index + 1);
    }

    9. 最长回文子字符串

    回文字符串的子串也是回文,比如P[i,j](表示以i开始以j结束的子串)是回文字符串,
    那么P[i+1,j-1]也是回文字符串。这样最长回文子串就能分解成一系列子问题了。
    这样需要额外的空间O(N^2),算法复杂度也是O(N^2)。 首先定义状态方程和转移方程:
    P[i,j]=0表示子串[i,j]不是回文串。P[i,j]=1表示子串[i,j]是回文串。
    P[i,i]=1
    P[i,j]{=P[i+1,j-1],if(s[i]==s[j])
    =0 ,if(s[i]!=s[j])}

    public static String longestPalindrome(String s){
            if(s == null || s.length() == 1){
                return s;
            }
            int len = s.length();
            //dp[i][j]=1 表示子串i-j为回文字符串
            int[][] dp = new int[len][len];
            
            int start = 0;
            int maxlen = 0;
            
            for (int i = 0; i < len; i++) {
                dp[i][i] = 1;
                if(i < len - 1 && s.charAt(i) == s.charAt(i + 1)){
                    dp[i][i + 1] = 1;
                    start = i;
                    maxlen = 2;
                }
            }
            
            //m代表最长子串长度
            for (int m = 3; m <= len; m++) {
                for (int i = 0; i < len - m + 1; i++) {
                    int j = i + m - 1;
                    if(dp[i + 1][j - 1] == 1 && s.charAt(i) == s.charAt(j)){
                        dp[i][j] = 1;
                        start = i;
                        maxlen = m;
                    }
                }
            }
            
            
            return s.substring(start, start + maxlen);
    }
    private static int maxLen = 0;
    
    private static String sub = "";
    
    public static String longestPalindrome(String s) {
            if(s.length() <= 1)
                return s;
    
            for(int i = 0;i < s.length()-1;i++){
    
                findLongestPalindrome(s,i,i);//单核回文
    
                findLongestPalindrome(s,i,i+1);//双核回文
            }
            return sub;
        }
        public static  void findLongestPalindrome(String s,int low,int high){
            while (low >= 0 && high <= s.length()-1){
                if(s.charAt(low) == s.charAt(high)){
                    if(high - low + 1 > maxLen){
                        maxLen = high - low + 1;
                        sub = s.substring(low , high+1);
                    }
                    low --;//向两边扩散找当前字符为中心的最大回文子串
                    high ++;
                }
                else
                    break;
            }
        }

    10. 0-1背包问题(完全背包、多重背包)

    一个背包有一定的承重cap,有N件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大。

    给定物品的重量w价值v及物品数n和承重cap。请返回最大总价值。

    测试样例:
    [1,2,3],[1,2,3],3,6
    返回:6

    第一,包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);

    第二,还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }

    其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i)但价值增加了v(i);

    由此可以得出递推关系式:

    1) j<w(i)      V(i,j)=V(i-1,j)

    2) j>=w(i)     V(i,j)=max{ V(i-1,j)V(i-1,j-w(i))+v(i) 

    填表,首先初始化边界条件,V(0,j)=V(i,0)=0;

     

     然后一行一行的填表,示例:

     

    public static int[] maxValue(int[] w, int[] v, int cap) {
            // write code here
            int[][] dp = new int[w.length + 1][cap + 1];
    
            // 第一行和第一列不用赋初值,因为都是0
            for (int i = 1; i <= w.length; i++) {
                for (int j = 1; j <= cap; j++) {
                    dp[i][j] = dp[i - 1][j];
                    if (j >= w[i - 1]) {
                        dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
                    }
                }
            }
    
            int maxValue = dp[w.length][cap];// 获取的最大价值
    
            /**
             * 到这一步,可以确定的是可能获得的最大价值,但是我们并不清楚具体选择哪几样物品能获得最大价值。
             * 
             * 另起一个 x[] 数组,x[i]=0表示不拿,x[i]=1表示拿。
             * 
             * dp[n][c]为最优值,如果dp[n][c]=dp[n-1][c] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则
             * x[n]=1。当x[n]=0时,由dp[n-1][c]继续构造最优解;当x[n]=1时,则由dp[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。
             */
            int[] x = new int[w.length + 1];//不看0位,为了和矩阵对应,x[0]不用看
            
            for (int i = w.length; i > 1; i--) {
                if(dp[i][cap] == dp[i - 1][cap]){
                    x[i] = 0;
                }else{
                    x[i] = 1;
                    cap -= w[i - 1];
                }
            }
            
            x[1] = dp[1][cap] > 0 ? 1 : 0;
    
            return x;
    }

     这个其实可以优化的,优化成:

    https://blog.csdn.net/sun897949163/article/details/49559679

    https://blog.csdn.net/tinyguyyy/article/details/51203935

    01背包问题空间压缩版:

    package com.darrenchan.dp;
    
    import java.util.Arrays;
    
    /**
     * 空间压缩版01背包问题
     * 
     * @author Think
     *
     */
    public class Backpack01 {
    
        public static void main(String[] args) {
            System.out.println(maxValue(new int[] { 15, 10, 12, 8 }, new int[] { 12, 8, 9, 5 }, 30));
        }
    
        public static int maxValue(int[] w, int[] v, int cap) {
            int[] dp = new int[cap + 1];
    
            for (int i = 0; i < w.length; i++) {
                for (int j = cap; j >= w[i]; j--) {// 倒序遍历
                    dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
                }
            }
    
            int maxValue = dp[cap];// 获取的最大价值
            System.out.println(Arrays.toString(dp));
    
            return maxValue;
        }
    }

    完全背包问题空间压缩版:

    package com.darrenchan.dp;
    
    import java.util.Arrays;
    
    /**
     * 空间压缩版完全背包问题
     * 
     * @author Think
     *
     */
    public class BackpackComplete {
    
        public static void main(String[] args) {
            System.out.println(maxValue(new int[] { 15, 10, 12, 8 }, new int[] { 12, 8, 9, 5 }, 30));
        }
    
        public static int maxValue(int[] w, int[] v, int cap) {
            int[] dp = new int[cap + 1];
    
            for (int i = 0; i < w.length; i++) {
                for (int j = w[i]; j <= cap; j++) {// 正序遍历
                    dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
                }
            }
    
            int maxValue = dp[cap];// 获取的最大价值
            System.out.println(Arrays.toString(dp));
    
            return maxValue;
        }
    }

    多重背包问题空间压缩版:

    package com.darrenchan.dp;
    
    import java.util.Arrays;
    
    /**
     * 空间压缩版多重背包问题
     * 
     * n是每一个物品的个数
     * @author Think
     *
     */
    public class BackpackMultiple {
    
        public static void main(String[] args) {
            System.out.println(maxValue(new int[] { 15, 10, 12, 8 }, new int[] { 12, 8, 9, 5 },new int[]{1,1,1,1}, 30));
        }
    
        public static int maxValue(int[] w, int[] v,int[] n, int cap) {
            int[] dp = new int[cap + 1];
    
            for (int i = 0; i < w.length; i++) {
                for (int k = 0; k <= n[i]; k++) {
                    for (int j = cap; j >= k * w[i]; j--) {// 倒序遍历
                        dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]);
                    }
                }
            }
    
            int maxValue = dp[cap];// 获取的最大价值
            System.out.println(Arrays.toString(dp));
    
            return maxValue;
        }
    }

    11. 最长整除子序列

    给出一个由无重复的正整数组成的集合, 找出其中最大的整除子集, 子集中任意一对 (Si, Sj) 都要满足: Si % Sj = 0 或 Sj % Si = 0。

    如果有多个目标子集,返回其中任何一个均可。(LeetCode 368)类比最长递增子序列。

    示例 1:

    集合: [1,2,3]
    
    结果: [1,2] (当然, [1,3] 也正确)
    

     示例 2:

    集合: [1,2,4,8]
    
    结果: [1,2,4,8]
    public List<Integer> largestDivisibleSubset(int[] nums) {
             // write your code here
            List<Integer> list = new ArrayList<Integer>();
            if(nums == null || nums.length == 0){
                return list;
            }
            Arrays.sort(nums);
            int[] dp = new int[nums.length];
            dp[0] = 1;
    
            for (int i = 1; i < nums.length; i++) {
                for (int j = i - 1; j >= 0; j--) {
                    if (nums[i] % nums[j] == 0) {
                        dp[i] = Math.max(dp[i], dp[j] + 1);
                    }
                }
            }
    
            int maxIndex = nums.length - 1;
            for (int i = nums.length - 1; i > 0; i--) {
                maxIndex = dp[i] > dp[maxIndex] ? i : maxIndex;
            }
    
            list.add(nums[maxIndex]);//最大的那个值
            for (int i = maxIndex - 1; i >= 0; i--) {
                if (nums[maxIndex] % nums[i] == 0 && dp[maxIndex] == dp[i] + 1) {
                    list.add(nums[i]);
                    maxIndex = i;
                }
            }
    
            return list;
        }

    12. 寻找和为定值的多个数

    题目:输入两个整数n和sum,从数列1,2,3.......n 中随意取几个数,使其和等于sum,要求将其中所有的可能组合列出来。

    思路:

    我们设置flag背包,用来标注对应的n+1是否被选中,1表示被选中,0则表示未选中,每当满足m==n时,则输出一组解。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Scanner;
    
    public class SearchSomeSureValue {
        static int length;
        static void findCombination(int n,int m,int flagI[]){
            if (n<1||m<1) {
                return;
            }
            if (n>m) {
                n=m;
            }
            if (n==m) {
                flagI[n-1]=1;
                for (int i = 0; i < length; i++) {
                    if (flagI[i]==1) {
                        System.out.print(i+1+" ");
                    }
                    
                }
                System.out.println();
                flagI[n-1]=0;
            }
            
            flagI[n-1]=1;
            findCombination(n-1, m-n, flagI);
            
            flagI[n-1]=0;        
            findCombination(n-1, m, flagI);
        }
        public static void main(String[] args) {
            int n,m;
            Scanner s=new Scanner(System.in);
            n=s.nextInt();
            m=s.nextInt();
            length=n;
            int[] flag=new int[n];
            findCombination(n, m, flag);
        }
    }
  • 相关阅读:
    js 解压缩编码列表
    js 拥有最多糖果
    js 所有奇数长度子数组的和
    js和jquery中有关透明度操作的问题
    python pandas初体验
    NumPy基础及取值操作
    python之pandas简介
    Study Plan The FortyThird day
    Study Plan The ThirtyNine Day
    Study Plan The FortySeventh Day
  • 原文地址:https://www.cnblogs.com/DarrenChan/p/8734203.html
Copyright © 2011-2022 走看看