LintCode 解题记录17.5.10(tag:线段树)

第一次学习“线段树”这种数据结构。记录一下LintCode线段树相关的题目。

LintCode 201 线段树的构造
每个节点代表一个[start, end]的间隔(interval),同时还可以用第三变量val来存储和具有区间和性质的变量(比如该区间的最大值、最小值、元素和、元素个数等等)。具有区间和的意思就是说在一个区间上的操作是否可以转化为两个子区间上的操作。
父亲节点的间隔为[start, end],那么(mid=(start+end)>>1)左孩子的间隔就是[start, mid],右孩子的为[mid+1, end]。递归建树直到start==end递归结束。

/**
 *这是官方给的线段树的节点类,学习一下。
 * Definition of SegmentTreeNode:
 * class SegmentTreeNode {
 * public:
 *     int start, end;
 *     SegmentTreeNode *left, *right;
 *     SegmentTreeNode(int start, int end) {
 *         this->start = start, this->end = end;
 *         this->left = this->right = NULL;
 *     }
 * }
 */
class Solution {
public:
    /**
     *@param start, end: Denote an segment / interval
     *@return: The root of Segment Tree
     */
    SegmentTreeNode * build(int start, int end) {
        // write your code here
        if (start > end) return NULL;//写这句话是排除非法输入- -。
        SegmentTreeNode *root = new SegmentTreeNode(start, end);
        if (start == end) return root;//如果到达叶子节点
        int mid = (start+end)>>1;//由于保证了start<end,所以这里start <= mid < mid+1 <= end。另外说一句这里用位运算代替除法的原因是可以更快的进行运算
        //递归建树
        root->left = build(start, mid);
        root->right = build(mid+1, end);
        return root;
    }
};

LintCode 439 线段树的构造2
不同于上一题,这一题要求要用第三个变量max来存储区间的最大值。思路是一样的,最后回溯更新该节点的值。

    SegmentTreeNode *build(int left, int right, vector<int> &A) {
        //if (left > right) return NULL; 不考虑这种非法输入的情况
        SegmentTreeNode *root = new SegmentTreeNode(left, right, A[left]);
        if (left == right) return root;
        int mid = (left+right) >> 1;// left < mid < mid+1 <= right
        root->left = build(left, mid, A);
        root->right = build(mid+1, right, A);
        root->max = max(root->left->max, root->right->max); //区间的最大值具有区间的操作性质,回溯更新
        return root;
    }
    SegmentTreeNode * build(vector<int>& A) {
        // write your code here
        if (A.size() == 0) return NULL;
        return build(0, A.size()-1, A);
    }

LintCode 202 线段树的查询
线段树的基本操作之一,重要,单词查询的时间复杂度为O(h),h为树高logn。本题为查询区间[start, end]内的最大值。请看代码:

    int query(SegmentTreeNode *root, int start, int end) {
        //如果当前访问的节点的区间包含于 所要查询的区间,那么则直接返回
        if (root == NULL) return 0;
        if (start <= root->start && end >= root->end) return root->max;
        //否则的话则查看左右孩子节点,其区间分别为[root->start, mid],[mid+1, root->end]
        int mid = (root->start+root->end)>>1;
        int left_max, right_max;
        left_max = right_max = 0;
        if (start <= mid) //与左孩子区间有交集 或者 end < root->start即整个区间在该根节点区间的左边
            left_max = query(root->left, start, end);
        if (mid+1 <= end) //与右孩子区间有交集 或者 整个区间在根节点区间的右边
            right_max = query(root->right, start, end);
        return max(left_max, right_max);
    }

LintCode 247 线段树的查询2
这一题和上一题最主要的区别就是测试用例给了查询的区间在总区间的最右侧这种特殊情况。所以要判断root == NULL。

    int query(SegmentTreeNode *root, int start, int end) {
        // write your code here
        if (root == NULL) return 0;
        if (start <= root->start && end >= root->end) return root->count;
        int mid = (root->start+root->end)>>1;
        int ans = 0;
        if (start <= mid) ans += query(root->left, start, end);
        if (mid+1 <= end) ans += query(root->right, start, end);
        return ans;
    }

