分块与莫队(详详详解)

以下内容主要借鉴oiwiki

分块思想

简介

  • 分块是一种思想,而不是一种数据结构。
  • 分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
  • 分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
  • 分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
  • 当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好

例题

LibreOJ6284
题目描述
给出一个长为 的数列,以及 个操作,操作涉及区间加法,区间求和.
在这里插入图片描述

在这里插入图片描述
参考代码

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const ll maxn = 5e5 + 5;
ll a[maxn];
ll partLen;       //块长
ll partID[maxn];  //块号
ll partSum[maxn]; //块和
ll tag[maxn];     //块标记

void add(ll l, ll r, ll c)
{
    ll startID = partID[l], endID = partID[r];
    if (startID == endID) //l和r在同一个块,直接暴力
    {
        for (ll i = l; i <= r; i++)
        {
            a[i] += c;
            partSum[startID] += c;
        }
        return;
    }
    //不在同一个块,分成三段处理
    for (ll i = l; partID[i] == startID; i++)//起始段
    {
        a[i] += c;
        partSum[startID] += c;
    }
    for (ll i = startID + 1; i < endID; i++)//中间若干个整块
    {
        tag[i] += c;
        partSum[i] += c * partLen;
    }
    for (ll i = r; partID[i] == endID; i--)//末尾段
    {
        a[i] += c;
        partSum[endID] += c;
    }
}
//查询的思想和修改基本相同
ll query(ll l, ll r, ll c)
{
    ll ans = 0;
    ll startID = partID[l], endID = partID[r];
    if (startID == endID)
    {
        for (ll i = l; i <= r; i++)
        {
            ans += a[i] + tag[endID];
            ans %= c;
        }
        return ans;
    }
    for (ll i = l; partID[i] == startID; i++)
    {
        ans += a[i] + tag[startID];
        ans %= c;
    }
    for (ll i = startID + 1; i < endID; i++)
    {
        ans += partSum[i];
        ans %= c;
    }
    for (ll i = r; partID[i] == endID; i--)
    {
        ans += a[i] + tag[endID];
        ans %= c;
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll n;
    cin >> n;
    partLen = sqrt(n);
    memset(partID, -1, sizeof(partID));
    for (ll i = 0; i < n; i++)
    {
        cin >> a[i];
        partID[i] = i / partLen + 1;
        partSum[partID[i]] += a[i];
    }
    for (ll i = 0; i < n; i++)
    {
        ll opt, l, r, c;
        cin >> opt >> l >> r >> c;
        l--, r--;
        if (opt)
            cout << query(l, r, c + 1) << '\n';
        else
            add(l, r, c);
    }
    return 0;
}

补充:对于简单的求和操作,在无需修改的情况下,还可以通过维护前缀和来实现O(1)询问。

基于分块思想的分块数组

块状数组,即把一个数组分为几个块,块内信息整体保存,若查询时遇到两边不完整的块直接暴力查询。一般情况下,块的长度为 n \sqrt n n 。详细分析可以阅读 2017 年国家集训队论文中徐明宽的《非常规大小分块算法初探》。

例题

例题一洛谷P2801 教主的魔法

  • 题目大意: 对n个数进行q次操作, 每次输入opt ,l, r,v 操作1:询问【l,r】区间大于v的数的个数
    操作2:给【l,r】区间的数都加上v

  • 题解
    n \sqrt n n 为块长进行分块处理。
    对于询问:先判断l,r是否在同一个块内。在同一个块只需暴力求解即可,时间复杂度是块长O( n \sqrt n n );不在同一个块内则分成三段处理:两端暴力求解,中间段在数组有序的情况下可以通过二分快速得到答案,因此另外维护一个每个块是有序的t数组,时间复杂度是O( n \sqrt n n ∗ * log n \sqrt n n )。
    对于修改:先判断l,r是否在同一个块内。在同一个块只需暴力修改并更新t数组即可,时间复杂度O( n \sqrt n n ∗ * log n \sqrt n n );不在同一个块内则分成三段处理:两端暴力修改并更新t数组,时间复杂度O( n \sqrt n n ∗ * log n \sqrt n n ),中间段由于是整块修改,顺序不会改变,所以只用打个标记即可,时间复杂度是整块的数量,即O( n \sqrt n n )。
    所以总的时间复杂度是O(q* n \sqrt n n ∗ * log n \sqrt n n

  • 参考代码

#include <bits/stdc++.h>

using namespace std;
const int maxn = 1e6+5,maxm=1e3+5;
int st[maxm], ed[maxm], Size[maxm],dlt[maxn]; //每个块的起点、终点、块长,块上标记
int belong[maxn];//下标所在的块号
int num, n,q;
int a[maxn], t[maxn] ;//a为原来的(未被排序的)数组,t为对每个块排过序的数组
void Sort(int k) {
  for (int i = st[k]; i <= ed[k]; i++) t[i] = a[i];
  sort(t + st[k], t + ed[k] + 1);
}
void Modify(int l, int r, int c)
{
    int x = belong[l], y = belong[r];
    if (x == y)//在同一个块就直接修改
    {
        for (int i = l; i <= r; i++)
            a[i] += c;
        Sort(x);
        return;
    }
    for (int i = l; i <= ed[x]; i++)//直接修改起始段
        a[i] += c;
    for (int i = st[y]; i <= r; i++)//直接修改结束段
        a[i] += c;
    for (int i = x + 1; i < y; i++)//中间的整块打上标记
        dlt[i] += c;
    Sort(x);Sort(y);//起始段和末尾段需要重排
}
int Answer(int l, int r, int c)
{
    int ans = 0, x = belong[l], y = belong[r];
    if (x == y)//l和r属于同一个块时,直接暴力
    {
        for (int i = l; i <= r; i++)
            if (a[i] + dlt[x] >= c)
                ans++;
        return ans;
    }
    for (int i = l; i <= ed[x]; i++)//处理开头
        if (a[i] + dlt[x] >= c)
            ans++;
    for (int i = st[y]; i <= r; i++)//处理结尾
        if (a[i] + dlt[y] >= c)
            ans++;
    //用lower_bound找出中间每一个整块中第一个大于等于c的数的位置,注意考虑dlt
    //然后取包含该位置的后面所有的数,即为大于等于c的数
    for (int i = x + 1; i <= y - 1; i++)
        ans += ed[i] - (lower_bound(t + st[i], t + ed[i] + 1, c - dlt[i]) - t) + 1;
    return ans;
}
//根据需要初始化块状数组
void init()
{
    num = sqrt(n);
    for (int i = 1; i <= num; i++)
        st[i] = n / num * (i - 1) + 1, ed[i] = n / num * i;
    ed[num] = n;
    for (int i = 1; i <= num; i++)
    {
        for (int j = st[i]; j <= ed[i]; j++)
            belong[j] = i;
        Size[i] = ed[i] - st[i] + 1;
        sort(t+st[i],t+ed[i]+1);
    }

}
int main()
{
    cin>>n>>q;
    for (int i = 1; i <= n; i++)
    {
        cin>>a[i];
        t[i]=a[i];
    }
    init();
    while (q--)
    {
        char c;int l,r,w;
        cin>>c>>l>>r>>w;
        if(c=='A')
            cout<<Answer(l,r,w)<<'\n';
        else
            Modify(l,r,w);
    }
    
    return 0;
}

例题二
链接没找到,有兴趣自己去找找。
在这里插入图片描述
参考代码(没交过,不一定对)

#include <bits/stdc++.h>

#define inf 0x3f3f3f3f3f3f3f3fll
using namespace std;
const int maxn = 1e6 + 5, maxm = 1e3 + 5;
int st[maxm], ed[maxm], Size[maxm], dlt[maxn]; //每个块的起点、终点、块长,块上标记
int belong[maxn];                              //下标所在的块号
int num, n, q;
int a[maxn], t[maxn]; //原来的(未被排序的)数组,对每个块排过序的数组
/*
    注意点:
    同样使用dlt来给每个块打上标记,所以在处理开头和结尾时要先将标记下传
    下传时需将a数组和t数组同时更新
*/
void Sort(int k)
{
    for (int i = st[k]; i <= ed[k]; i++)
        t[i] = a[i];
    sort(t + st[k], t + ed[k] + 1);
}
void PushDown(int x)
{
    if (dlt[x] != inf)
        for (int i = st[x]; i <= ed[x]; i++)
            a[i] = t[i] = dlt[x];
    dlt[x] = inf;
}
void Modify(int l, int r, int c)
{
    int x = belong[l], y = belong[r];
    PushDown(x);
    if (x == y)
    {
        for (int i = l; i <= r; i++)
            a[i] = c;
        Sort(x);
        return;
    }
    PushDown(y);
    for (int i = l; i <= ed[x]; i++)
        a[i] = c;
    for (int i = st[y]; i <= r; i++)
        a[i] = c;
    Sort(x);Sort(y);
    for (int i = x + 1; i < y; i++)
        dlt[i] = c;
}
int Binary_Search(int l, int r, int c)//同lower_bound
{
    int ans = l - 1, mid;
    while (l <= r)
    {
        mid = (l + r) / 2;
        if (t[mid] <= c)
            ans = mid, l = mid + 1;
        else
            r = mid - 1;
    }
    return ans;
}
int Answer(int l, int r, int c)
{
    int ans = 0, x = belong[l], y = belong[r];
    PushDown(x);
    if (x == y)
    {
        for (int i = l; i <= r; i++)
            if (a[i] <= c)
                ans++;
        return ans;
    }
    PushDown(y);
    for (int i = l; i <= ed[x]; i++)
        if (a[i] <= c)
            ans++; 
    for (int i = st[y]; i <= r; i++)
        if (a[i] <= c)
            ans++;
    for (int i = x + 1; i <= y - 1; i++)
    {
        if (inf == dlt[i])//没有标记就直接二分查位置
            ans += Binary_Search(st[i], ed[i], c) - st[i] + 1;
        else if (dlt[i] <= c)//有标记,说明都是同一个数,直接比较即可
            ans += Size[i];
    }
    return ans;
}
void init()
{
    num = sqrt(n);
    for (int i = 1; i <= num; i++)
        st[i] = n / num * (i - 1) + 1, ed[i] = n / num * i;
    ed[num] = n;
    for (int i = 1; i <= num; i++)
    {
        for (int j = st[i]; j <= ed[i]; j++)
            belong[j] = i;
        Size[i] = ed[i] - st[i] + 1;
        sort(t + st[i], t + ed[i] + 1);
    }
}
int main()
{
    cin >> n >> q;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        t[i] = a[i];
    }
    init();
    while (q--)
    {
        char c;
        int l, r, w;
        cin >> c >> l >> r >> w;
        if (c == 'A')
            cout << Answer(l, r, w) << '\n';
        else
            Modify(l, r, w);
    }

    return 0;
}

莫队算法

莫队算法简介

莫队算法是由莫涛(当初是OI中国队队长)提出的算法,因此被称为莫队算法。在莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人,现在在国际上也是讲其称为“Mo‘s algorithm”。莫涛提出莫队算法时,只分析了普通莫队(顺序数据结构,不支持修改)算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。
莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。

普通莫队算法

对于关于一个序列的区间询问,如果满足以下两个条件:

1. 能从 区间【l,r】 的答案 O ( 1 ) O(1) O(1)扩展到区间【l-1,r】,【l+1,r】,【l,r-1】,【l,r+1】 (即与
相邻的区间)的答案
2. 询问可以被离线处理

那么在n(序列长度)和m(询问次数)同阶的情况下,将序列 n \sqrt n n 为长度进行分块,则对于序列上的区间询问问题,可以在 O( n ∗ n* n n \sqrt n n )的时间复杂度内求出所有询问的答案。
在n(序列长度)和m(询问次数)不同阶的情况下,将序列 n / n/ n/ m \sqrt m m 为长度进行分块,则对于序列上的区间询问问题,可以在 O( n ∗ n* n m \sqrt m m )的时间复杂度内求出所有询问的答案。

莫队算法的基本思想为分块,精髓在于对询问进行排序,即对所有的区间询问’ 【 l , r 】 【l,r】 l,r , 以 l l l 所在块的编号为第一关键字, r r r 为第二关键字从小到大排序。
排序后对所有询问顺序处理,即从上一次询问的答案扩展得到下一次询问。
具体的时间复杂度证明请参考OI wiki莫队算法复杂度证明以及罗勇军老师的博客
在这里插入图片描述

这是罗老师博客中有莫涛本人给出建议的复杂度对比图。
横坐标表示 l l l,纵坐标表示 r r r,点即表示询问。线段长度即表示从一个询问转移到下一个询问所要的计算次数。
补充说明:罗老师的博客写的很好也很仔细,但是推荐在具备线段树以及树状数组基础的时候再去看。

例题

例题一洛谷P2709 小B的询问
在这里插入图片描述
题解:

  1. 询问显然可以离线处理。
  2. 区间【l,r】转移到相邻区间会使得区间发生以下情况 增加一个数字,假设为X,则答案增加 ( x + 1 ) 2 − x 2 = 2 ∗ x + 1 (x+1)^ 2-x^2=2*x+1 x+12x2=2x+1
    减少一个数字,假设为X,则答案减少 x 2 − ( x − 1 ) 2 = 2 ∗ x − 1 x^ 2-(x-1)^2=2*x-1 x2(x1)2=2x1

所以套用普通莫队板子即可解决。
参考代码:

#include <bits/stdc++.h>
#define N 50005
#define ll long long
using namespace std;
struct Query
{
    int l, r, id, pos;
    bool operator<(const Query &x) const
    {
        if (pos == x.pos)
            return r < x.r;
        else
            return pos < x.pos;
    }
} query[N];
int num[N], n, m, K;
ll cnt[N], Ans[N];
ll ans = 0;
//把区间改变对答案所造成的影响推出来,
//然后对add和del进行相应修改即可
void add(int x)
{
    ans+=2*cnt[x] + 1;
    cnt[x]++;
}
void del(int x)
{
    ans -= 2 * cnt[x] - 1;
    cnt[x]--;
}
int main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m>>K;
    int size = (int)sqrt(n);
    for (int i = 1; i <= n; i++)
        cin>>num[i];
    //读入并排序询问
    for (int i = 1; i <= m; i++)
    {
        cin>>query[i].l>>query[i].r;
        query[i].id = i;
        query[i].pos = (query[i].l - 1) / size + 1;
    }
    sort(query + 1, query + m + 1);
    //离线处理询问
    int l = 1, r = 0;
    for (int i = 1; i <= m; i++)
    {
        while (l > query[i].l)//l需要往左移,区间增大
            add(num[--l]);
        while (r < query[i].r)//r需要往右移,区间增大
            add(num[++r]);
        while (l < query[i].l)//l需要往右移动,区间缩小
            del(num[l++]);
        while (r > query[i].r)//r需要往左移,区间缩小
            del(num[r--]);
        Ans[query[i].id] = ans;
    }
    for (int i = 1; i <= m; i++)
        cout<<Ans[i]<<'\n';
    return 0;
}

