线段树,实例,代码实现,区间最值,区间求和,顺序存储,链式存储

一、概述

     线段树是一种二叉搜索树,将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。

 

二、实例说明

      以求某区间的min为例,应用线段树,来说明其优势所在。并给出源码实现。

      求数组arr=[2,5,1,4,9,3]的某个区间[left,right]的最小值,原数组中的值允许随时更新。

      1、直观解法

      遍历数组区间找最小值,for(int i(left);i<=right;++i)minVal=min(minVal,arr[i]);

时间复杂度O(n),空间复杂度O(1),更新操作复杂度O(1),但是大数据辆,并且查询操作频繁时,会相当耗时。

      2、第二种解法

      用二维数组存储区间[i,j]的minVal,预处理的复杂度为O(n*n),查询时为O(1),可以应对频繁的查询操作。

      mat = [ 2  2  1  1  1   1]

                 [-1  5  1  1  1  1]

                 [-1-1  1  1  1  1]               

                           [-1-1-1  4  4  3]

                 [-1-1-1 -1  9  3]

                 [-1-1-1 -1 -1  3]                

      但是这种方案空间消耗很大,如果更新某个值,需要更新相应的二维数组中的minVal。

这种做法形式上类似于邻接矩阵。

 

     3、应用线段树

     构造线段树如图,预处理耗时O(n),查询、更新操作O(logn),需要额外的空间O(n)。

       

叶子节点是原始组数arr中的元素,非叶子节点代表它的所有子孙叶子节点所在区间的最小值。

 

三、源码

      线段树是二叉树,形式上有点像完全二叉树,先以顺序存储为例。使用数组实现,数组大小大概是序列个数的4倍,解释如下:

第一部分,叶子节点是原arr数组中元素,为N个。

第二部分,该二叉树只含有度为0和度为2的节点,所以非叶子节点个数是N-1个。

第三部分,倒数第二层的叶节点占掉的存储位置不会超过N*2。

所以数组大小可以设为4*N-1。

 

结点定义如下:

struct SegTree{
     int minVal;//该节点对应区间内的最小值,默认为-1
     int addMark;//延迟标记,默认为0
     SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
};

vector<SegTree> trees;//线段树,数组表示

trees.resize(4*N-1);

 

 1、求区间最小值,顺序存储

#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
#define root 0 , 0 , N - 1
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2