LintCode 203 线段树修改
即把某一叶子节点的值修改为val。首先要递归找到该叶子节点,然后再回溯修改其祖先节点的值。

    void modify(SegmentTreeNode *root, int index, int value) {
        // write your code here
        if (root->start == index && root->end == index) {
            root->max = value;
            return;
        }
        int mid = (root->start+root->end)>>1;
        if (index <= mid) modify(root->left, index, value);//该叶子节点在左子树
        if (index >= mid+1) modify(root->right, index, value);//该叶子节点在右子树
        root->max = max(root->left->max, root->right->max);
    }

LintCode 206. 区间求和I
不同于以往给出了线段树的结构声明,此题没有给。所以这里有两种思路:一是按照之前的做法,自己定义线段树节点类,定义其build函数与query函数。另外一种思路是直接用数组来模拟树的结构。
思路1:

struct SegmentTree {
    int start, end;
    long long sum;
    SegmentTree *left, *right;
    SegmentTree(int start, int end, long long sum) {
        this->start = start;
        this->end = end;
        this->sum = sum;
        this->left = this->right = NULL;
    }
};
class Solution { 
public:
    /**
     *@param A, queries: Given an integer array and an query list
     *@return: The result list
     */
    SegmentTree* build(int start, int end, vector<int> &A) {
        if (start > end) return NULL;
        SegmentTree *root = new SegmentTree(start, end, A[start]);
        if (start == end) return root;
        int mid = (start+end)>>1;
        root->left = build(start, mid, A);
        root->right = build(mid+1, end, A);
        root->sum = root->left->sum + root->right->sum;
        return root;
    }

    long long query(SegmentTree* root, int start, int end) {
        if (root == NULL) return 0;
        if (start <= root->start && end >= root->end) return root->sum;
        int mid = (root->start+root->end)>>1; 
        long long ans = 0;
        if (start <= mid) ans += query(root->left, start, end);
        if (mid+1 <= end) ans += query(root->right, start, end);
        return ans;
    }
   vector<long long> intervalSum(vector<int> &A, vector<Interval> &queries) {
        vector<long long> ret;
        SegmentTree *root = build(0, A.size()-1, A);
        for (int i = 0; i < queries.size(); i++) {
            ret.push_back(query(root, queries[i].start, queries[i].end));
        }
        return ret;
    }
};

思路2:由于线段树结构上很像完全二叉树(但并不是完全二叉树),所以可以利用类似完全二叉树的下标表示法来表示节点之间的关系。即标号0…n-1,节点i的左孩子节点2i+1,右孩子2i+2,这里可以用位操作来代替乘2操作。
不过为什么下面的代码总是超时,在百分之55的时候超时,暂时没有解决,马克一下。还望大神指点。

    void build(vector<long long> &Sum, vector<int> &A, int start, int end, 
        int idx) {
        if (start > end) return;
        if (start == end) {
            Sum[idx] = A[start];
            return;
        }
        int mid = (start+end)>>1;
        build(Sum, A, start, mid, (idx<<1)+1);
        build(Sum, A, mid+1, end, (idx<<1)+2);
        Sum[idx] = Sum[(idx<<1)+1]+Sum[(idx<<1)+2];
    }
    long long query(int root_start, int root_end, int start, int end, int idx, 
        vector<long long> Sum) {
        if (start <= root_start && end >= root_end) return Sum[idx];
        if (root_start == root_end) return 0;
        int mid = (root_start+root_end)>>1;
        long long ans = 0;
        if (start <= mid)
            ans += query(root_start, mid, start, end, (idx<<1)+1, Sum);
        if (mid+1 <= end)
           ans += query(mid+1, root_end, start, end, (idx<<1)+2, Sum);
        return ans;
    }
    vector<long long> intervalSum(vector<int> &A, vector<Interval> &queries) {
        // write your code here
        int len = A.size();
        vector<long long> Sum(4*len, 0);
        vector<long long> ret;
        //Sum.resize(4*A.size());
        build(Sum, A, 0, A.size()-1, 0);
        for (int i = 0; i < queries.size(); i++) {
            ret.push_back(query(0, A.size()-1, queries[i].start, queries[i].end, 0, Sum));
        }
        return ret;
    }