例题二洛谷P1494[国家集训队]小Z的妹子
题解:
和上一题大同小异,分析出转移到相邻区间给答案所带来的变化即可。
参考代码:

#include<bits/stdc++.h>

using namespace std;
const int N=50010;
int n,m;
int B,fz,fm,len,col[N],cnt[N],ans[N][2];

struct Query
{
    int l,r,id;
    bool operator<(Query& b)//l按块号排序,然后r按小到大排序
    {
        return l/B==b.l/B?r<b.r:l<b.l;
    }
} q[N];
void add(int x)
{
    fz+=cnt[x];
    ++cnt[x];
    fm+=len;
    ++len;
}
void del(int x)
{
    --cnt[x];
    fz-=cnt[x];
    --len;
    fm-=len;
}
int gcd(int a,int b)
{
    return b==0?a:gcd(b,a%b);
}
int main()
{
    int i,l=1,r=0,g;
    cin>>n>>m;
    B=n/sqrt(m);
    for (i=1;i<=n;++i)
    {
        cin>>col[i];
    }
    for (i=0;i<m;++i)
    {
        cin>>q[i].l>>q[i].r;
        q[i].id=i;
    }
    sort(q,q+m);
    for (i=0;i<m;++i)
    {
        if (q[i].l==q[i].r)
        {
            ans[q[i].id][0]=0;
            ans[q[i].id][1]=1;
            continue;
        }
        //从上一个区间转移到当前区间
        while (l>q[i].l)
            add(col[--l]);
        while (r<q[i].r)
            add(col[++r]);
        while (l<q[i].l) 
            del(col[l++]);
        while (r>q[i].r)
            del(col[r--]);
        g=gcd(fz,fm);
        ans[q[i].id][0]=fz/g;
        ans[q[i].id][1]=fm/g;
    }
    for (i=0;i<m;++i)
        printf("%d/%d\n",ans[i][0],ans[i][1]);
    return 0;
}