struct SegTree{
	int minVal;//该节点对应的区间最值,默认为-1
	int addMark;//延迟标记,默认为0
	SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
};
vector<int> arr;//原数组
vector<SegTree> trees;//线段树
//建立线段树,O(n),rt为线段树当前节点的下标,[iStart,iEnd]为原数组中的对应区间
void build(const int &rt,const int &iStart,const int &iEnd){
	trees[rt].addMark = 0;//SegTree中addMark初始化为0,所以build中的trees[rt].addMark = 0;可以省略掉。
	if (iStart == iEnd) {
                  //叶子节点赋值
		trees[rt].minVal = arr[iStart];
		return;
	}//if
	int iMid = (iStart + iEnd) >> 1;
         //左右子树赋值
	build(lRoot, iStart, iMid);
	build(rRoot, iMid + 1, iEnd);
         //当前节点更新
	trees[rt].minVal = min(trees[lRoot].minVal, trees[rRoot].minVal);
	return;
}//build
//延迟标记相关的函数,将本节点的延迟标记向左右子节点传递,同时更新最值,清除自身标记
void pushDown(const int &rt) {
	if (trees[rt].addMark) {
		trees[lRoot].addMark += trees[rt].addMark;
		trees[rRoot].addMark += trees[rt].addMark;
		trees[lRoot].minVal += trees[rt].addMark;
		trees[rRoot].minVal += trees[rt].addMark;
		trees[rt].addMark = 0;
	}//if
	return;
}//pushDown
//区间查询,[qStart,qEnd]为查询区间,rt为当前线段树的节点下标,[nStart,nEnd]为其所代表的相应区间
int query(const int &rt, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
	if (nStart > qEnd || nEnd < qStart)return INT_MAX;//当前区间与查询区间互不相交
	if (qStart <= nStart&&nEnd <= qEnd)return trees[rt].minVal;//线段树的当前区间包含于查询区间,返回对应节点的最值
	pushDown(rt);
	int nMid = (nStart + nEnd) >> 1;
	int qLeft = query(lRoot, nStart, nMid, qStart, qEnd);
	int qRight = query(rRoot, nMid + 1, nEnd, qStart, qEnd);
	return min(qLeft, qRight);
}//query
//单节点更新,index为原数组中待更新节点的下标,addVal为更新的值,rt为当前线段树的节点下标,[nStart,nEnd]为其所代表的相应区间
void updateOne(const int &rt,const int &nStart,const int &nEnd,const int &index, const int &addVal) {
	if (nStart == nEnd) {
                  //找到该叶节点,更新
		trees[rt].minVal += addVal;
		return;
	}//if
         //二分寻找待更新的节点
	int nMid = (nStart + nEnd) >> 1;
	if (index <= nMid)updateOne(lRoot, nStart, nMid, index, addVal);
	else updateOne(rRoot, nMid + 1, nEnd, index, addVal);
         //index节点被更新了,其他受影响的节点也要更新
	trees[rt].minVal = min(trees[lRoot].minVal, trees[rRoot].minVal);
	return;
}//updateOne
//区间更新,[uStart,uEnd]为原数组中待更新的区间,addVal是更新值,rt为当前线段树的节点下标,[nStart,nEnd]为其所代表的相应区间
void update(const int &rt, const int &nStart, const int &nEnd, const int &uStart, const int &uEnd, const int &addVal) {
	if (nStart > uEnd || nEnd < uStart)return;//区间不相交
	if (uStart <= nStart&&nEnd <= uEnd) {//当前区间包含于待更新区间
		trees[rt].addMark += addVal;
		trees[rt].minVal += addVal;
		return;
	}//if
	pushDown(rt);
	int nMid = (nStart + nEnd) >> 1;
	update(lRoot, nStart, nMid, uStart, uEnd, addVal);
	update(rRoot, nMid+1, nEnd, uStart, uEnd, addVal);
	trees[rt].minVal = min(trees[lRoot].minVal, trees[rRoot].minVal);
	return;
}//update

int main() {
	arr = { 3,4,5,7,2,1,0,3,4,5 };
	int N = (int)arr.size();
	trees.resize(N + N - 1 + (N << 1));

	build(0, 0, N - 1);
	/*updateOne(root, 6, 3);
	updateOne(root, 5, 3);
	updateOne(root, 6, -3);*/
	
	/*int minVal = query(root, 5, 8);
	printf("minVal=%d\n", minVal);
	
	minVal = query(root, 2, 5);
	printf("minVal=%d\n", minVal);*/

	/*update(root, 5, 8, 30);
	
	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal,tmp.addMark); putchar(10);
	printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);
	update(root, 2, 6, -40);
	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);

	int minVal = query(root, 0, 9);
	printf("minVal=%d\n", minVal);
	
	minVal = query(root, 5, 5);
	printf("minVal=%d\n", minVal);

	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);*/

	return 0;
}//main

区间查询query,必选满足“相邻的区间的信息可以被合并成两个区间的并区间的信息”,在这个问题上,区间最小值问题是满足的。将待查询区间[uStart,uEnd]划分为一个个小区间,这些小区间不重不漏,组合起来正好覆盖[uStart,uEnd],完美解决[uStart,uEnd]最小值的问题。

 

延迟标记addMark主要应用在update和query中,是线段树的精髓,每次操作不必将更改作用在叶子节点上,而是在不影响查询结果的情况下,尽量减少对线段树的更改。这种信息就被保存在addMark这个变量中。对于minVal,可以查看源码理解其操作,对于sumVal(见后面源码),也有相应的pushDown函数实现,可对照学习。

updateOne是单节点更新,每次的操作都作用在叶节点上了,理论上可以用多个updateOne来代替update的效果,但是效率会很低。

参考图片,

2、求区间最小值,链式存储

结点定义如下

struct SegTree {
	int minVal;//该节点对应的区间最值,默认为-1
	int addMark;//延迟标记
	SegTree *lch = NULL;//左节点指针
	SegTree *rch = NULL;//右节点指针
	SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
	~SegTree() {
		printf("[%d,%d]\n", minVal, addMark);
		if (lch) {
			delete lch;
			lch = NULL;
		}//if
		if (rch) {
			delete rch;
			rch = NULL;
		}//if
	}//~SegTree
};