思路:这题可以用 前缀和来做。好像更简单一点- -。

        vector<long long> ret;
        vector<long long> preSum(A.size(), 0);
        long long sum = 0;
        for (int i = 0; i < A.size(); i++) {
            sum += A[i];
            preSum[i] = sum;
        }
        for (int i = 0; i < queries.size(); i++) {
            int s = queries[i].start, e = queries[i].end;
            ret.push_back(preSum[e]-preSum[s]+A[s]);
        }
        return ret;

LintCode 205 区间最小数
同之前的题目一样,用线段树来做。

struct SegmentTree {
    int start, end, min;
    SegmentTree *left, *right;
    SegmentTree(int start, int end, int min) {
        this->start = start;
        this->end = end;
        this->min = min;
        this->left = this->right = NULL;
    }
};
//构造线段树的操作时间复杂度和空间复杂度都是O(n)。
SegmentTree* build(int start, int end, vector<int> &A) {
    if (start > end) return NULL;
    SegmentTree* root = new SegmentTree(start, end, A[start]);
    if (start == end) return root;
    int mid = (start+end)>>1;
    root->left = build(start, mid, A);
    root->right = build(mid+1, end, A);
    root->min = min(root->left->min, root->right->min);
    return root;
}
//从root查到leaf node,所以线段树查询的时间复杂度为O(logn)
int query(SegmentTree* root, int start, int end) {
    if (start > end || root == NULL) return 0x7fffffff;
    if (start <= root->start && end >= root->end) return root->min;
    int mid = (root->start + root->end)>>1;
    int ANS = 0x7fffffff;
    if (start <= mid) ANS = min(ANS, query(root->left, start, end));
    if (mid+1 <= end) ANS = min(ANS, query(root->right, start, end));
    return ANS;
}
class Solution { 
public:
    /**
     *@param A, queries: Given an integer array and an query list
     *@return: The result list
     */
    vector<int> intervalMinNumber(vector<int> &A, vector<Interval> &queries) {
        // write your code here
        vector<int> ret;
        SegmentTree *root = build(0, A.size()-1, A);
        for (int i = 0; i < queries.size(); i++) {
            ret.push_back(query(root, queries[i].start, queries[i].end));
        }
        return ret;
    }
};

同样用数组来代替树状结构的代码仍然超时了,我真是日了狗了。

LintCode 248.统计比给定整数小的数的个数
Challenge:
1.just loop。
2.sort and binary search。
3.build segment tree and search。
第一种:
试了一下最简单无脑循环,结果果不其然TLE了。后来想了一下,就用类似前缀和法的方法来做吧。
虽然AC了,但是这种方法的时间复杂度是多少呢??O(n^2)?还望大神请教!

    vector<int> countOfSmallerNumber(vector<int> &A, vector<int> &queries) {
        vector<int> ret(queries.size(), 0);//这样声明的原因是 若A={},返回答案需要是{0,0,0..,0}。
        vector<int> small(10001, 0);//small[i]表示比i小的数的个数
        if (A.size() == 0) return ret;
        sort(A.begin(), A.end());//先进行 排序
        for (int i = 1; i < A.size(); i++) {
            if (A[i] == A[i-1]) {//如果后一个数等于前一个数
                small[A[i]] = small[A[i-1]];
                continue;
            }
            for (int j = A[i-1]+1; j <= A[i]; j++)//否则的话,A[i-1]+1~A[i]这个范围内的数的small值为其下标i。
                small[j] = i;
        }
        for (int i = 0; i < queries.size(); i++) {
            ret[i] = small[queries[i]];
        }
        return ret;
    } 