关于莫队算法的一些卡常技巧

  1. 采用奇偶排序法,即奇数块以 r r r从小到大排序,偶数块以 r r r从大到小排序。这就使得询问在块间转移时过渡更加平滑,如下图(红色部分即是普通排序所存在的骤变问题)。
    在这里插入图片描述

  2. 压行写法。

  3. 手写快读
    给出一位大佬的代码,内含卡常详细说明。出处
    例题链接:洛谷SP3267

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;

#define maxn 1010000
#define maxb 1010
int aa[maxn], cnt[maxn], belong[maxn];
int n, m, size, bnum, now, ans[maxn];
struct query {
	int l, r, id;
} q[maxn];

int cmp(query a, query b) {
	return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
int read() {
	int res = 0;
	char c = getchar();
	while(!isdigit(c)) c = getchar();
	while(isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar();
	return res;
}
void printi(int x) {
	if(x / 10) printi(x / 10);
	putchar(x % 10 + '0');
}

int main() {
	scanf("%d", &n);
	size = sqrt(n);
	bnum = ceil((double)n / size);
	for(int i = 1; i <= bnum; ++i) 
		for(int j = (i - 1) * size + 1; j <= i * size; ++j) {
			belong[j] = i;
		}
	for(int i = 1; i <= n; ++i) aa[i] = read(); 
	m = read();
	for(int i = 1; i <= m; ++i) {
		q[i].l = read(), q[i].r = read();
		q[i].id = i;
	}
	sort(q + 1, q + m + 1, cmp);
	int l = 1, r = 0;
	for(int i = 1; i <= m; ++i) {
		int ql = q[i].l, qr = q[i].r;
		while(l < ql) now -= !--cnt[aa[l++]];
		while(l > ql) now += !cnt[aa[--l]]++;
		while(r < qr) now += !cnt[aa[++r]]++;
		while(r > qr) now -= !--cnt[aa[r--]];
		ans[q[i].id] = now;
	}
	for(int i = 1; i <= m; ++i) printi(ans[i]), putchar('\n');
	return 0;
}

带修莫队

普通莫队算法是不支持修改的,强制在线需要另寻他法,但是对于某些允许离线的带修改区间查询经过改进,还能使用。
即原本的查询是用 【 l , r 】 【l,r】 l,r二维表示,现在加一维时间戳 t t t。对于每个查询操作,时间戳不重合时,如果当前修改多了就把多出的修改回退,否则接着修改,直到时间戳重合。
这样,我们当前区间的移动方向从四个([l−1,r]、[l+1,r]、[l,r−1]、[l,r+1])变成了六个([l−1,r,t]、[l+1,r,t]、[l,r−1,t]、[l,r+1,t]、[l,r,t−1]、[l,r,t+1])。
带修莫队的块长一般取 O ( n 2 / 3 ) O(n^{2/3}) O(n2/3),时间复杂度为 O ( n 5 / 3 ) O(n^{5/3}) O(n5/3),所以请谨慎使用。

例题
P1903 [国家集训队]数颜色 / 维护队列
在这里插入图片描述

#include <bits/stdc++.h>

using namespace std;
#define maxn 50500
#define maxc 1001000
int a[maxn], cnt[maxc], ans[maxn], belong[maxn];
struct query {
	int l, r, time, id;
} q[maxn];
struct modify {
	int pos, color, last;
} c[maxn];
int cntq, cntc, n, m, size, bnum;
int cmp(query a, query b) {
	return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.r] ^ belong[b.r]) ? belong[a.r] < belong[b.r] : a.time < b.time);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
