动态规划专题(II)

1.钢条切割问题


长度i             1                       2                    3                  4               5                 6                 7                  8               9           10
价格pi      1                      5                   8                  9              10               17               17                20             24          30

给定一段长度为n的钢条和一个价格表pi,求切割方案使得销售收益最大。注意如果长度为n的钢条的价格pn足够大,最优解就可能是完全不需要切割。

这个问题我们就根据上一篇博客《动态规划专题(I) 》中提到的方法进行思考。首先很容易猜测这是一个一维dp问题,假设最终的结果就是dp[len],那么dp[i]的意思就是到长度i的位置,最大收益。进一步我们再来推导递推公式,dp[i+1]位置时的最大收益可能有很多中情况,比如从i的位置切割一次,这时dp[i+1] = dp[i]+p1,也可以从前面的任何一个位置进行切割,这时dp[i+1] = dp[k]+p(i+1-k),遍历完所有的切割情况后,取最大值就是dp[i+1]的最大值。

前面为了利用我们上一篇博客中提到的方法,分析过程略显复杂,现在给出Java代码,大家看后应该就很清楚了。

public class Solution{
	public static int cut_rod(int[] p,int n){
		int[] dp = new int[n+1];
		for(int i = 1; i <= n; i++){
			int max = Integer.MIN_VALUE;
			for(int j = i-1; j>0; j--){
				dp[i] = Math.max(max,p[i-j]+dp[j]);
			}
		}
		return dp[n];
	}
	public static void main(String[] args){
		int[] p = {1,5,8,9,10,17,17,20,24,30};
		System.out.println(cut_rod(p,10));
	}
}
最终结果返回30.

2.矩阵链乘法问题

这也是《算法导论》中探讨的一个问题,这里我们继续用自己的思路来解决这个问题。

矩阵链乘法问题可描述如下:给定n个矩阵的链<A1,A2,.....,An>,矩阵Ai的规模为pi-1 X pi (1<=i<=n),求完全括号化方案。

因为矩阵链乘法会涉及到AiAi+1...Aj的相乘,我们很自然的想到这是一个二维动态规划,用dp[i][j]表示AiAi+1...Aj的标量乘法次数的最小值。那么最终结果就是dp[1][n],接下来就需要找递推关系了,怎么寻找递推关系呢?平时接触比较多的简单动归问题一般是根据dp[i]来构造dp[i+1],也就是根据前一个来推到下一个,这其实是最简单的一种情况,它的本质实际上是从已解决的问题来表示目前的待解决的问题,这一步需要根据具体问题具体分析。就矩阵链乘法问题,我们想到可以把一个长的链分割成两个部分,整个长链要想是最优的,那么分割的两个短链也一定是最优的。这可以用反证法来证明,假如存在一个短链不是最优的,那么我们用它的最优解来替换它,那么长链会更优。这与我们假设的长链是最优的矛盾。因此,我们得到下面的递推公式:

                  0       if i == j