第二种:
排序+二分查找。代码比较简单,九章上给的样例也是利用了这种方法。
这里可用STL中的lower_bound(iterator_begin,iterator_end, val, cmp)返回第一个大于等于val的迭代器,cmp为自定义规则。

    vector<int> countOfSmallerNumber(vector<int> &A, vector<int> &queries) {
        vector<int> ret;
        sort(A.begin(), A.end());
        for (auto q:queries) { //C++ 11中定义的范围for语句,如果要修改需要 写成引用即&q
            int cnt = lower_bound(A.begin(), A.end(), q)-A.begin();
            ret.push_back(cnt);
        }
        return ret;
    }
    //下面是手写 二分查找(抄的九章,虽然二分查找思路不难,但是细节地方还是有很多需要注意的啊)
        int find(vector<int> &array, int val) {
        int l = 0;
        int r = array.size() - 1;
        int ans = -1;//这里ans的作用是啥呢?
        while(l <= r) {
            int mid = (l + r) >> 1;
            if(array[mid] < val) {
                l = mid + 1;
                ans = mid;
            }
            else //为啥不考虑array[mid] == val呢?
                r = mid - 1;
        }
        return ans + 1;//这里为啥又要返回ans+1呢???
    }

    vector<int> countOfSmallerNumber(vector<int> &A, vector<int> &queries) {
        // write your code here
        vector<int> result;
        sort(A.begin(), A.end());
        int qlen = queries.size();
        for(int i = 0; i < qlen; ++i)
            result.push_back(find(A, queries[i]));

        return result;
    }

第三种:利用线段树来做。
可是我的线段树做法竟然 超时了,真是百思不得其解。我的线段树想法是这样的,每个节点存储该区间内的最大数,和该区间内的个数。那么如果一个val > root->max,那么这个区间内的所有点都会小于这个val。
否则就划分到左右子树去找。复杂度O(n)+O(queries.size()*logn)。
下面代码超时 不知为何。

struct SegmentTree {
    int start, end;
    int max, cnt;
    SegmentTree *left, *right;
    SegmentTree(int start, int end, int max, int cnt) {
        this->start = start;
        this->end = end;
        this->max = max;
        this->cnt = cnt;
        this->left = this->right = NULL;
    }
};

SegmentTree *build(vector<int> &A, int start, int end) {
    if (start > end) return NULL;
    SegmentTree *root = new SegmentTree(start, end, A[start], 1);
    if (start == end) return root;
    int mid = (start+end)>>1;
    root->left = build(A, start, mid);
    root->right = build(A, mid+1, end);
    root->max = max(root->left->max, root->right->max);
    root->cnt = root->left->cnt + root->right->cnt;
    return root;
}

int query(SegmentTree *root, int val) {
    if (root == NULL) return 0;
    if (val > root->max) return root->cnt;
    int ans = 0;
    ans += query(root->left, val);
    ans += query(root->right, val);
    return ans;

}

class Solution {
public:
   /**
     * @param A: An integer array
     * @return: The number of element in the array that 
     *          are smaller that the given integer
     */
    vector<int> countOfSmallerNumber(vector<int> &A, vector<int> &queries) {
        // write your code here
        vector<int> ret(queries.size(), 0);
        if (A.size() == 0) return ret;
        SegmentTree *root = build(A, 0, A.size()-1);
        for (int i = 0; i < queries.size(); i++) {
            ret[i] = query(root, queries[i]);
        }
        return ret;
    }
};

更新:在看了别人的线段树做法之后,我发现了问题所在。由于我是以vector_A来建立线段树的,那么树高就和A的大小有关。观察发现,当A的size特别特别大的时候,就会超时。那么怎么样改进呢?注意到题目中这样一个条件:value from 0 to 10000。如果以这个区间来建线段树,同时声明一个count来代表其出现的次数,那么当查询某数x时,只要统计[0,x-1]内的所有count和就可以了。有了这个思路,立马就来动手实现一下!

struct SegmentTree {
    int start, end, cnt;
    SegmentTree *left, *right;
    SegmentTree(int start, int end) {
        this->start = start;
        this->end = end;
        this->cnt = 0;
        this->left = this->right = NULL;
    }
};

SegmentTree* build(int start, int end) {
    if (start > end) return NULL;
    SegmentTree *root = new SegmentTree(start, end);
    if (start < end) {
        int mid = (start+end)>>1;
        root->left = build(start, mid);
        root->right = build(mid+1, end);
    }
    return root;
}

int query(SegmentTree *root, int start, int end) {
    if (start > end || root == NULL) return 0;
    if (start <= root->start && end >= root->end) return root->cnt;
    int mid = (root->start + root->end) >> 1;
    int ans = 0;
    if (start <= mid) ans += query(root->left, start, end);
    if (mid+1 <= end) ans += query(root->right, start, end);
    return ans;
}

