【基础算法思想与搜索枚举】学习笔记

基础算法思想与搜索枚举

位运算

【一】基本位运算

举例:正数 1010 1010 1010 0011 0011 0011 32 32 32 位)

  • 按位与:& 0010 0010 0010

  • 按位或:| 1011 1011 1011

  • 按位异或:^ 1001 1001 1001

  • 取反:~ 0101 0101 0101 1100 1100 1100

  • 二进制左移:<< 2 2 2 位: 101000 101000 101000 1100 1100 1100

  • 二进制右移:>> 2 2 2 位: 10 10 10 0 0 0

C++ \textit{C++} C++ 运算符优先级

原码:二进制

反码:原码取反

补码:反码 + 1 +1 +1

【二】位运算应用

  1. 表示集合:二进制中的每一位 0 / 1 0/1 0/1 代表集合中某元素的存在情况。

  2. 特殊题目要求的位运算。

技巧:

1.获取一个数二进制的某一位:

inline int getBit(int a,int b){return (a>>b)&1;}
  1. 将一个数二进制设为 0 / 1 / 取反 0/1/\text{取反} 0/1/取反
inline int unsetBit(int a,int b){return a&~(1<<b);}	// 设为 0
inline int setBit(int a,int b){return a|(1<<b)};	// 设为 1
inline int flapBit(int a,int b){return a^(1<<b)};	// 取反
A P1100 高低位交换

分为前 16 16 16 位和后 16 16 16 位。

  1. 右移 16 16 16 位。

  2. 左移 16 16 16 位。

  3. 相加。

#include <iostream>
using namespace std;
int main() {
	unsigned int n;
	cin>>n;
	cout<<(n>>16)+(n<<16)<<endl;
}

【三】bitset

std::bitset 是标准库中一个存储 0 / 1 0/1 0/1 的大小不可变的容器。

#include <bitset>
bitset<1010> s,s1;	// 声明对象
// bitset<长度> s;
// bitset<长度> s(初始化元素);
// bitset<长度> s(string 类型 01 串);
// bitset<长度> s(char[] 类型 01 串);
// bitset 的下标从 0 开始,遍历时是从右往左遍历,也就是说它的第 0 位是二进制最右面的那个数。
s[1]=1;	// 和数组一样,通过下标更改元素
s.set(1,true);	// 含义同上
cout<<s[1]<<endl;	// operator []
cout<<s.test(1)<<endl;	// 功能同上
cout<<(s!=s1)<<endl;	// operator == !=
s|=s1;	// operator & | ^ &= |= ^=
s<<=1;	// operator << >> <<= >>=
cout<<s.count()<<endl;	// 统计 1 的个数
cout<<s.size()<<endl;	// 定义时定义的长度
cout<<s.any()<<endl;	// 是否至少有一个 1
cout<<s.none()<<endl;	// 是否所有位都为 0
cout<<s.all()<<endl;	// 是否所有位都为 1
cout<<s.to_string()<<endl;	// 转化成字符串然后输出
// 以下两个成员函数如果无参数,默认是全部设 0 或全部取反。
s.reset(1);	// 将第 1 位改为 0(同:s[1]=0)
s.flip(1);	// 将第 1 位取反(同:s[1]^=1)
B B3632 集合运算1

使用 bitset

#include <bits/stdc++.h>
using namespace std;
bitset<64> a,b,c;
int main(){
   int x,y;
   cin>>x;
   for(int i=1,tmp;i<=x;i++)cin>>tmp,a.set(tmp,1);
   for(int i=1,tmp;i<=x;i++)cin>>tmp,b.set(tmp,1);
   cout<<a.count()<<endl;
   c=a&b;
   for(int i=0;i<=63;i++)
		if(c[i])
        cout<<i<<" ";
   cout<<endl;
   return 0;
}

搜索

【一】搜索分类

【二】暴力枚举

对于比较复杂的题目,暴力枚举可以拿部分分。

框架:

void dfs(int n,...){
	if(所有状态均已枚举完成){
		...
		return;
	}
	for(遍历当前状态所有可能性){
		if(判断某一个可能是否合法){
			调整状态;
			dfs(n+1,...);
			撤回状态调整(回溯);
		}
	}
}
A P1157 组合的输出

暴力枚举即可,时间复杂度 O ( C n r ) \mathcal{O}(C^r_n) O(Cnr)

#include <bits/stdc++.h>
using namespace std;

int a[21];
bool vis[21];
int n, r;

void dfs(int pos) {
    if(pos == r + 1) {
        for(int i = 1; i <= r; ++i)
            cout << setw(3) << a[i];
        cout << endl;
        return;
    }
    for(int i = pos; i <= n; ++i)
        if(!vis[i] && a[pos - 1] < i) {
            vis[i] = 1;
            a[pos] = i;
            dfs(pos + 1);
            vis[i] = 0;
        }
}

int main() {
    cin >> n >> r;
    dfs(1);
    return 0;
}

【三】图的遍历

深度优先搜索和广度优先搜索(时间复杂度均为 O ( n + m ) \mathcal{O}(n+m) O(n+m)

// 深度优先搜索
void dfs(int u){
	vis[u]=1;
	for(所有 u 相邻的节点 v){
		if(!vis[v])
			dfs(v);
	}
}
// dfs(s);
// 广度优先搜索
void bfs(int s){
	queue<XXX> q;
   q.push(s);
	vis[s]=1;
  	while(!q.empty()){
		XXX u=q.front();
		q.pop();
		for(所有 u 相邻的节点 v){
			if(!vis[v])q.push(v),vis[v]=1;
      }
	}
}
B P5318 【深基18.例3】查找文献

可以用邻接表存图,数据输入完成后直接对所有的 vector 排序,然后分别 DFS \textit{DFS} DFS BFS \textit{BFS} BFS 即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
vector<int> p[MAXN];
int n, m;
bool vis[MAXN];
queue<int> q;

void dfs(int x) {  
    cout << x << " ";
    for (int i = 0; i < p[x].size(); i++)
        if (!vis[p[x][i]]) {
            vis[p[x][i]] = true;
            dfs(p[x][i]);
        }
}

void bfs() {
    memset(vis, 0, sizeof vis);
    vis[1] = 1;
    q.push(1);
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        cout << x << " ";
        for (int i = 0; i < p[x].size(); i++)
            if (!vis[p[x][i]]) {
                vis[p[x][i]] = true;
                q.push(p[x][i]);
            }
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        p[u].push_back(v);
    }
    for (int i = 1; i <= n; i++)
        sort(p[i].begin(), p[i].end());
    vis[1] = true;
    dfs(1);
    cout << endl;
    bfs();
    cout << endl;
    return 0;
}

【四】拓展技巧

I. 记忆化搜索

记忆化搜索是一种通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。

记忆化搜索确保了每个状态只访问一次

需要保证无后效性,即从不同的路径走到一个共同状态,后续的状态变迁都是一样的,和之前采用何种路径到这个状态没有关系。

C P1434 滑雪

从该点出发能够滑行的距离最大值是一定的,故可以使用记忆化搜索

无后效性:从某一点出发能够滑行的距离最大值是固定的。

f x , y f_{x,y} fx,y 记录从 x , y x,y x,y 点出发能够滑行的距离最大值,每次 DFS \textit{DFS} DFS 到一个点,先看一下这个点的 f f f 是否已经求出,如果求出了直接调用即可,而不必重新再运算一次。

#include <bits/stdc++.h>
using namespace std;
int arr[100][100];
int dis[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
int f[100][100];
int max2 = 0;
int n = 0;
int m = 0;
int dfs(int x, int y) {
int max1 = 0;
if (f[x][y] != 0) {
return f[x][y];
}
for (int i = 0; i < 4; i++) {
int fx = x + dis[i][1];
int fy = y + dis[i][0];
if (fx >= 0 && fx < n && fy >= 0 && fy < m && arr[fx][fy] < arr[x][y]) {
max1 = max(max1, dfs(fx, fy));
}
}
return max1 + 1;
}
int main() {
int max1 = -1;
cin >> n >> m;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> arr[i][j];
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
f[i][j] = dfs(i, j);
max1 = max(max1, f[i][j]);
}
}
cout << max1 << endl;
return 0;
}
II. 剪枝初步