源码实现上只有build函数变化较大,声明如下SegTree *build(SegTree *root, const int &iStart, const int &iEnd) ;

结尾要做内存处理

if(root){

     delete root;

     root = NULL;

}//if

#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2
#define rt root, 0, N - 1
#define CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>

struct SegTree {
	int minVal;//该节点对应的区间最值,默认为-1
	int addMark;//延迟标记,默认为0
	SegTree *lch = NULL;;//左节点指针
	SegTree *rch = NULL;;//右节点指针
	SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
	~SegTree() {
		printf("[%d,%d]\n", minVal, addMark);
		if (lch) {
			delete lch;
			lch = NULL;
		}//if
		if (rch) {
			delete rch;
			rch = NULL;
		}//if
	}//~SegTree
};
vector<int> arr;

SegTree *build(SegTree *root, const int &iStart, const int &iEnd) {
	root = new SegTree();
	//root->addMark = 0;
	if (iStart == iEnd) {
		root->minVal = arr[iStart];
		return root;
	}//if
	int iMid = (iStart + iEnd) >> 1;
	root->lch = build(root->lch, iStart, iMid);
	root->rch = build(root->rch, iMid + 1, iEnd);
	root->minVal = min(root->lch->minVal, root->rch->minVal);
	return root;
}//build

void pushDown(SegTree *root) {
	if (root->addMark) {
		root->lch->addMark += root->addMark;
		root->rch->addMark += root->addMark;
		root->lch->minVal += root->addMark;
		root->rch->minVal += root->addMark;
		root->addMark = 0;
	}//if
	return;
}//pushDown

int query(SegTree *root, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
	if (nStart > qEnd || nEnd < qStart)return INT_MAX;
	if (qStart <= nStart&&nEnd <= qEnd)return root->minVal;
	pushDown(root);
	int nMid = (nStart + nEnd) >> 1;
	int qLeft = query(root->lch, nStart, nMid, qStart, qEnd);
	int qRight = query(root->rch, nMid + 1, nEnd, qStart, qEnd);
	return min(qLeft, qRight);
}//query

void updateOne(SegTree *root, const int &nStart, const int &nEnd, const int &index, const int &addVal) {
	if (nStart == nEnd) {
		root->minVal += addVal;
		return;
	}//if
	int nMid = (nStart + nEnd) >> 1;
	if (index <= nMid)updateOne(root->lch, nStart, nMid, index, addVal);
	else updateOne(root->rch, nMid + 1, nEnd, index, addVal);
	root->minVal = min(root->lch->minVal, root->rch->minVal);
	return;
}//updateOne

void update(SegTree *root, const int &nStart, const int &nEnd, const int &uStart, const int &uEnd, const int &addVal) {
	if (nStart > uEnd || nEnd < uStart)return;
	if (uStart <= nStart&&nEnd <= uEnd) {
		root->addMark += addVal;
		root->minVal += addVal;
		return;
	}//if
	pushDown(root);
	int nMid = (nStart + nEnd) >> 1;
	update(root->lch, nStart, nMid, uStart, uEnd, addVal);
	update(root->rch, nMid + 1, nEnd, uStart, uEnd, addVal);
	root->minVal = min(root->lch->minVal, root->rch->minVal);
	return;
}//update

int main() {
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	arr = { 3,4,5,7,2,1,0,3,4,5 };
	int N = (int)arr.size();

	SegTree *root(NULL);
	root = build(rt);

	updateOne(rt, 6, 3);
	updateOne(rt, 5, 3);
	updateOne(rt, 6, -3);

	int minVal = query(rt, 5, 8);
	printf("minVal=%d\n", minVal);

	minVal = query(rt, 2, 5);
	printf("minVal=%d\n", minVal);

	/*update(rt, 5, 8, 30);
	update(rt, 2, 6, -40);

	int minVal = query(rt, 0, 9);
	printf("minVal=%d\n", minVal);

	minVal = query(rt, 5, 5);
	printf("minVal=%d\n", minVal);*/

	if (root) {
		delete root;
		root = NULL;
	}//if

	return 0;
}//main



3、其他相关问题

      线段树适用于解决区间动态查询和修改的问题,刚才是求区间最小值的问题,现在给出求区间和的问题。

也可以使用树状数组来解决,见树状数组应用于区间求和