void modify(SegmentTree *root, int index, int value) {
    if (root == NULL) return;
    if (root->start == root->end && root->start == index) {
        root->cnt += value;
        return;
    }
    int mid = (root->start + root->end) >> 1;
    if (index <= mid)
        modify(root->left, index, value);
    else
        modify(root->right, index, value);
    root->cnt = root->left->cnt + root->right->cnt;
}

class Solution {
public:
   /**
     * @param A: An integer array
     * @return: The number of element in the array that 
     *          are smaller that the given integer
     */
    vector<int> countOfSmallerNumber(vector<int> &A, vector<int> &queries) {
        // write your code here
        vector<int> ret;
        SegmentTree *root = build(0, 10000);
        for (auto num : A) {
            modify(root, num, 1);
        }
        for (auto q : queries) {
            ret.push_back(query(root, 0, q-1));
        }
        return ret;
    }
};

终于AC!这种方法在数据量很大的时候,可以节省很多时间。

LintCode 207 区间求和
自己辛辛苦苦写了百行代码,打开九章一看??wtf??直接循环就可以??
我自己的。。。

struct SegmentTree {
    int start, end;
    long long sum;
    SegmentTree *left, *right;
    SegmentTree(int start, int end, long long sum) {
        this->start = start;
        this->end = end;
        this->sum = sum;
        this->left = this->right = NULL;
    }
};

SegmentTree* build(vector<int> &A, int start, int end) {
    if (start > end) return NULL;
    SegmentTree *root = new SegmentTree(start, end, A[start]);
    if (start == end) return root;
    int mid = (start+end)>>1;
    root->left = build(A, start, mid);
    root->right = build(A, mid+1, end);
    root->sum = root->left->sum + root->right->sum;
    return root;
}

long long query2(SegmentTree* root, int start, int end) {
    if (start > end || root == NULL) return 0;
    if (end < root->start || start > root->end) return 0;
    if (start <= root->start && end >= root->end) return root->sum;
    long long leftsum = query2(root->left, start, end);
    long long rightsum = query2(root->right, start, end);
    return leftsum + rightsum;
}

void modify2(SegmentTree* &root, int index, int value) {
    if (root == NULL) return;
    if (root->start == root->end) {
        if (root->start == index)
            root->sum = value;
        return;
    }
    int mid = (root->start + root->end) >> 1;
    if (index <= mid) modify2(root->left, index, value);
    else modify2(root->right, index, value);
    root->sum = root->left->sum + root->right->sum;
}
class Solution {
public:
    /* you may need to use some attributes here */


    /**
     * @param A: An integer vector
     */
    SegmentTree *root;
    Solution(vector<int> A) {
        // write your code here
        root = build(A, 0, A.size()-1);
    }

    /**
     * @param start, end: Indices
     * @return: The sum from start to end
     */
    long long query(int start, int end) {
        // write your code here
        return query2(root, start, end);
    }

    /**
     * @param index, value: modify A[index] to value.
     */
    void modify(int index, int value) {
        // write your code here
        return modify2(root, index, value);
    }
};

感受一下人与人之间的差距。。(九章上的)
不过我看来看去这个就是 最基础的暴力膜法。。竟然不会超时,可能九章是三种语言三种方法。

class Solution {
public:
    /* you may need to use some attributes here */
    vector<int> A;

    /**
     * @param A: An integer vector
     */
    Solution(vector<int> A) {
        // write your code here
        this->A = A;
    }

    /**
     * @param start, end: Indices
     * @return: The sum from start to end
     */
    long long query(int start, int end) {
        // write your code here
        long long sum = 0;
        int len = A.size();
        for (int i = start; i <= end && i < len; ++i)
            sum += this->A[i];
        return sum;
    }

    /**
     * @param index, value: modify A[index] to value.
     */
    void modify(int index, int value) {
        // write your code here
        //if (index < this->A.size())
        this->A[index] = value;
    }
};

LintCode 249 统计前面比自己小的个数
既然是在线段树专题下,那么首先当然是用线段树来解决啦!注意和前面那道题一样,以[0, 10000]区间建树。按照我最初的思路的话仍然会超时- -。