常见剪枝方法:

  • 最优性剪枝:当当前解已经比已有解差时,直接停止搜索。

  • 可行性剪枝:当当前解已经不可用时,直接停止搜索。

ans=最坏情况;
void dfs(int n,...){
	if(当前的答案比 ans 还要差)return;	// 最优性剪枝
	if(当前的状态已经不可用了)return;	// 可行性剪枝
	// 判断是否所有状态均已枚举完成。
	// 遍历当前状态的所有可能性,并进行对应调整。
}
D P1434 猫粮规划

搜索时注意剪枝即可。

#include <bits/stdc++.h>
using namespace std;

int a[41], w[41];
int n, l, r, ans;

void dfs(int pos) {
    int sum = 0;
    for(int i = 1; i < pos; ++i)
        sum += a[i] * w[i];
    if(sum > r)
        return;
    if(pos == n + 1) {
        if(l <= sum && sum <= r)
            ++ans;
        return;
    }
    for(int i = 0; i <= 1; ++i) {
        a[pos] = i;
        dfs(pos + 1);
    }
}

int main() {
    cin >> n >> l >> r;
    for(int i = 1; i <= n; ++i)
        cin >> w[i];
    dfs(1);
    cout << ans << endl;
    return 0;
}

贪心算法

贪心算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。贪心算法所做出的仅是某种意义上的局部最优解

使用贪心算法解题一定需要保证从不同的路径走到一个共同状态,后续的状态变迁都是一样的,和之前采用何种路径到这个状态没有关系,即无后效性。同时一定要确保自己大概能证明其正确性;如果不能保证,那么就要做好这道题拿的分很少或者拿不到分的心理准备。

思路和实现流程:从问题的某一初始解出发。当能朝给定总目标前进一步时,求出可行解的一个解元素;最后,由所有解元素组合成问题的一个可行解。

证明正确性:大胆假设,小心求证。

  1. 反证法:如果交换方案中任意两个元素/相邻的两个元素后,答案不会变得更好,那么可以推定目前的解已经是最优解了。

  2. 归纳法:先算出边界情况(例如 n = 1 n=1 n=1)的最优解 F 1 F_1 F1,然后再证明:对于每个 F n F_n Fn,都可以由 F n − 1 F_{n-1} Fn1 推导出结果来。

证伪贪心算法往往采用构造出一个反例的方式。

A P1208 [USACO1.3] 混合牛奶 Mixing Milk

优先选择从单价最低的农民处购买牛奶,由便宜到贵进行购买。

证明(反证法):易证,如果已经由便宜到贵全部购买,换一位农民购买不会更便宜,即不会使答案更优。

#include <bits/stdc++.h>
using namespace std;
struct node{
	int p,a;
}c[5010];
int n,m,sum,ans;
bool cmp(node a,node b){
	if(a.p==b.p)return a.a>b.a;
	return a.p<b.p;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)cin>>c[i].p>>c[i].a;
	sort(c+1,c+m+1,cmp);
	for(int i=1;i<=m;i++){
		if(sum+c[i].a>=n){
			ans+=c[i].p*(n-sum);
			break;
		}
		sum+=c[i].a,ans+=c[i].p*c[i].a;
	}
	cout<<ans<<endl;
	return 0;
}
B B3635 硬币问题

可以证明,贪心算法是有后效性的,因此此题应当使用 DP \textit{DP} DP 或搜索。

// bfs

#include <iostream>
#include <queue>
#include <cstring>

using namespace std;

const int N = 1000010, C[] = {1, 5, 11};

int n, dis[N];