求区间和,顺序存储,源码

相比于求区间最小值,pushDown有较大改动,原型为void pushDown(const int &rt, const int &len);后面的参数len是rt所对应结点区间的长度。

当前结点更新操作为trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;而不再是***=min(***,**)。

#include<cstdio>
#include<vector>
using namespace std;

#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2 
#define root 0,0,N-1

struct SegTree {
	int sumVal;
	int addMark;
	SegTree(int sumVal_ = -1, int addMark_ = 0) :sumVal(sumVal_), addMark(addMark_) {}
};
vector<int> arr;
vector<SegTree> trees;

void pushDown(const int &rt, const int &len) {
	if (trees[rt].addMark) {
		trees[lRoot].addMark += trees[rt].addMark;
		trees[rRoot].addMark += trees[rt].addMark;
		trees[lRoot].sumVal += trees[rt].addMark * (len - (len >> 1));//len-(len>>1)为左结点的长度,addMark为一个结点增加的值,整个trees[lRoot].sumVal增加的值为trees[rt].addMark*(len-(len>>1))。
		trees[rRoot].sumVal += trees[rt].addMark * (len >> 1);//len>>1为右结点长度
		trees[rt].addMark = 0;
	}//if
	return;
}//pushDown
void build(const int &rt, int iStart,int iEnd) {
	//trees[rt].addMark = 0;
	if (iStart == iEnd) {
		trees[rt].sumVal = arr[iStart];
		return;
	}//if
	int iMid = (iStart + iEnd) >> 1;
	build(lRoot, iStart, iMid);
	build(rRoot, iMid + 1, iEnd);
	trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;
	return;
}//build
int query(const int &rt, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
	if (nStart > qEnd || nEnd < qStart)return 0;
	if (qStart <= nStart&&nEnd <= qEnd) return trees[rt].sumVal;
	pushDown(rt, nEnd - nStart + 1);
	int nMid = (nStart + nEnd) >> 1;
	int retSum(0);
	retSum += query(lRoot, nStart, nMid, qStart, qEnd);
	retSum += query(rRoot, nMid + 1, nEnd, qStart, qEnd);
	return retSum;
}//query

void updateOne(const int &rt,const int &nStart,const int &nEnd,const int &index,const int &addVal) {
	if (nStart == nEnd) {
		trees[rt].sumVal += addVal;
		return;
	}//if
	int nMid = (nStart + nEnd) >> 1;
	if (index <= nMid)updateOne(lRoot, nStart, nMid, index, addVal);
	else updateOne(rRoot, nMid + 1, nEnd, index, addVal);
	trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;
	return;
}//updateOne

void update(const int &rt,const int &nStart,const int &nEnd,const int &uStart,const int &uEnd, const int &addVal) {
	if (nStart > uEnd || nEnd < uStart)return;
	if (uStart <= nStart&&nEnd <= uEnd) {
		trees[rt].addMark += addVal;
		trees[rt].sumVal += addVal*(nEnd - nStart + 1);
		return;
	}//if
	pushDown(rt, nEnd - nStart + 1);
	int nMid = (nStart + nEnd) >> 1;
	update(lRoot, nStart, nMid, uStart, uEnd, addVal);
	update(rRoot, nMid + 1, nEnd, uStart, uEnd, addVal);
	trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;
	return;
}//update

int main() {
	arr = { 3,4,5,7,2,1,0,3,4,5 };
	int N = arr.size();
	trees.resize(N + N - 1 + 2 * N);
	build(root);
	
	int sum = query(root, 5, N - 1);
	printf("%d\n", sum);

	update(root, 1, 4, 10);
	sum = query(root, 0, N - 1);
	printf("%d\n", sum);

	update(root, 8, 9, -100);
	sum = query(root, 0, N - 1);
	printf("%d\n", sum);

	sum = query(root, 0, 1);
	printf("%d\n", sum);

	updateOne(root, 5, 100);
	//update(root,5,5,100);
	sum = query(root, 0, N - 1);
	printf("%d\n", sum);
	
	printf("trees: "); for (int i(0); i < 4 * N - 1; ++i) printf("[%d,%d] ", trees[i].sumVal, trees[i].addMark); putchar(10);
	return 0;
}//main



4、求区间最小值,另一种实现思路

上面的源码中,注意到arr数组没有改变过,一直都是线段树本身结点在被更新。