struct SegmentTree {
    int start, end, cnt;
    SegmentTree *left, *right;
    SegmentTree(int start, int end) {
        this->start = start;
        this->end = end;
        this->cnt = 0;
        this->left = this->right = NULL;
    }
};

SegmentTree* build(int start, int end) {
    if (start > end) return NULL;
    SegmentTree *root = new SegmentTree(start, end);
    if (start == end) return root;
    int mid = (start+end) >> 1;
    root->left = build(start, mid);
    root->right = build(mid+1, end);
    return root;
}

int query(SegmentTree *root, int start, int end) {
    if (start > end || root == NULL) return 0;

    if (start <= root->start && end >= root->end) 
        return root->cnt;

    int mid = (root->start + root->end) >> 1;
    int ans = 0;
    if (start <= mid) ans += query(root->left, start, end);
    if (mid+1 <= end) ans += query(root->right, start, end);
    return ans;
}

void modify(SegmentTree *root, int index, int value) {
    if (root == NULL) return;
    if (root->start == root->end && root->start == index) {
        root->cnt += value;
        return;
    }
    int mid = (root->start + root->end) >> 1;
    if (index <= mid)
        modify(root->left, index, value);
    else
        modify(root->right, index, value);
    root->cnt = root->left->cnt + root->right->cnt;
}
class Solution {
public:
   /**
     * @param A: An integer array
     * @return: Count the number of element before this element 'ai' is 
     *          smaller than it and return count number array
     */
    vector<int> countOfSmallerNumberII(vector<int> &A) {
        // write your code here
        vector<int> ret;
        SegmentTree *root = build(0, 10000);
        for (auto a : A) {
            modify(root, a, 1);
            ret.push_back(query(root, 0, a-1));//由于是统计在自己之前的,所以边修改边查询。如果修改完再查询就是整个array内小于自己的了。
        }
        return ret;
    }
};

这里再说一种解法,用 树状数组。对于树状数组我也只有稍微了解,曾经在做PAT 1057 的时候用了这种方法,可以看一下这篇文章的后半部分:http://blog.csdn.net/qq_32108329/article/details/60869088 用树状数组求中位数的。
更详细的区别可以自行百度。
树状数组两个基本操作:
1.修改数组上的某处值,使其增加num,同时还要循环更新该节点的父亲节点的值。
2.求1~k区间的和。
所以这道题可以用这种思路,区间0~10000,同样是数组代表出现的次数,求区间和就相当于统计了小于k的个数。

    int c[20000]; //这里如果大小开为10001,那么就会获得worng answer,why?

    //c[i] = A[i]+A[i-1]+..+A[i-2^k+1],其中k是数i的二进制末尾0的个数,2^k可用下面的函数求出。是不是有一种前缀和的感觉??
    int lowbit(int x) { 
        return x&(-x);
    }
    void add(int k, int num, int n) {
        while (k <= n) {
            c[k] += num;
            k += lowbit(k);//修改其父亲节点
        }
    }

    int getSum(int k) {
        if (k <= 0)
            return 0;
        int sum = 0;
        while (k > 0) {
            sum += c[k];
            k -= lowbit(k);
        }
        return sum;
    }
    vector<int> countOfSmallerNumberII(vector<int> &A) {
        // write your code here
        vector<int> ret;
        for (auto a : A) {
            ret.push_back(getSum(a));//先求小于a的个数,由于是getSum(k)求的是1..k的和。所以这里统一向右移动一位。
            add(a+1, 1, 10001);//然后使数a+1出现的次数+1。
        }
        return ret;
    }

呼~大概花了三天的时间终于把这10道题做完了,虽然估计过段时间就忘得没有影了。
总结一下:
1.线段树最重要的操作:三个,build, query, modify。常用于区间查询问题。可以以O(logn)实现查询与修改操作。O(n)的建树操作。掌握其三种基本操作的实现。
2.做几道应用题总是感觉很憋屈,我特喵的也不知道为什么。。。
3.所有题都值得回顾总结+二刷。
4.当然线段是还有区间修改更复杂的操作,但是由于这些题目都没涉及,所以我懒得看了,以后遇到了类似的题目了再说吧。
5.最后提一下 树状数组这个概念,以后遇到了能想起来在这里遇到。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值