void bfs() {
	queue<int> q;
	q.push(0);
	
	memset(dis, 0x3f, sizeof(dis));
	dis[0] = 0;
	
	while(!q.empty()) {
		int cur = q.front();
		q.pop();
		if(cur == n) cout<<dis[cur];
		for(int c : C)
			if(dis[cur] + 1 < dis[cur + c])
				dis[cur + c] = dis[cur] + 1, q.push(cur + c);
		
	}
}

int main() {
	cin>>n;
	bfs();
	return 0;
}

二分

二分查找

在一个有序序列中查找某一元素的算法。

时间复杂度 O ( log ⁡ n ) \mathcal{O}(\log{n}) O(logn)

猜数游戏

小 A 想一个 1 1 1~ 2 10 2^{10} 210 的整数,小 B 猜这个数。小 A 会告诉小 B 她说的数字是大于、等于还是小于生成的数字。

朴素算法是从 1 1 1 开始枚举,时间复杂度 O ( n ) \mathcal{O}(n) O(n),会超时,而且小 A 和小 B 会很累。

优秀的策略:

假定序列单调递增:

  • 在指定的区域之间尝试中间值

  • 如果中间值是答案直接输出答案

  • 如果中间值太大,则处理左区间

  • 如果中间值太小,则处理右区间

  • 每次可以将范围缩小至一半。

一般来说,二分查找只能在有序数组中查找某一元素。

二分搜索模板
int a[100010];
int binary_search(int L,int R,int k){
	int l=L,r=R,ans=-1;
	while(l<=r){
		int mid=(l+r)/2;
		if(a[mid]==k)l=mid+1;// 或 r=mid-1;
		else if(a[mid]<k)l=mid+1;
		else r=mid-1;
	}
	return ans;
}
A 查找重复序列中的某数字第一次出现的位置
int a[100010];
int binary_search(int x){
	int ans=-1,l=1,r=n;
	while(l<=r){
		int mid=(l+r)/2;
		if(a[mid]==x)ans=x,r=mid-1;
		else if(a[mid]<x)l=mid+1;
		else r=mid-1;
	}
	return ans;
}
二分答案

二分答案与二分查找类似。

有一类题,答案具有有界性和单调性且直接求解很难,但是如果验证某解是否符合题意相对容易。那这类题便可采用二分答案求解。

单调性:设答案区间 [ 1 , n ] [1,n] [1,n],存在某一合法解 x x x,使得 x + 1 , x + 2 , . . . , n x+1,x+2,...,n x+1,x+2,...,n 都符合要求,而 1 , 2 , . . . , x − 1 1,2,...,x-1 1,2,...,x1 都不符合要求。则 x x x 为答案,且可以说答案具有单调性

二分答案本质:每次缩小答案的规模,在答案合法条件下不断逼近最优,得到答案。

一般而言,可用二分答案解决的题目具有以下特征:

  • 求最小的最大值

  • 求最大的最小值

  • 求满足条件的最小(大)值

  • 求最接近某值的一个值

  • 求最小的能满足条件的代价

B P9455 [入门赛 #14] 塔台超频 (Hard Version)

二分答案。

#include <bits/stdc++.h>
using namespace std;
// 二分答案
int a[500010],b[500010],n;
bool check(int x){
	int maxx=a[1]+b[1]+x;
	for(int i=2;i<=n;i++){
		if(maxx<a[i])return false;
		maxx=max(maxx,a[i]+b[i]+x);
	}
	return true;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i]>>b[i];
	int l=0,r=1e9,mid,ans;
	while(l<=r){
		mid=(l+r)/2;
		if(check(mid))ans=mid,r=mid-1;
		else l=mid+1;
	}
	cout<<ans<<endl;
	return 0;
}
C P1824 进击的奶牛

二分答案。