inline int read() {
	int res = 0;
	char c = getchar();
	while(!isdigit(c)) c = getchar();
	while(isdigit(c)) res = (res << 1) + (res << 3) + (c ^ 48), c = getchar();
	return res;
}
int main() {
	n = read(), m = read();
	size = pow(n, 2.0 / 3.0);
	bnum = ceil((double)n / size);
	for(int i = 1; i <= bnum; ++i) 
		for(int j = (i - 1) * size + 1; j <= i * size; ++j) belong[j] = i;
	for(int i = 1; i <= n; ++i) 
		a[i] = read();
	for(int i = 1; i <= m; ++i) {
		char opt[100];
		scanf("%s", opt);
		if(opt[0] == 'Q') {
			q[++cntq].l = read();
			q[cntq].r = read();
			q[cntq].time = cntc;
			q[cntq].id = cntq;
		}
		else if(opt[0] == 'R') {
			c[++cntc].pos = read();
			c[cntc].color = read();
		}
	}
	sort(q + 1, q + cntq + 1, cmp);
	int l = 1, r = 0, time = 0, now = 0;
	for(int i = 1; i <= cntq; ++i) {
		int ql = q[i].l, qr = q[i].r, qt = q[i].time;
		while(l < ql) now -= !--cnt[a[l++]];
		while(l > ql) now += !cnt[a[--l]]++;
		while(r < qr) now += !cnt[a[++r]]++;
		while(r > qr) now -= !--cnt[a[r--]];
		while(time < qt) {
			++time;
			if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
			swap(a[c[time].pos], c[time].color);
		}
		while(time > qt) {
			if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
			swap(a[c[time].pos], c[time].color);
			--time;
		}
		ans[q[i].id] = now;
	}
	for(int i = 1; i <= cntq; ++i) 
		printf("%d\n", ans[i]);
	return 0;
}

树上莫队

对于询问树上路径问题的(对于有点权的树,给出树上路径的起点和终点,求路径上的权值和,或者种类数等),可以通过欧拉序加LCA转换成区间询问问题(其他处理区间询问的数据结构而经常用到这个思想),然后再用莫队算法解决,有兴趣自行了解。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值