dp[i][j] = {

                  min{dp[i][k]+dp[k+1][j]+pi-1pkpj}   if i < j


上面求得是值,如何构造解呢?如何把括号方案打印出来?在求dp[i][j]的时候我们保存它的分割位置,假如用s[i][j]来保存,比如dp[i][j]的分割位置在k,那么s[i][j] = k,最终在构造解的时候,只要递归的进行就可以了。也就是说找到k之后,继续对s[i][k]和s[k+1][j]进行递归就可以了。

下面就是实际代码实现:

public class Test{
	public static void matrix_chain_order(int[] p){
		int n = p.length-2;
		int[][] dp = new int[n+1][n+1];
		int[][] s = new int[n+1][n+1];
		for(int j = 2;j<=n;j++){
			for(int i = j-1;i>0;i--){
				dp[i][j] = Integer.MAX_VALUE;
				for(int k = j-1;k>=i;k--){//[i,j] k
					int val = dp[i][k]+dp[k+1][j]+p[i]*p[k+1]*p[j+1];
					if(val < dp[i][j]){
						s[i][j] = k;
						dp[i][j] = val;
					}
				}
			}
		}
		print_optimal_parens(s,1,n);
		System.out.println();
	}
	public static void print_optimal_parens(int[][] s,int i,int j){
		if(i == j) System.out.print("A"+i);
		else{
			System.out.print("(");
			print_optimal_parens(s,i,s[i][j]);
			print_optimal_parens(s,s[i][j]+1,j);
			System.out.print(")");
		}
	}

	public static void main(String[] args){
		int[] p = {0,30,35,15,5,10,20,25}; //A1=30X35  A2=35X15 ....
		matrix_chain_order(p);
	}
}
上面的print_optimal_parens方法的实现也非常精炼,else部分的直接翻译就是对一条矩阵链整体加括号,并对其两个子链分别加括号。

3.最长公共子序列(LCS)问题

给定两个序列X和Y,如果Z既是X的子序列,又是Y的子序列,我们称它是X和Y的公共子序列。注意区分公共子串。

例如 X=<A,B,C,B,D,A,B>,Y = <B,D,C,A,B,A>,那么序列<B,C,A>就是X和Y的公共子序列。

下面我们继续用上一篇文章中提到的思路来解决这个问题,首先想到的还是这是一个二维动态规划问题,假设最终结果为dp[len1][len2],那么dp[i][j]就是X中前i个元素与Y中前j个元素的最长公共子序列长度。接下来找递推关系,看一下dp[i+1][j],如果X[i+1] == Y[j],那么显然dp[i+1][j] = dp[i][j-1]+1;如果X[i+1] != Y[j],那么dp[i+1][j] = max{dp[i][j],dp[i+1][j-1]。接下来看一下dp[i][j+1],同样的道理,如果Y[j+1] == X[i],那么dp[i][j+1] = dp[i][j-1]+1,否则dp[i][j+1] = max{dp[i-1][j+1],dp[i][j]};这样就得到了递推公式。

《算法导论》中的推导与上面的方式不同,它是这样推导的:

令X = <x1,x2,....,xm> 和 Yn = <y1,y2,....,yn>为两个序列,Z = <z1,z2,...,zk>为X和Y的任意LCS。

1.如果xm = yn,则zk = xm = yn 且Zk-1是Xm-1和Yn-1的一个LCS

2.如果xm != yn,那么zk != xm 意味着Z是Xm-1和Y的一个LCS

3.如果 xm != yn,那么zk != yn 意味着Z是X和Yn-1的一个LCS。

这样得到的递推公式如下:

             0    若 i = 0 或 j = 0

c[i,j] = { c[i-1,j-1] + 1  若i,j > 0且xi = yi

             max{c[i,j-1],c[i-1,j]}   若i,j > 0,且xi != yj
上面两种思路本质上都是一样的,第一种更容易想到,第二种更便于归纳。

如何保存结果呢?上一道题中,我们记录每一个最优切割的位置,找到这个位置后,又可以对被切割的部分执行同样的操作,进而构造出解。也就是说要想按图索骥构造出解,我们必须知道每个结果是由哪个结果演化来的。这道题也是同样的道理,必须找到目前的结果的上一个结果。这里,每个c[i,j]可能由3个途径转换而来,我们只要记录这些信息就可以了。

最终Java代码如下:

public class Test{
	private static final int LEFT = 0;
	private static final int UP = 1;
	private static final int LU = 2;
	public static void LCS(char[] X,char[] Y){
		int m = X.length-1, n = Y.length-1;
		int[][] dp = new int[m+2][n+2];
		int[][] s = new int[m+2][n+2];
		for(int i = 1;i <= m;i++){
			for(int j = 1;j <= n;j++){
				dp[i][j] = Integer.MIN_VALUE;
				if(X[i] == Y[j]){
					dp[i][j] = dp[i-1][j-1]+1;
					s[i][j] = LU;
				}else{
					if(dp[i-1][j] > dp[i][j]){
						dp[i][j] = dp[i-1][j];
						s[i][j] = UP;
					}
					if(dp[i][j-1] > dp[i][j]){
						dp[i][j] = dp[i][j-1];
						s[i][j] = LEFT;
					}
				}
			}
		}
		print_LCS(s,X.length-1,Y.length-1,X);
		System.out.println();
	}
	
	public static void print_LCS(int[][] s,int i,int j,char[] X){
		if(i == 0||j == 0) return;
		if(s[i][j] == LU) {
			print_LCS(s,i-1,j-1,X);
			System.out.print(X[i]);
		}else if(s[i][j] == LEFT) print_LCS(s,i,j-1,X);
		else print_LCS(s,i-1,j,X);
	}

	public static void main(String[] args){
		char[] X = {'o','A','B','C','B','D','A','B'};
		char[] Y = {'o','B','D','C','A','B','A'};
		LCS(X,Y);
	}
}

4.最长公共子串

区别一下子串与子序列,举个例子就很清楚了:

X = <A,B,C,D,E> Y = <A,D,E>

那么X中的DE与Y中的DE是一个公共子串,X中的ADE与Y中的ADE是一个公共子序列。

公共子串比公共子序列问题简单,还是一个二维dp问题,设dp[i][j]表示Xi与Yj的最长子串,且X[i] = Y[j]。那么要求dp[i+i][j+1],显然如果X[i+1] != Y[j+1],那么dp[i+1][j+1] = 0;如果X[i+1] = Y[j+1],那么dp[i+1][j+1] = dp[i][j] + 1;

关于结果保存,我们可以在计算上面的dp[i][j]的过程中顺便记录最大值的i,j以及长度最终就可以直接得出结果了。

Java代码如下:

public class Test{
	public static void LCS(String X,String Y){
		int m = X.length(), n = Y.length();
		X = "0"+X;Y = "0"+Y;
		char[] XX = X.toCharArray(),YY = Y.toCharArray();
		int[][] dp = new int[m+1][n+1];
		int index = 0,len = 0;
		for(int i = 1;i <= m;i++){
			for(int j = 1;j <= n;j++){
				if(XX[i] == YY[j]){
					dp[i][j] = dp[i-1][j-1]+1;
					if(dp[i][j] > len){
						len = dp[i][j];
						index = i;
					}
				}//else dp[i][j] = 0;
			}
		}
		System.out.print("maxLen= "+len+" : ");
		System.out.println(X.substring(index-len+1,index+1));
	}
	public static void main(String[] args){
		String X = "ABCBDAB";
		String Y = "BDCABA";
		LCS(X,Y);
	}
}


接下来是leetcode上的部分动态规划题目

5.Regular Expression Matching

Implement regular expression matching with support for'.' and'*'.


'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true


这道题目我是用回溯法做的,来看一下思路,p中的字符只有三种情况: a-z 或者 . 或者 * ,如果是a-z,并且不匹配并且后面不是*,那就直接返回false即可,如果后面有*,就按匹配0次处理。如果是a-z并且匹配,这与.的情况是一样的,这时根据其后是否为*进行不同的操作。首先如果其后不是*,加入现在是s[i][j],那么下一步就进行s[i+1][j+1]的判断,接着就会重复整个过程;如果其后是*,那么分匹配0次与多次进行递归处理。

回溯版代码如下:

public class Solution {
    public boolean isMatch(String s, String p) {
        if(s==null && p == null) return true;
        if(s == null || p == null) return false;
        
        int lp = p.length();
        if(lp == 0) return s.length() == 0;
        
        char ch = p.charAt(0);
        if(ch == '.'||(ch != '*')){
            if(!s.isEmpty() && (ch == '.'|| ch == s.charAt(0))){
                if(0 == lp-1) return isMatch(s.substring(1,s.length()),"");
                if(p.charAt(1) == '*'){
                    if(s.isEmpty()) return isMatch("",p.substring(2,lp));
                    return isMatch(s,p.substring(2,lp))||isMatch(s.substring(1,s.length()),p);
                }else return isMatch(s.substring(1,s.length()),p.substring(1,lp));
            }else{ //not match
                if(0 == lp-1) return false;
                if(p.charAt(1) == '*') return isMatch(s,p.substring(2,lp));
                else return false;
            }
        }
        return true;
    }
}


现在,用动态规划来重新做这道题,分析的过程与上面是一样的。设dp[i][j]表示s[1...i]与p[1...j]是否匹配,那么如果s[i] == p[j] or p[j] = '.',dp[i][j] = dp[i-1][j-1];
如果p[j] == '*',如果p[j-1] != s[i],dp[i][j] = dp[i][j-2](匹配0次);如果p[j-1] == '.' or p[j-1] == s[i],那么dp[i][j] = dp[i][j-2] | dp[i-1][j-2] | dp[i-1][j];分别对应匹配0次,1次,多次的情况。还要特别注意对dp[0][j]的处理,因为其他的可能会依赖它。代码如下:

public class Solution {
    public boolean isMatch(String s, String p) {
        int lp = p.length(),ls = s.length();
        if(lp == 0) return s.length() == 0;
        boolean[][] dp = new boolean[ls+1][lp+1];
        dp[0][0] = true;
        
        for(int i =0;i <= ls;i++){
            for(int j = 1;j <= lp; j++){
                if(i!=0 &&(s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.')){
                    dp[i][j] = dp[i-1][j-1]; //match 1 times
                }
                if(p.charAt(j-1) == '*'){
                    dp[i][j] = dp[i][j-2]; //match 0 times
                    if(i == 0) continue;
                    if(j-2>=0 && p.charAt(j-2) == s.charAt(i-1) || p.charAt(j-2) == '.'){
                        dp[i][j] |= dp[i][j-1]|dp[i-1][j]; //match 0,1,more than 1 
                    }
                }
            }
        }
        return dp[ls][lp];
    }
}


6.Longest Valid Parentheses

Given a string containing just the characters '(' and ')', find the length of the longest valid (well-formed) parentheses substring.

For "(()", the longest valid parentheses substring is"()", which has length = 2.

Another example is ")()())", where the longest valid parentheses substring is"()()", which has length = 4.

这道题当然也可以不用动态规划做,比较容易想到的方法是通过栈来保存左括号,遇到右括号时进行判断,如果对应的栈不为空,说明是匹配的,计数增加,如果如果遇到右括号但是栈为空,说明没有匹配项,那么就重新计数。可以用栈来实现代码如下:

public class Solution {
    public int longestValidParentheses(String s) {
        if(s == null || s.isEmpty() || s.length() == 0) return 0;
        LinkedList<Integer> stack = new LinkedList<Integer>();
        int max = 0, len = s.length(),l=0;
        
        char ch;
        for(int i = 0;i<len;i++){
            ch = s.charAt(i);
            switch(ch){
            case '(':
                stack.addLast(i);
                break;
            case ')':
                if(stack.size() != 0){ //stack不为空,
                    stack.removeLast();
                    if(stack.size() == 0) max = Math.max(max,i-l+1);
                    else max = Math.max(max,i-stack.getLast());
                }else l=i+1;
                break;
            }
        }
        return max;
    }
}

但是这里我们用动态规划来做。

这里很容易认为这是一个二维动态规划,但实际上用一维的会比较容易,所以不要限制在一个思维中,需要多尝试。

假设字符串为S,dp[i]表示以i结尾的(包括它)最长有效括号长度。那么来找递推关系,在dp[i-1]的基础上,S[i]有两种可能,一个是'(',一个是')'.

如果是'(',很明显以其结尾的最长有效括号长度为0;如果是')',那么需要判断前面是否有'('与其对应,我们用parens[i]来表示i之前的未配对的'('的个数。那么如果parens[i] = 0,说明当前的')'没有与之配对的,那么dp[i] = 0;如果parens[i]>0,说明有与之配对的,那么dp[i]首先等于dp[i-1]+2,这种情况比如()()或者()(()),然后还需要继续加上与当前配对的'('之前的最长有效括号长度,比如()(())这种情况。精简到一句话就是,如果)有与之匹配的(,那么二者之间的串一定是有效括号,二者前面还有可能存在有效括号需要加上

最终代码如下:

public class Solution {
    public int longestValidParentheses(String s) {
        if(s == null || s.isEmpty() || s.length() == 0) return 0;
        char[] ss = s.toCharArray();
        int len = ss.length, max = 0;
		int[] dp = new int[len];
		int[] parens = new int[len];
		for(int i = 0;i < len;i++){
			if(i == 0 && ss[0] == '(') {parens[0] = 1;continue;}
			if(ss[i] == '(') parens[i] = parens[i-1]+1;
			else{
				if(i>0 && parens[i-1]>0){
					dp[i] = dp[i-1]+2;
					if(i-dp[i]>=0)dp[i] += dp[i-dp[i]];
					parens[i] = parens[i-1]-1;
					max = Math.max(max,dp[i]);
				}
			}
		}
		return max;
    }
}

7.Wildcard Matching

Implement wildcard pattern matching with support for'?' and'*'.

'?' Matches any single character.
'*' Matches any sequence of characters (including the empty sequence).

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "*") → true
isMatch("aa", "a*") → true
isMatch("ab", "?*") → true
isMatch("aab", "c*a*b") → false

这道题与前面的Regular Expression Matching比较相似。用递归写的话会超时,只能模拟递归的操作。思路如下如果当前字符是*,那么它有可能匹配任意字符序列,比如aabb与a*b*b, 这里的*可以匹配空字符序列,也可以匹配a,或者ab,或者abb,当它选择匹配的字符序列之后,其后的字符b就从它匹配序列的下一个开始继续匹配,如果最终全部匹配了,那么返回ture;如果遇到了不匹配的说明上一次*的匹配选择不合理,现在就应该重新选择,也就是增加*的匹配长度,然后重复前面的过程。因此,需要记录*匹配的位置,以使得我们可以在不匹配时返回重新从下一个位置继续。这里面其实比较容易产生困惑的地方并不在于我们上面的分析的内容,而在于如果之前有一个*,我们记录了它的位置,并从p的下一个位置继续匹配,那么如果再次遇到了*呢?是不是仍然需要保存它的位置呢?还是直接覆盖前一个*的位置?答案是直接覆盖前面*的记录即可。

举个例子:

s = aab, p = *a*b;

假如第一个*匹配空串,那么p[2] = s[1] = a匹配,然后碰到了p[3] = '*';到这里说明,p中当前的*与上一个*之间的内容与s中相应的这段内容匹配。那么只要p中当前*(包括)之后的内容仍然与s中当前位置(包括)之后的串匹配,那么二者就是匹配的。那么如果p中当前*(包括)之后的内容仍然与s中当前位置(包括)之后的串不匹配,那么整个串也是不匹配的,这个可以用反证法比较容易的证明。(不知道有没有讲清楚?)

代码如下:

public class Solution {
    public boolean isMatch(String s, String p) {
        int lenp = p.length(),lens = s.length();
        int ss = -1,pp = -1;
        int i = 0,j = 0;
        while(i < lens){
            if(j == lenp){//false,
                if(pp == -1) return false;
                j = pp+1; i = ss++;
            }else if(p.charAt(j) == '?' || s.charAt(i) == p.charAt(j)){//相同
                j++;i++;
            }else if(p.charAt(j) == '*'){
                pp = j;ss = i; j = pp+1;
            }else{
                if(pp == -1) return false;
                j = pp+1; i = ss++;
            }
        }//while
        while(j<lenp){
            if(p.charAt(j) != '*')
                break;
            j++;
        }
        return j == lenp; 
    }
}

接下来还是用动态规划重做这道题。设dp[i][j]表示s[1...i]与p[1....j]是否匹配。如果s[i] == p[j](or p[j] == '?'),那么dp[i][j] = dp[i-1][j-1];如果s[i] != p[j],但是p[j] == '*',

那么dp[i][j] = dp[i-1][j-1] | dp[i-1][j] | dp[i][j-1]。代码如下:

public class Solution {
    public boolean isMatch(String s, String p) {
        int lenp = p.length(),lens = s.length();
        boolean[][] dp = new boolean[lens+1][lenp+1];
        dp[0][0] = true;
        int i = 0;
        while(i < lenp&& p.charAt(i++) == '*'){
            for(int j = 0;j <= lens;j++) dp[j][i] = true;
        }
        for(i = 1;i <= lens;i++){
            for(int j = 1;j <= lenp;j++){
                if(s.charAt(i-1) == p.charAt(j-1)||p.charAt(j-1) == '?') dp[i][j] |= dp[i-1][j-1];
                else if(p.charAt(j-1) == '*') dp[i][j] = dp[i-1][j-1] | dp[i-1][j] | dp[i][j-1];
                //else dp[i][j] = false;
            }
        }
        return dp[lens][lenp];
    }
}

8.Maximum Subarray

Find the contiguous subarray within an array (containing at least one number) which has the largest sum.

For example, given the array [−2,1,−3,4,−1,2,1,−5,4],
the contiguous subarray [4,−1,2,1] has the largest sum = 6.

这个思路比较简单,用dp[i]表示以i结尾并包括i的最大连续子数组和,那么递推关系就是:

               dp[i-1]+nums[i]; if dp[i-1]>0

dp[i] = {    nums[i]             if dp[i-1] <= 0

而且如果允许修改数组,dp数组就可以用nums数组实现。

public class Solution {
    public int maxSubArray(int[] nums) {
        if(nums == null||nums.length == 0) return 0;
        if(nums.length == 1) return nums[0];
        int max = nums[0];
        for(int i = 1;i < nums.length;i++){
            if(nums[i-1]>0) nums[i] += nums[i-1];
            max = Math.max(nums[i],max);
        }
        return max;
    }
}

9.Unique Paths

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?

Note: m and n will be at most 100.

这个问题的思路也比较简单,dp[i][j]表示第i行第j列位置处的路径数,那么递推关系如下

                  1   i == 0 or j == 0

dp[i][j] = {

                   dp[i-1][j] + dp[i][j-1]    others

代码如下:

public class Solution {
    public int uniquePaths(int m, int n) {
        int[][] a = new int[m][n];
        int i=0,j=0;
        for(i = 0;i < m;i++){
            for(j = 0;j < n;j++){
                if(i == 0) a[i][j] = 1;
                else if(j == 0) a[i][j] = 1;
                else{
                    a[i][j] = a[i][j-1]+a[i-1][j];
                }
            }
        }//for
        return a[m-1][n-1];
    }
}

10.Unique Paths II

Follow up for "Unique Paths":

Now consider if some obstacles are added to the grids. How many unique paths would there be?

An obstacle and empty space is marked as 1 and0 respectively in the grid.

For example,

There is one obstacle in the middle of a 3x3 grid as illustrated below.

[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]

The total number of unique paths is 2.

Note: m and n will be at most 100.

思路与上一题类似,这里障碍点的dp值为0,而且不能越过障碍,i = 0,j = 0初始化时需要注意,直接看代码:
public class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length, n = obstacleGrid[0].length;
        int[][] res = new int[m][n];
        int i = 0,j = 0;
        if(obstacleGrid[0][0] == 0) res[0][0] = 1; 
        for(i = 0;i < m; i++){
            for(j = 0;j < n;j++){
                if(i == 0){ 
                    if(j == 0) continue;
                    if(res[i][j-1]==1 && obstacleGrid[i][j-1] == 0)
                        res[i][j] = 1;
                    else res[i][j] = 0;
                }
                else if(j == 0){ 
                    if(i == 0) continue;
                    if(res[i-1][j] == 1 && obstacleGrid[i-1][j] == 0)
                        res[i][j] = 1;
                    else res[i][j] = 0;
                }
                else{
                    if(obstacleGrid[i][j-1] != 1) res[i][j] += res[i][j-1];
                    if(obstacleGrid[i-1][j] != 1) res[i][j] += res[i-1][j];
                }
            }
        }///for
        return obstacleGrid[m-1][n-1] == 1?0:res[m-1][n-1];
    }



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值