#include <bits/stdc++.h>
using namespace std;
// 二分答案
int a[500010],n,c;
bool check(int x){
	int cnt=0,lc=a[1];
	for(int i=2;i<=n&&cnt<=n-c;i++){
		if(a[i]-lc<x)++cnt;
		else lc=a[i];
	}
	return cnt<=n-c;
}
int main(){
	cin>>n>>c;
	for(int i=1;i<=n;i++)cin>>a[i];
	sort(a+1,a+n+1);
	int l=0,r=a[n]-a[1],mid,ans;
	while(l<=r){
		mid=(l+r)/2;
		if(check(mid))ans=mid,l=mid+1;
		else r=mid-1;
	}
	cout<<ans<<endl;
	return 0;
}

前缀和、差分与双指针

前缀和

前缀和可以简单理解为“数列的前 n n n 项的和”。它是一种重要的与处理方式,能大大降低查询的时间复杂度(查询的时间复杂度为 O ( 1 ) \mathcal{O}(1) O(1))。

一般来讲,我们会预处理一个数组。对数组中的每个元素,我们记录从起始该元素对应下标/状态所有数字的总和。

  • 一维前缀和

预处理: s u m i = ∑ j = 1 i a j = s u m i − 1 + a i sum_i=\sum\limits_{j=1}^{i}a_j=sum_{i-1}+a_i sumi=j=1iaj=sumi1+ai

查询: ∑ i = l r a i = s u m r − s u m l − 1 \sum\limits_{i=l}^{r}a_i=sum_r-sum_{l-1} i=lrai=sumrsuml1

  • 二维前缀和

预处理: s u m i , j = ∑ k = 1 i ∑ l = 1 j s u m k , l = s u m i − 1 , j + s u m i , j − 1 + s u m i − 1 , j − 1 + a i , j sum_{i,j}=\sum\limits_{k=1}^{i}\sum\limits_{l=1}^{j}sum_{k,l}=sum_{i-1,j}+sum_{i,j-1}+sum_{i-1,j-1}+a_{i,j} sumi,j=k=1il=1jsumk,l=sumi1,j+sumi,j1+sumi1,j1+ai,j

查询: ∑ i = x 1 x 2 ∑ j = y 1 y 2 a i , j = s u m x 2 , y 2 + s u m x 1 − 1 , y 1 − 1 − s u m x 2 , y 1 − 1 − s u m x 1 − 1 , y 2 \sum\limits_{i=x1}^{x2}\sum\limits_{j=y1}^{y2}a_{i,j}=sum_{x2,y2}+sum_{x1-1,y1-1}-sum_{x2,y1-1}-sum_{x1-1,y2} i=x1x2j=y1y2ai,j=sumx2,y2+sumx11,y11sumx2,y11sumx11,y2

  • 多维前缀和

可以使用容斥原理推导。

A P8218 【深进1.例1】求区间和

一维前缀和模板。

#include <iostream>
using namespace std;

typedef long long ll;
const int N = 1e5 + 5;
int a[N];
ll sum[N];

int main() {
	int n;
	cin >> n;
	for(int i = 1; i <= n; ++i) {
		cin >> a[i];
		sum[i] = sum[i - 1] + a[i];
	}
	int m;
	cin >> m;
	for(int i = 1, l, r; i <= m; ++i) {
		cin >> l >> r;
		cout << sum[r] - sum[l - 1] << endl;
	}
	return 0;
}
差分

差分可以简单地理解为前缀和的逆运算

它也是一种重要的处理方式,能大大降低多次修改但最终只有一次查询的时间复杂度。

一般来讲,我们会处理一个数组,对数组中的每个元素,我们记录该元素对应下表/状态代表的值与其之前一个元素的值的差值。

形式化地讲,对一个长度为 n n n 的数组 a a a,我们对其建立差分数组 f f f,结果如下:

f i = { a 1 i = 1 a i − a i − 1 2 ≤ i ≤ n f_i=\begin{cases} a_1&i=1 \\ a_i-a_{i-1}&2\le i\le n \end{cases} fi={a1aiai1i=12in

主要作用:多次修改但最终只有一次查询

举例:给定 n n n 个整数, m m m 次修改,第 i i i 次修改将区间 [ l i , r i ] [l_i,r_i] [li,ri] 中的数字统一 + x i +x_i +xi

如果使用暴力,需要对每个修改从 l i l_i li 枚举到 r i r_i ri,时间复杂度会很大。

但是如果我们按照如下方式使用差分:

f l i + = x i , f r i + 1 − = x i f_{l_i}+=x_i,f_{r_i+1}-=x_i fli+=xi,fri+1=xi

在最后查询时,我们遍历 1 1 1~ n n n,然后进行前缀和即可得到每一个修改后的元素。

B 差分模板
#include <bits/stdc++.h>
using namespace std;
int n,m,a[100010],l[100010],r[100010],x[100010],f[100010];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++)cin>>l[i]>>r[i]>>x[i],f[l[i]]+=x[i],f[r[i]+1]-=x[i];
	int cur=0;
	for(int i=1;i<=n;i++)cur+=f[i],a[i]=cur;
	for(int i=1;i<=n;i++)cout<<a[i]<<" ";
	cout<<endl;
	return 0;
}
双指针

双指针是同时使用两个指针,指向序列、链表结构上的位置,或指向树、图结构中的节点,通过同向移动或相向移动来维护、统计信息。

  1. 利用序列有序性。

  2. 维护区间信息。

  3. 快慢指针(例:在单向链表中找环)。

时间复杂度一般为 O ( n ) \mathcal{O}(n) O(n)

C P1102 A-B 数对
  1. 维护序列有序性。

将数组排序后使用双指针,前指针枚举 A A A,后指针尝试寻找满足条件的 B B B

对于某个 A A A,当后指针指向的元素小于 A − C A-C AC 时,不断向数组增大方向跳跃,直至找到 ≥ A − C \ge A-C AC 的元素。

#include <bits/stdc++.h>
using namespace std;
int n,c,l,r,a[200010];
long long sum;
int main(){
	cin>>n>>c;
	for(int i=1;i<=n;i++)cin>>a[i];
	sort(a+1,a+n+1);
	l=r=1;
	for(int i=1;i<=n;i++){
		while(a[l]<a[i]-c&&l<=n)++l;
		while(a[r]<=a[i]-c&&r<=n)++r;
		if(a[i]-a[l]==c)sum+=r-l;
	}
	cout<<sum<<endl;
	return 0;
}
D P1147 连续自然数和
  1. 维护区间信息。

使用双指针,用 l , r l,r l,r 两个指针,维护区间和 s = ∑ i = l r i s=\sum\limits_{i=l}^{r}i s=i=lri

r < M r<M r<M r ≥ M r\ge M rM 时显然不符合条件)的情况下,每次让 r r r 右移一位, s ← s + r s\gets s+r ss+r。当 s > M s>M s>M 时,不断右移 l l l,同时 s ← s − l s\gets s-l ssl。如果移动 l l l s = M s=M s=M,则输出此时的 l , r l,r l,r

#include <bits/stdc++.h>
using namespace std;
int l,r,m,cur;
int main(){
	cin>>m;
	l=r=cur=1;
	while(r<m){
		while(cur>m)cur-=l++;
		if(cur==m)cout<<l<<" "<<r<<endl;
		cur+=++r;
	}
	return 0;
}
E 在单向链表中找环
  1. 快慢指针(例:在单向链表中找环)。

首先两个指针都指向链表的头部,令一个指针一次走一步,另一个指针一次走两步,如果它们相遇了,证明有环,否则无环。

总时间复杂度为 O ( n ) \mathcal{O}(n) O(n)

找到环的起点 找到环的起点 找到环的起点:在两个指针相遇后,将其中一个指针移到表头,让两者都一步一步走,再度的位置即为环的起点。

证明:设而这第一次相遇时慢指针一共走 k k k 步,快指针走了 2 k 2k 2k 步,设单指针在环上走了 l l l 步,环长为 C C C,有 2 k = n × C + l + ( k − l )    ⟹    k = n × C 2k=n\times C+l+(k-l)\implies k=n\times C 2k=n×C+l+(kl)k=n×C

第一次相遇时 n = 1 n=1 n=1,即 k = C k=C k=C

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三日连珠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值