下面从另一个角度,给出一个区间min值的实现。线段树中存储的不是相应区间的最小值,而是相应区间最小值对应的下标。

差别在于,比如build函数中,

if (iStart == iEnd) {
    trees[rt].minVal = iStart;
    return;
}//if

叶结点被赋值,之前是trees[rt].minVal=arr[iStart];现在改为,trees[rt].minVal=iStart;这就是直接存储下标的结果。

每次更新结点的minVal时,trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;

还需要比较arr中的值。

#include<stdio.h>
#include<vector>
using namespace std;
#define root 0 , 0 , N - 1
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2

struct SegTree {
	int minVal;
	int addMark;
	SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
};
vector<int> arr;
vector<SegTree> trees;

void build(const int &rt, const int &iStart, const int &iEnd) {
	//trees[rt].addMark = 0;
	if (iStart == iEnd) {
		trees[rt].minVal = iStart;
		return;
	}//if
	int iMid = (iStart + iEnd) >> 1;
	build(lRoot, iStart, iMid);
	build(rRoot, iMid + 1, iEnd);
	trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
	return;
}//build

void pushDown(const int &rt) {
	if (trees[rt].addMark) {
		trees[lRoot].addMark += trees[rt].addMark;
		trees[rRoot].addMark += trees[rt].addMark;
		arr[trees[lRoot].minVal] += trees[rt].addMark;
		arr[trees[rRoot].minVal] += trees[rt].addMark;
		trees[rt].addMark = 0;
	}//if
	return;
}//pushDown

int query(const int &rt, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
	if (nStart > qEnd || nEnd < qStart)return -1;
	if (qStart <= nStart&&nEnd <= qEnd)return trees[rt].minVal;
	pushDown(rt);
	int nMid = (nStart + nEnd) >> 1;
	int q1 = query(lRoot, nStart, nMid, qStart, qEnd);
	int q2 = query(rRoot, nMid + 1, nEnd, qStart, qEnd);
	if (q1 == -1)return q2;
	if (q2 == -1)return q1;
	return arr[q1] < arr[q2] ? q1 : q2;
}//query

void updateOne(const int &rt, const int &nStart, const int &nEnd, const int &index, const int &addVal) {
	if (nStart == nEnd) {
		arr[trees[rt].minVal] += addVal;
		return;
	}//if
	int nMid = (nStart + nEnd) >> 1;
	if (index <= nMid)updateOne(lRoot, nStart, nMid, index, addVal);
	else updateOne(rRoot, nMid + 1, nEnd, index, addVal);
	trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
	return;
}//updateOne

void update(const int &rt, const int &nStart, const int &nEnd, const int &uStart, const int &uEnd,const int &addVal){
	if (nStart > uEnd || nEnd < uStart)return;
	if (uStart <= nStart&&nEnd <= uEnd) {
		trees[rt].addMark += addVal;
		arr[trees[rt].minVal] += addVal;
		return;
	}//if
	pushDown(rt);
	int nMid = (nStart + nEnd) >> 1;
	update(lRoot, nStart, nMid, uStart, uEnd, addVal);
	update(rRoot, nMid + 1, nEnd, uStart, uEnd, addVal);
	trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
	return;
}//update

int main() {
	arr = { 3,4,5,7,2,1,0,3,4,5 };
	int N = (int)arr.size();
	trees.resize(N + N - 1 + (N << 1));

	build(0, 0, N - 1);
	updateOne(root, 6, 3);
	updateOne(root, 5, 3);
	updateOne(root, 6, -3);

	int minIndex = query(root, 5, 8);
	printf("arr[%d]=%d\n", minIndex, arr[minIndex]);

	minIndex = query(root, 2, 5);
	printf("arr[%d]=%d\n", minIndex, arr[minIndex]);

	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
	printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);

	/*update(root, 5, 8, 30);

	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal,tmp.addMark); putchar(10);
	printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);
	update(root, 2, 6, -40);
	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
	printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);

	int minIndex = query(root, 0, 9);
	printf("arr[%d]=%d\n", minIndex,arr[minIndex]);

	minIndex = query(root, 5, 5);
	printf("arr[%d]=%d\n", minIndex, arr[minIndex]);

	printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
	printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);*/

	return 0;
}//main

 参考图,

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值