PAT (Advanced Level) Practice 1081~1100

1081 Rational Sum

一道比较简单的考查分数运算的题目。

注意点:

  • 负数不用特殊处理,只需将分子变为负数即可;
  • 用 long long 来定义所以的整数类型;
  • 每一步加法做完之后要约分;
  • 要计算分子分母绝对值的最大公约数;
  • 在 PAT 里,abs 函数不在头文件 cmath里,而是 algorithm 里。

关于分数运算的知识,可以学习 PAT OJ 刷题必备知识总结的第25点,写的很易懂详细。

#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll; // 记 long long 为 ll

ll gcd(ll a, ll b) // 求 a 与 b 的最大公约数
{
    return b == 0 ? a : gcd(b, a % b);
}

struct Fraction // 分数的结构体
{
    ll up, down; //up 为分子,down 为分母
};

Fraction reduction(Fraction result) // 该函数实现分数的化简
{
    if (result.down < 0) // 分子为负数,分子分母变相反数
    {
        result.up = -result.up;
        result.down = -result.down;
    }
    else if (result.up == 0) // 分子为0,分母变1
        result.down = 1;
    else // 约分
    {
        int d = gcd(abs(result.up), abs(result.down)); // 求分子分母的最大公约数
        result.up /= d; // 约去最大公约数
        result.down /= d;
    }

    return result;
}

Fraction add(Fraction f1, Fraction f2) // 分数加法
{
    Fraction result;
    result.up = f1.up * f2.down + f2.up * f1.down; // 分数和的分子
    result.down = f1.down * f2.down; // 分数和的分母
    return reduction(result); // 返回化简后的分数
}

void showResult(Fraction r) // 输出分数 r
{
    r = reduction(r); // 化简

    if (r.down == 1) // 整数
        printf("%lld\n", r.up);
    else if (abs(r.up) > r.down) // 假分数
        printf("%lld %lld/%lld\n", r.up / r.down, abs(r.up) % r.down, r.down);
    else
        printf("%lld/%lld\n", r.up, r.down); // 真分数
}

int main()
{
    int n;
        scanf("%d", &n);
    Fraction temp, sum;
    sum.up = 0, sum.down = 1; // 初始化
    for (int i = 0; i < n; ++i)
    {
        scanf("%lld/%lld", &temp.up, &temp.down);
        sum = add(sum, temp); // sum 加上 temp
    }
    showResult(sum); // 输出结果

    return 0;
}


1082


1083 List Grades

题意:

输入第一行给出学生总数 N。随后 N 行记录,每行记录包括学生的姓名,ID 以及成绩,姓名和成绩都是不超过10个字符且不含空格的字符串。最后给出两个整数 grade1grade2,表示一个既定的区间 [grade1, grade2],要求你按成绩非递减的顺序输出所有成绩在区间内的学生姓名以及 ID,每个学生占一行。

思路:

题目比较简单。定义结构体 student 保存学生的姓名 nameid 以及成绩 grade。定义 vector 数组 stus 保存所有的学生记录,输入结束后定义 vector 数组 list 保存所有成绩在区间内的学生记录。后面就正常排序然后输出即可。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

struct student {
	int grade;
	string name, id;
};

bool cmp(student a, student b) { return a.grade > b.grade; }

int main()
{
	int N, grade1, grade2;
	cin >> N;
	vector<student> stus(N);
	for (int i = 0; i < N; ++i)
		cin >> stus[i].name >> stus[i].id >> stus[i].grade;
	cin >> grade1 >> grade2;
	vector<student> list;
	for (auto i : stus)
		if (grade1 <= i.grade && i.grade <= grade2)
			list.push_back(i);	// 成绩在区间内的记录放到 list 中
	sort(list.begin(), list.end(), cmp);
	if (list.size())
		for (auto i : list) cout << i.name << " " << i.id << endl;
	else cout << "NONE" << endl;

    return 0;
}

1084 Broken Keyboard

此题为 PAT (Basic Level) Practice 1029 旧键盘 的英文版,题解可看 PAT (Basic Level) Practice 1023~1044


1085 Perfect Sequence

此题为 PAT (Basic Level) Practice 1030 完美数列 的英文版,点击链接进入原题,题解可看 PAT (Basic Level) Practice 1023~1044


1086 Tree Traversals Again

题意:

输入第一行一个整数 N,表示树中结点的总数。接下来 2N 行,每行代表一个入栈或出栈的操作,入栈操作后面会同时输入一个整数,代表结点的值。根据出栈得到的值序列,就是二叉树的中序遍历序列。现在要求你根据该入栈、出栈的操作,求出该二叉树的后序遍历序列

思路:

这里假定你知道如何通过栈来求一棵二叉树的中序遍历序列。对于任何一棵二叉树来说,通过非递归的方法寻找中序遍历序列时,其实结点入栈的顺序刚好就是其前序遍历的序列。因此这一题实际上是根据一棵二叉树的前序序列和中序序列,来求出其后序序列。

那么主要的做法就是,输入的时候,根据入栈、出栈的顺序,得到前序序列和中序序列,将其分别保存在数组 prein 中。调用 PostOrder 函数来求解后序序列。在这里,我们不去定义结点的结构体,因为我们得到的都是结点的遍历序列,所以通过结构体结点操作反而会更麻烦。

先简单讲讲如何由前序和中序序列得到一棵二叉树的结构:前序序列的第一个数 x 一定是整棵树的根结点,那么在中序序列中,找到 x,在 x 的左侧就是左子树的中序序列,在 x 的右侧就是右子树的中序序列。假设 x 左侧有 y 个数(即左子树右 y 个结点),那么在前序序列中,从 x 往右数 y 个数,就是左子树的前序序列,再往后一直到序列末尾都是右子树的前序序列。对于左子树的前序序列的第一个数,就是左子树的根结点,再去左子树的中序序列找到该数,对右子树的前序序列和中序序列重复以上的步骤,不断递归,最终就能确定出整个二叉树的结构了。

既然大概过程知道了,那么核心思路就来了。上述过程,实际上就是从上往下寻找根结点的过程。二叉树的后序遍历遵循左孩子、右孩子、根结点的先后规则。所以在寻找根结点的过程中,将根结点、右孩子、左孩子依次放入 post 数组中(这同时就提示了我们递归的顺序),最后倒着打印 post 中所有的数,得到的就是二叉树的后序遍历序列。

#include <iostream>
#include <vector>
#include <stack>
using namespace std;

vector<int> pre, in, post;
stack<int> st;

void PreInToTree(int root, int left, int right)
{
    if(left > right) return ;   // 区间长度为0时返回
    int i = left;               // i 保存中序序列下根结点的位置(索引)
    while(i < right && in[i] != pre[root]) ++i;
    post.push_back(pre[root]);
    PreInToTree(root + i - left + 1, i + 1, right); // 递归处理中序序列右子树
    PreInToTree(root + 1, left, i - 1);             // 递归处理中序序列左子树
}

int main()
{
    int N, n;
    cin >> N;
    string op;
    for (int i = 0; i < 2 * N; ++i)
    {
        cin >> op;
        if (op.size() == 4)         // 操作字符串长度为4,说明是 Push
        {
            cin >> n;               // 读取后面的整数
            pre.push_back(n);       // 放入序列 pre
            st.push(n);             // 入栈
        }
        else
        {
            in.push_back(st.top()); // 栈顶元素入序列 in
            st.pop();               // 栈顶元素出栈
        }
    }
    PreInToTree(0, 0, N - 1);
    cout << post[N - 1];
    for (int i = N - 2; i >= 0; --i)
        cout << " " << post[i];
    
    return 0;
}

1087 All Roads Lead to Rome

题意:

输入第一行是两个正整数和一个字符串,第一个是城市的个数 N,第二个是所有城市中的道路数量 K,最后一个是起始城市的名称 startcity。随后是 N - 1 行城市的幸福度,每行第一个数据是字符串 city1,表示城市的名称,然后是该城市的幸福度。随后是 K 行道路数据,每一行前两个字符串 city1city2 表示道路两端的城市名称,第三个数据是一个正整数,表示这条道路的花费。题目规定了每个城市的名称由三个大写字母构成。现在要求你找出从起始城市到名称为 ROM 的城市中花费最短的路径,如果不止一条,就选择路径上幸福度总和最高的城市,如果还是不止一条,就选择路径上幸福度平均值最高的,题目保证了这样的路径是唯一的。

思路:

迪杰斯特拉算法深度优先搜索遍历解决。

不妨将幸福度当作每个城市结点的权重,如何求出最少花费路径很好解决,要找出平均值最低只需要在权重相等的情况下,选择路径上城市数量最少的一条即可。

c[i] 表示由起始点到点 i 的最小花费,weight[i] 表示点 i 的权重,G[u][v] 表示从点 u 到点 v 的花费(值为0时表明 u, v 之间没有道路),n[i] 表示到达点 i 的最少花费的路径有多少条,pre[i] 保存城市 i 的所有前驱节点,visit[i] 表示点 i 已被访问过。

因为城市结点数量不多,所以完全可以使用哈希映射。定义 stiits 来分别将城市名称映射到城市编号(包括反过来),因此在读取每个城市的权重时,就可以依次给它们编好号。在找到所有最少花费路径后,数组 n 中就保存了从起始城市,到达每个城市结点的最少花费路径的数量。

DFS 函数中,先从终点 ROM 开始遍历,访问它所有的前驱结点,w 表示每进到一层时路径的权重之和,n 表示当前路径上已经有多少个城市了,当访问到起始城市(编号为0)时,就判断 w 是否更大,w 相等的情况下判断 n 是否更小。


1088 Rational Arithmetic

题意:

给出两个分数(可能有负分数),求它们的加法、减法、乘法、除法的计算式。如果是假分数,则按带分数的形式输出;如果是整数,则输出整数;否则,输出真分数;如果做除法时除数为0,那么应当输出 “Inf"。

思路:

这是一道比较常规的分数四则运算的题目,只不过加减乘除都有所涉及,所以代码量稍微大一点。详情可以参考PAT OJ 刷题必备知识总结的第23、25点内容。

注意点:

  • 负数无需特殊处理,只需当作分子为负数的分数即可。
  • 分母相乘时,最大可以达到 long long,因此要用 long long 定义分子分母。
  • 计算最大公约数时,要注意是计算分子分母绝对值的公约数,否则下面的数据会错误。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;

ll gcd(ll a, ll b)
{ // 求 a 与 b 的最大公约数
    return !b ? a : gcd(b, a % b);
}

struct Fraction // 分数结构体
{
    ll up, down; // 分别表示分子和分母
}a, b;

Fraction reduction(Fraction result) // 化简
{
    if (result.down < 0) // 分子为负,分子分母变相反数
    {
        result.up = -result.up;
        result.down = -result.down;
    }
    if (result.up == 0) // 分子为0,分母变1
        result.down = 1;
    else // 分子不为0,约分
    {
        int d = gcd(abs(result.up), abs(result.down)); // 求分子分母的最大公约数
        result.up /= d; // 约去最大公约数
        result.down /= d;
    }

    return result;
}

Fraction add(Fraction f1, Fraction f2) // 分数加法
{
    Fraction result;
    result.up = f1.up * f2.down + f2.up * f1.down;
    result.down = f1.down * f2.down;
    return reduction(result);
}

Fraction minu(Fraction f1, Fraction f2) // 分数减法
{
    Fraction result;
    result.up = f1.up * f2.down - f2.up * f1.down;
    result.down = f1.down * f2.down;
    return reduction(result);
}

Fraction multiply(Fraction f1, Fraction f2) // 分数乘法
{
    Fraction result;
    result.up = f1.up * f2.up;
    result.down = f1.down * f2.down;
    return reduction(result);
}

Fraction divide(Fraction f1, Fraction f2) // 分数除法
{
    Fraction result;
    result.up = f1.up * f2.down;
    result.down = f1.down * f2.up;
    return reduction(result);
}


void showResult(Fraction r)
{
    r = reduction(r); // 化简

    if (r.up < 0) printf("(");
    if (r.down == 1) // 整数
        printf("%lld", r.up);
    else if (abs(r.up) > r.down) // 假分数
        printf("%lld %lld/%lld", r.up / r.down, abs(r.up) % r.down, r.down);
    else
        printf("%lld/%lld", r.up, r.down); // 真分数
    if (r.up < 0) printf(")");
}

int main()
{
    scanf("%lld/%lld %lld/%lld", &a.up, &a.down, &b.up, &b.down);
    // 加法
    showResult(a);
    printf(" + ");
    showResult(b);
    printf(" = ");
    showResult(add(a, b));
    printf("\n");
    // 减法
    showResult(a);
    printf(" - ");
    showResult(b);
    printf(" = ");
    showResult(minu(a, b));
    printf("\n");
    // 乘法
    showResult(a);
    printf(" * ");
    showResult(b);
    printf(" = ");
    showResult(multiply(a, b));
    printf("\n");
    // 除法
    showResult(a);
    printf(" / ");
    showResult(b);
    printf(" = ");
    if (b.up == 0) printf("Inf");
    else showResult(divide(a, b));
    printf("\n");

    return 0;
}


1089


1090 Highest Price in Supply Chain

题意:

输入第一行是三个数,第一个是正整数 N,表明树中所有成员(结点)的个数(编号 0 ~ N - 1),第二个数是浮点数 price,表明产品单价,第三个是浮点数 rise,表明增长率。随后一行是 N 个整数,第 i 个整数表明其是编号 i 结点的经销商,也就是双亲结点。题目规定了供应商(根结点)的编号为-1,产品单价每往下一层就增加 rise%。只有零售商(叶结点)能直接出售产品。现在要求你计算出最大的供应层数。并打印该层产品的单价,以及该层叶结点的数量。

思路:

深度优先搜索遍历解决。

根据题目的输入用例,画出树的结构图后,其实可以发现,如果对层数从0开始编号,编号为1的层(也就是根结点的孩子结点所在的那一层)拿到产品的单价就是题目输入的原价,往下每一层就要上涨 rise%。可以这么做,对整棵树进行深度优先搜索遍历,统计每一层结点的个数,将其保存在数组 cnt 中。数组 childs[i] 保存结点 i 的所有孩子结点的编号。

对整棵树进行深度优先搜索遍历,每进入一个结点 root,就对 cnt[depth] 进行自增操作,表明 depth 层结点个数加1。更新最大层数,然后对其所有孩子都进行深度优先搜索遍历。

注意:

  • 在第二行输入中会输入-1。但是数组的索引没有-1,所以我们需要将其保存在数组 childs[N] 中。
  • rise 的单位是 %,所以要将其除以100后再加上1才能得到增长率。
  • 因为层数编号是从0开始的,所以根结点的下面一层是1,如果直接计算 pow(1 + rise / 100, maxdepth) 就会多算一层,所以要对 maxdepth 进行减1。
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

int maxdepth = -1;
vector<int> childs[100005], cnt(100005);

void DFS(int root, int depth)
{
    ++cnt[depth];   // 第 depth 层结点个数加1
    maxdepth = depth > maxdepth ? depth : maxdepth; // 更新最大层数
    for (int i = 0; i < childs[root].size(); ++i)   // 遍历结点 root 的所有孩子结点
        DFS(childs[root][i], depth + 1);
}

int main()
{
    int N, n;
    double price, rise;
    scanf("%d%lf%lf", &N, &price, &rise);
    for (int i = 0; i < N; ++i)
    {
        scanf("%d", &n);
        if (n == -1) childs[N].push_back(i);    // 如果输入-1,表明其是根结点,将编号 i 放入数组 childs[N] 中
        else childs[n].push_back(i);            // 其他情况就放入数组 childs[n]
    }

    DFS(N, 0);   // 进行深度优先遍历确定最大销售价格
    printf("%.2lf %d", price * pow(1 + rise / 100, maxdepth - 1), cnt[maxdepth]);

    return 0;
}

1091 Acute Stroke

题意:

输入第一行是四个整数:行数 M,列数 N,切片数 L,以及阈值 T。随后 M * L 行数据,每一行都是各个像素点的像素值(0或1)。要求你输出所有阈值不小于 T 的像素块中像素的个数。

根据题目的意思,其实就是给你一个三维矩阵,三个坐标对应空间中的一个像素点。其中,像素值为1且连在一起的像素点所构成的区域,称为急性脑卒中核心区域,也就是像素块。如何定义是否连在一起呢?上下左右前后挨着的就称为连在一起的像素点。然后在这些像素块中,只有像素个数 >= T 的像素块才满足要求。你需要计算所有满足要求的像素块中,像素的个数,也即像素值为1的像素点的个数。

思路:

广度优先搜索(BFS) 解决。

首先需要定义结构体 Pixel 保存一个像素点的坐标,然后定义数组 arr 来保存三维矩阵中的所有像素点的值。bool 数组 inq(in queue)用于标记像素点是否入过队列。数组 X、Y、Z 保存每个像素点在各个方向上的坐标偏移

因为题目是将整个三维矩阵横切的,所以得到的是由上至下的 L 个切片矩阵,每个矩阵的大小为 M * N。因此在读取数据的时候先遍历矩阵的数量,再遍历每个矩阵。因此是 L,M,N 的循环顺序,在进行 BFS 时也是先遍历数量。

读取完数据后,枚举三维矩阵的每一个像素点 (x, y, z),对符合要求的像素点进行广度优先搜索遍。在 BFS 中,首先将传入的坐标参数保存到像素点 pixel 中,然后放入队列 Q。循环以队列为空作为结束条件。

每一次循环,都取出队首的像素点,因为只要入队的都必定是像素值为1的像素(后面会解释),所以 cnt 要加1。然后循环枚举它六个方向上的像素点,这个循环很好理解:对于空间中的任何一个点来说,对于它上下左右前后相邻的像素点,仅仅只会变动一个维度的坐标,所以 X、Y、Z 中,在同一个维度下(或者说下标相同时)只会有一个数组的值不为0。所以我们将对应数组的值加到相应的坐标上,就能得到它的相邻像素点的坐标。将这个坐标传入 Judge 函数,来判断是否可以将该像素点入队。

Judge 函数首先会判断坐标是否合法,其次再看该坐标对应的像素值是否为1,最后再看该坐标是否已经入过队列了,只要满足以上任何一种情况就会返回 false,否则返回 true。返回 true 后,就将坐标值保存到像素点 pixel 中,然后放入队列 Q。最后要将其对应的 inq 中的值设置为 true,表明它已经入过队列了,这样在访问后面像素点的过程中,就不会将该像素点重复入队。

退出循环后,若 cnt 不小于阈值 T,就将 cnt 的值返回;否则返回0,表明当前 BFS 的像素块不满足题目要求。由上述的过程也可以知道,我们并不会 BFS 三维矩阵中的每一个像素点,而是只枚举像素值为1且没有入过队的像素点

#include <iostream>
#include <queue>
using namespace std;

struct Pixel {
    int x, y, z;    // 像素点的坐标
} pixel;


int M, N, L, T, arr[1300][130][70];
bool inq[1300][130][70] = { false };
int X[6] = { 0, 0, 1, -1, 0, 0 };
int Y[6] = { 1, -1, 0, 0, 0, 0 };
int Z[6] = { 0, 0, 0, 0, 1, -1 };

bool Judge(int x, int y, int z) // 判断坐标 (x, y, z) 是否需要入队
{
    if (x < 0 || x >= M || y < 0 || y >= N || z < 0 || z >= L) return false;
    if (arr[x][y][z] == 0 || inq[x][y][z] == true) return false;
    return true;
}
int BFS(int x, int y, int z)
{
    int cnt = 0;            // cnt 统计每个像素块中像素的个数
    pixel.x = x, pixel.y = y, pixel.z = z;
    queue<Pixel> Q;         // 队列 Q 保存每一个像素值为1的像素点
    Q.push(pixel);          // 像素点 pixel 入队
    inq[x][y][z] = true;    // 标记坐标 (x, y, z) 已经访问过
    while (!Q.empty())
    {
        Pixel top = Q.front();      // 取出队首元素
        Q.pop();                    // 队首元素出队
        ++cnt;                      // 块的个数加1
        for (int i = 0; i < 6; ++i) // 循环6次,得到6个方向的增量
        {
            int newx = top.x + X[i], newy = top.y + Y[i], newz = top.z + Z[i];
            if (Judge(newx, newy, newz))        // 判断新的像素点是否需要访问
            {
                pixel.x = newx, pixel.y = newy, pixel.z = newz;
                Q.push(pixel);
                inq[newx][newy][newz] = true;   // 设置为 true 表明已经入过队
            }
        }
    }
    if (cnt >= T) return cnt;
    else return 0;
}

int main()
{
    cin >> M >> N >> L >> T;
    for (int k = 0; k < L; ++k)         // 先遍历矩阵的数量
        for (int i = 0; i < M; ++i)     // 再遍历每一个矩阵
            for (int j = 0; j < N; ++j)
                cin >> arr[i][j][k];

    int ans = 0;
    for (int k = 0; k < L; ++k)
    {
        for (int i = 0; i < M; ++i)
        {
            for (int j = 0; j < N; ++j)
                if (arr[i][j][k] == 1 && inq[i][j][k] == false)
                    ans += BFS(i, j, k);    // 如果像素值为1且没有入过队,就执行 BFS
        }

    }
    cout << ans;

    return 0;
}

1092 To Buy or Not to Buy

此题为 PAT (Basic Level) Practice 1039 到底买不买 的英文版,题解可看 PAT (Basic Level) Practice 1023~1044


1093 Count PAT’s

此题为 PAT (Basic Level) Practice 1040 有几个PAT 的英文版,点击链接进入原题,题解可看 PAT (Basic Level) Practice 1023~1044


1094 The Largest Generation

题意:

输入第一行给出两个正整数 NM,N 是树中结点总数(编号 01 ~ N,N < 100),M 是树中有孩子的结点的个数。随后 N 行输入,每一行第一个整数是结点的编号 memberid,第二个整数是该结点的孩子结点个数 K,然后是 K 个整数 childid,表明其孩子结点的编号。现在要求你计算该树哪一层结点个数最多,打印结点个数和层数,层数从1开始编号,且规定了根结点的编号为01。

思路:

深度优先搜索(DFS) 解决。

定义数组的数组 childs,数组 childs[i] 保存结点 i 的孩子结点的编号,population 数组保存每一层结点个数,定义长度为100是防止极端的情况(一层只有一个结点,最多有99层)。读取数据之后,对树进行深度优先搜索遍历。对于每个结点,假设其在 depth 层,则将其孩子结点的个数加到 population[depth + 1] 上,表明第 depth + 1 的结点个数增加了。然后判断其是否超过了最大值 maxg,超过了就更新 maxg 以及 maxl。

注意:

  • 测试点1是另一种极端情况:只有一个结点。此时应该输出 1 1,所以 maxg 和 maxl 的初值都为1,N 为正整数这一点保证了树中至少有一个结点。
#include <iostream>
#include <vector>
using namespace std;

vector<int> childs[100], population(100);
int maxg = 1, maxl = 1;

void DFS(int root, int depth)
{
    population[depth + 1] += childs[root].size();   // 将结点 root 的孩子数量加到其下一层结点个数中
    if (population[depth + 1] > maxg)               // 如果下一层的结点数量超过了最大值
    {
        maxg = population[depth + 1];   // 更新最大结点数量
        maxl = depth + 1;               // 更新所在是第几层
    }
    for (int i = 0; i < childs[root].size(); ++i)   // 对每个孩子递归调用 DFS
        DFS(childs[root][i], depth + 1);
}

int main()
{
    int N, M, memberid, K, childid;
    cin >> N >> M;
    while (M--)
    {
        cin >> memberid >> K;
        while (K--)
        {
            cin >> childid;
            childs[memberid].push_back(childid);    // 将编号 childid 放入数组 childs[memberid] 中
        }
    }
    DFS(1, 1);  // 第一层、编号为1的结点调用深度优先搜索遍历
    cout << maxg << " " << maxl;

    return 0;
}

1095 Cars on Campus

题意:

输入第一行是两个整数:总的记录数 N 和 查询次数 K。随后 N 行记录:七位由大写字母或者数字组成的字符串,hour:minute:second 格式的时间,以及表示车子是进校园还是出校园的状态。然后是 K 行查询,同样是 hour:minute:second 格式的时间。

现在要求你输出在每个查询时间之前,已经进入到校园的车的数量(暂且认为它们进了学校就停车了),最后一行还要输入这一天内,在校园内停车时间最长的车的车牌号,如果有多辆车并列,就按照车牌号升序来输出。

要注意的是,同一辆车的任意两条记录,只有在时间顺序上是先后,且前者为 in 后者为 out 才能算作一次有效的停车。而且输入默认所有记录都是在同一天内发生的。

思路:

定义结构体 record 保存每一辆车的每一条记录,其中 plate 保存车牌号,time 保存从00:00:00开始到记录时间之间经过的秒数,status 保存车子的状态。

定义 vector 数组 records 保存所有的记录,vRecords 数组保存所有有效停车对应的相邻的两条记录。定义 map 映射 parkTime,来根据每辆车的车牌号保存它一天内的总停车时间。

随后定义 cmp 函数来规定指定规则:先按车牌号排序,再按时间先后排序。

输入数据的时候,先将 hour:minute:second 格式的时间转化为秒数,然后根据其状态是 in 还是 out 来标记 status 的值,如果是 in 则为 1,如果是 out 则为0。输入完后,records 中就保存了所有的记录,将其按 cmp 函数排序后,所有记录就是先车牌号字典序、后按时间由小到大排好了的。

此时用 i 枚举 records 中的每一条记录,如果相邻两条记录满足是同一辆车的,且前者 status 为1,后者为0的两条记录,就将其放入 vRecords 中,同时计算这两条记录的时间间隔,将其加到 parkTime[records[i].plate] 上。ltime 保存最长的停车时间,如果 parkTime[records[i].plate] 比 ltime 大,就将其值保存在 ltime 中。循环结束后,vRecords 中保存的就是每一辆车每次有效停车的两条记录,也因此 vRecords 数组的长度一定是偶数。

K 次查询时,同样要先计算秒数 time,然后用 it 枚举 vRecords 中的每一条记录,如果 it->time <= time < (it + 1)->time,就表明这个时间点,车辆 it 停在了校园内,就将计数值 cnt 加1。在每一次循环后,it 都进行加2,是因为 vRecords 数组的长度是偶数,不断自增2一定是到达尾迭代器 vRecords.end(),所以是以尾迭代器作为循环的条件。 循环结束后输出 cnt。

经验:

  • 如果用 for (auto i : container) 去遍历一个容器,那么 i 只是一个不会被改变的量(注意不是常量)!对于 i 下面的任何成员的任何修改都是有效的,但是修改的都是临时的副本,在退出 for 循环后就会全部失效。也就是说你对 i 下任何成员所做的任何修改,都不会影响到 i 本身。
  • 对于这种存在许多条记录,其中既有有效的,也有无效的,都可以采取这个思路:先保存全部记录,再筛选出有效的记录。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <map>
using namespace std;

struct record {
	char plate[10];
	int time, status;
};
vector<record> records, vRecords;		// 有效记录数组
map<string, int> parkTime;	// 根据车牌查询总停车时长

bool cmp(record &a, record &b) { return strcmp(a.plate, b.plate) == 0 ? a.time < b.time : strcmp(a.plate, b.plate) < 0; }

int main()
{
	int N, K, hour, minute, second, ltime = -1;
	char s1[10], s2[5];
	scanf("%d%d", &N, &K);
	records.resize(N);
	for (int i = 0; i < N; ++i)
	{
		scanf("%s %d:%d:%d %s", records[i].plate, &hour, &minute, &second, s2);
		records[i].time = hour * 3600 + minute * 60 + second;	// 转为秒数保存
		records[i].status = strcmp(s2, "in") == 0 ? 1 : 0;		// status 为1表明进 "in"
	}
	sort(records.begin(), records.end(), cmp);	// 先按车牌号排序,再按时间先后排序

	for (int i = 0; i < N - 1; ++i)
	{
		if (strcmp(records[i].plate, records[i + 1].plate) == 0 && records[i].status && !records[i + 1].status)
		{
			vRecords.push_back(records[i]);		// 将相邻两个有效记录放入 vRecords
			vRecords.push_back(records[i + 1]);
			parkTime[records[i].plate] += records[i + 1].time - records[i].time;		// 新键值对的初始值都为0,所以不用初始化
			if (parkTime[records[i].plate] > ltime) ltime = parkTime[records[i].plate];	// 更新最长停车时间
		}
	}
	
	while (K--)
	{
		int time, cnt = 0;
		scanf("%d:%d:%d", &hour, &minute, &second);	// 查询时间
		time = hour * 3600 + minute * 60 + second;
		for (auto it = vRecords.begin(); it != vRecords.end(); it += 2)
			if (it->time <= time && time < (it + 1)->time) ++cnt;	// 更新该时间点在校园的停车数量
		printf("%d\n", cnt);
	}

	for (auto i : parkTime)	// 打印停车时长最长的车的车牌号
		if (i.second == ltime) printf("%s ", i.first.c_str());
	printf("%02d:%02d:%02d", ltime / 3600, ltime % 3600 / 60, ltime % 3600 % 60);
	
    return 0;
}

1096 Consecutive Factors

题意:

输入第一行给出一个正整数 N 1 < N < 2 31 1 < N < 2^{31} 1<N<231),要求你输出它的最长连续因子序列的长度以及连续因子序列。比如 630 既可以表示为 2 × 2 × 3 × 3 × 4 × 5 2\times 2\times 3\times 3\times 4\times 5 2×2×3×3×4×5 也可以表示成 3 × 4 × 5 × 6 3\times 4\times 5\times 6 3×4×5×6,其中最长的连续因子序列就是 4 × 5 × 6 4\times 5\times 6 4×5×6。而且题目要求必须是长度最长,但是乘积最小的连续因子序列。

思路:

定义 maxlen 保存最大的长度,firstnum 保存连续因子序列的第一个数字。定义 sqr 保存 N \sqrt N N ,然后在外层循环从2枚举到 N \sqrt N N 。因为任何一个整数 N 不会被除自己以外的大于 N \sqrt N N 的整数整除,因此只需要枚举到 N \sqrt N N 即可。

每一次内层循环前,定义 temp 保存连续因子序列的乘积,j 表示当前的乘数。如果 temp 能整除 N(或者说 N 能被 temp 整除)就检查当前的序列是否更长,更长的话就更新 maxlen 和 firstnum,然后令 j 加1来获取下一个连续整数;如果不能整除 N,就退出内层循环。

最后,如果 maxlen 为0,表明 N 是个质数,那么它的最长连续因子序列就是它本身,所以要输出 N。如果不为0,就按照题目要求的格式,从 firstnum 开始输出连续因子序列即可。

#include <iostream>
#include <cmath>
using namespace std;

int main()
{
    int N, maxlen = 0, firstnum = 0;    // maxlen 保存最大长度,firstnum 保存连续序列的第一个数字
    cin >> N;
    int sqr = sqrt(1.0 * N);
    for (int i = 2; i <= sqr; ++i)
    {
        int temp = 1, j = i;            // temp 保存连续序列的乘积
        while (1)
        {
            temp *= j;                  // 计算当前乘积
            if (N % temp != 0) break;   // 不能整除 N,结束计算
            if (j - i + 1 > maxlen)     // 找到了更长的序列
            {
                firstnum = i;           // 更新序列首数字
                maxlen = j - i + 1;     // 更新最大长度
            }
            ++j;    // j 加1,表示下一个整数
        }
    }
    if (!maxlen) cout << "1\n" << N;
    else
    {
        cout << maxlen << "\n" << firstnum;
        for (int i = 1; i < maxlen; ++i)
            cout << "*" << firstnum + i;
    }

    return 0;
}

1097 Deduplication on a Linked List

题意:
给出单链表的头结点地址、结点总数 N。接下来 N 行条数据,每行给出结点的地址、数据以及下一个结点的地址。要求只保留绝对值第一次出现的结点,而去掉链表中绝对值重复的结点。最后按照地址、数据以及下一个结点的地址输出非重值链表,以及重值结点。

思路:
首先给出了结点的地址值且范围较小,故可以考虑用静态链表来处理。我的就基本思路就是用一个 bool 数组 check 来记录结点值的绝对值是否出现过。然后单链表将整个链表拆分成两条链表:重值链表和非重值链表。将绝对值第一次出现的链表链接到重值链表上,将绝对值已经出现过的连接到非重值链表上。最后按照顺序分别输出两条链表上的结点。

  1. 定义 bool 数组。 check 记录结点绝对值是否出现过,初始化为 false;
  2. 输入结点的数据。用 first 和 second 分别表示重值和非重值链表的第一个结点的地址,q 指向枚举当中的结点,p 指向它的前驱,r 指向非重值链表的尾结点,初值为-1。
  3. 枚举单链表。根据 check[x] 的值来判断是否为重值结点,枚举的过程手动模拟一下就能懂。cnt 用来表明有多少个重值结点,当 cnt == 1 时表明是重值链表的第一个结点,此时要用 second 记录下该结点的地址。枚举完后要令非重值链表的结尾指向-1。
  4. 输出链表。先后输出重值链表和非重值链表上的结点信息。当 second != first 时说明存在非重值链表,此时还需要输出非重值链表。

注意点:

  • 可以直接使用 %05d 的输出格式,以在不足5位时在高位补0。但是要注意-1不能使用 %05d 输出,否则会输出 -0001(而不是-1或者-00001),因此必须要留意-1的输出。
  • 题目可能会有无效结点,即不在题目给出的首地址开始的链表上。
  • r 的值一定要记得初始化,否则链表只存在一个结点时,语句 if (r != -1) node[r].next = -1; // 重值链表结尾置-1会出现数组越界的情况。
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;

struct NODE{ // 结点结构体
    int data; // 数据域
    int next; // next 指向下一个结点
} node[100010];

bool check[10110] = {false}; // check[x] == 1 表明该绝对值出现过

void print(int index) // 从地址 index 开始输出链表
{
    while (index != -1)
    {
        if (node[index].next == -1) // next 域为-1的要特别输出
            printf("%05d %d -1\n", index, node[index].data);
        else
            printf("%05d %d %05d\n", index, node[index].data, node[index].next);
        index = node[index].next;
    }
}

int main()
{
    int first, n, addr; // 首结点的地址、结点总数
    scanf("%d%d", &first, &n);
    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &addr); // 输入结点的地址
        scanf("%d%d", &node[addr].data, &node[addr].next);
    }

    int second = first, p = first, q = node[p].next, r = -1, cnt = 0;
    check[abs(node[p].data)] = true; // 记录第一个结点的绝对值
    while (q != -1) // q 始终为 p 的后继
    {
        if (check[abs(node[q].data)]) // 若结点 q 的绝对值出现过
        {
            ++cnt; // 重值结点计数+1
            node[p].next = node[q].next; // 从非重值链表上去除结点 q
            if (cnt == 1) // 计数值为1,说明 q 为重值链表的第一个结点
                second = r = q; // second, r 分别为重值链表的第一个和最后一个结点
            else // 计数值不为1
            {
                node[r].next = q; // q 加入重值链表尾部
                r = q; // r 指向新的重值链表尾结点
            }
            q = node[p].next; // q 指向 p 的后继
        }
        else // 结点 q 的绝对值没出现过
        {
            check[abs(node[q].data)] = true; // 记录 q 的绝对值
            p = q; // p, q 均往后挪一位
            q = node[p].next;
        }
    }
    if (r != -1)
        node[r].next = -1; // 重值链表结尾置-1

    print(first); // 输出非重值链表
    if (second != first) // 如果存在重值链表,则输出
        print(second);

    return 0;
}


1098 Insertion or Heap Sort

对直接插入排序和堆排序不了解的读者可以学习这篇文章:五大排序算法:插入、交换、选择、归并排序以及堆排序

题意:

输入第一行是序列中整数的个数 N,随后有两行数据,第一行是 N 个整数组成的序列,代表初始序列;第二行也是 N 个整数组成的序列,代表排序过程的一个中间序列。现在要求你判断出这个中间序列是应用哪一种排序方法得到的中间序列(是直接插入排序还是堆排序),并输出再应用一次该排序方法后得到的序列。

思路:

笨方法就是如下面代码所示,initial 数组保存初始序列,partially 数组保存输入的中间序列,in 保存每一步插入排序后得到的中间序列,heap 保存每一步堆排序后得到的中间序列。分别进行插入排序和堆排序,将每一次排序的结果与 partially 进行比较,如果相等就再排序一次然后按要求输出即可。

本题 AC 代码如下(更简洁的思路参见后面):

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

void DownAdjust(vector<int> &heap, int low, int high)
{
    int i = low, j = i * 2;         // i 为欲调整结点,j 为其左孩子
    while (j <= high)
    {
        if (j + 1 <= high && heap[j + 1] > heap[j]) j = j + 1;    // 右孩子比左孩子大就让 j 保存右孩子下标
        if (heap[j] <= heap[i]) break;  // 如果孩子结点不大于父结点 i,就不用往下继续调整
        swap(heap[j], heap[i]);     // 否则交换最大的孩子与父结点 i 的结点值
        i = j;                      // 较大的孩子结点作为新的欲调整结点,继续向下调整堆
        j = i * 2;
    }
}

int main()
{
    int N, flag = 1;
    cin >> N;
    vector<int> initial(N + 1), partially(N + 1), in(N + 1), heap(N + 1);
    for (int i = 1; i <= N; ++i) cin >> initial[i];     // 输入初始序列
    for (int i = 1; i <= N; ++i) cin >> partially[i];   // 输入中间序列
    in = initial, heap = initial;   // 初始化插入排序和堆排序的初始序列
    for (int i = 2; i <= N; ++i)
    {
        sort(in.begin() + 1, in.begin() + i + 1);
        if (in == partially)        // 当前的插入排序序列等于中间序列
        {
            sort(in.begin() + 1, in.begin() + i + 2);   // 再进行一次插入排序
            cout << "Insertion Sort\n" << in[1];        // 打印插入排序序列
            for (int i = 2; i < in.size(); ++i)
                cout << " " << in[i];
            flag = 0;   // flag = 0 表明不需要再计算堆排序的过程
            break;
        }
    }
    if (flag)
    {
        for (int i = N / 2; i >= 1; --i)    // 从最后一个非叶结点开始,向上将序列调整成大根堆
            DownAdjust(heap, i, N);
        for (int i = N; i > 1; --i)
        {
            swap(heap[1], heap[i]);         // 交换堆顶元素和最后一个未排序区间的最后一个元素
            DownAdjust(heap, 1, i - 1);     // 对堆顶元素进行一次向下调整
            if (heap == partially)          // 当前的堆排序列等于中间序列时停止循环
            {
                swap(heap[1], heap[i - 1]);             // 再进行一次插入排序
                DownAdjust(heap, 1, i - 2);             // 打印序列
                cout << "Heap Sort\n" << heap[1];       // 打印堆排序序列
                for (int i = 2; i < heap.size(); ++i)
                    cout << " " << heap[i];
                break;
            }
        }
    }
    
    return 0;
}

更简洁的思路:

对于直接插入排序来说,其每一步中间序列都是由有序部分无序部分组成的。假设变量 p 标记了无序部分的第一个元素,那么就需要先找到 p,如何找 p 呢?只需从输入的中间序列的第一个元素开始枚举,每一个元素与后一个元素进行比较,只要前一个元素小于等于后一个元素,就将 p 后移。退出循环时 p 指向的就是无序部分的第一个元素。由于无序部分的元素一定与初始序列对应的位置上的元素相等,此时只需要逐一比较这部分的元素,若最后 p 的值等于 N + 1,就表明用的是直接插入排序的方法。这样只需扫描序列一遍就能确定是否为直接插入排序。

如果不是直接插入排序,那它就是堆排序,因此输入的中间序列表明了堆排序过程的某一状态。而堆排序的后一部分是有序的,所以直接让 p 指向输入的中间序列的末尾,往序列的开头枚举,一旦其小于堆顶元素就退出循环。此时交换 p 指向的元素和堆顶元素,再从堆顶元素向下进行一次调整,即可得到下一次排序后的中间序列。

因为题目并没有说明序列中的元素是互不相同的,所以寻找无序部分第一个元素的时候,循环判定条件得用小于等于。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

void DownAdjust(vector<int> &heap, int low, int high)
{
    int i = low, j = i * 2;         // i 为欲调整结点,j 为其左孩子
    while (j <= high)
    {
        if (j + 1 <= high && heap[j + 1] > heap[j]) j = j + 1;    // 右孩子比左孩子大就让 j 保存右孩子下标
        if (heap[j] <= heap[i]) break;  // 如果孩子结点不大于父结点 i,就不用往下继续调整
        swap(heap[j], heap[i]);     // 否则交换最大的孩子与父结点 i 的结点值
        i = j;                      // 较大的孩子结点作为新的欲调整结点,继续向下调整堆
        j = i * 2;
    }
}

int main()
{
    int N, p = 2;
    cin >> N;
    vector<int> initial(N + 1), partially(N + 1);
    for (int i = 1; i <= N; ++i) cin >> initial[i];     // 输入初始序列
    for (int i = 1; i <= N; ++i) cin >> partially[i];   // 输入中间序列
    while (p <= N && partially[p - 1] <= partially[p]) ++p;     // p 不断后移直到指向无序部分的第一个元素
    int q = p;      // q 保存当前中间序列无序部分的第一个元素的下标
    while (p <= N && partially[p] == initial[p]) ++p;   // 往后继续枚举
    if (p == N + 1) // p 正常到达序列末尾,说明无序部分元素全部相等,为直接插入排序
    {
        cout << "Insertion Sort" << endl;
        sort(partially.begin() + 1, partially.begin() + q + 1); // 排序,得到下一次排序的结果
    }
    else
    {
        cout << "Heap Sort" << endl;
        p = N;
        while (p >= 2 && partially[p] > partially[1]) --p;
        swap(partially[1], partially[p]);
        DownAdjust(partially, 1, p - 1);
    }
    cout << partially[1];
    for (int i = 2; i <= N; ++i)
        cout << " " << partially[i];
    
    return 0;
}

1099 Build A Binary Search Tree

题意:

输入第一行是树中结点的个数 N(编号 0 ~ N - 1),随后 N 行,每行两个整数,分别对应编号第 0 ~ N - 1 的结点的左右孩子结点的编号,不存在就为-1,最后一行是树中结点的值(不是插入顺序)。相等于已经给了你二叉排序树的结构,而你需要将这些值放入相应的结点中。现在要求你输出这棵二叉排序树的层序遍历序列。

思路:

将每个结点的左右孩子键值、左右孩子分别保存在数组 keyslchildrchild 中。因为左小右大的二叉排序树,按照中序遍历得到的是一个递增的序列。所以可以先将所有结点的值进行排列,保存到数组 num 中。然后对二叉排序树进行中序遍历,根据顺序将 num 中的值放入结点中。最后再进行一次层序遍历,打印结点中的值即可。

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;

int N, j = 0, num[105], keys[105], lchild[105], rchild[105];

void InOrder(int root)
{
    if (lchild[root] != -1) InOrder(lchild[root]);  // 中序递归遍历左子树
    keys[root] = num[j++];                          // 为结点 root 赋值 num[j]
    if (rchild[root] != -1) InOrder(rchild[root]);  // 中序递归遍历右子树
}

int main()
{
    cin >> N;
    for (int i = 0; i < N; ++i)
        cin >> lchild[i] >> rchild[i];
    for (int i = 0; i < N; ++i) cin >> num[i];
    sort(num, num + N);     // 对键值排序
    InOrder(0);             // 从根结点开始进行中序遍历(用于给结点赋值)
    queue<int> q;
    cout << keys[0];        // 打印根结点的值
    if (lchild[0] != -1) q.push(lchild[0]);     // 如果根结点左孩子存在就入队
    if (rchild[0] != -1) q.push(rchild[0]);     // 如果根结点右孩子存在就入队
    while (!q.empty())
    {
        cout << " " << keys[q.front()];			// 打印队首结点的值
        if (lchild[q.front()] != -1) q.push(lchild[q.front()]); // 左孩子入队
        if (rchild[q.front()] != -1) q.push(rchild[q.front()]); // 右孩子入队
        q.pop();            // 队首结点出队
    }
    
    return 0;
}

1100 Mars Numbers

题意:
该题就是十进制数和十三进制数的转换,火星文即为十三进制下的数,只不过题目要求用字符串来表示每一位,其实就类似于十六进制的 ABCDEF。

思路:
题目中提到,不论是十进制数还是火星文的范围都在 [0, 169),因为13的平方是169,而十三进制下的169为100,因此按照题中的火星文规则,比100小的十三进制数都只有两个字符串位。从而可以定义两个字符串数组 low 和 high,分别表示低位字符串和高位字符串。数据范围比较小,可以在程序开始的时候就直接将此范围内十进制数对应的火星文和火星文对应的十进制数全部求出来,输入数据后直接查询即可。

numToStr 即为字符串数组,下标为十进制数,其元素为十进制数对应的火星文。strToNum 为 map 类型的数据,实现火星文到十进制数的映射。在程序开始时首先预处理所有的映射,并存储到 numToStr 和 strToNum 中,也即“打表”。但是对于火星文有两种特殊情况需要单独处理,这两种情况不论是高位还是低位的范围都是 [0, 12]。

  1. 第一种情况是高位为0,低位不为0,此时它对应的火星文就是 “tret” ~ “dec”。
  2. 第二种情况是高位不为0,低位为0,对应到十进制数下时,它们皆为13的倍数(自己验证一下即可),此时它对应的火星文就是 “tret” ~ “jou”。
    因此建立这两种情况映射的代码可以如下写:
for (int i = 0; i < 13; ++i)
{
    // 第一种情况
    numToStr[i] = low[i]; // 十进制数映射低位火星文
    strToNum[low[i]] = i; // 低位火星文映射十进制数
    // 第二种情况
    numToStr[i * 13] = high[i]; // 13的倍数映射高位火星文
    strToNum[high[i]] = i * 13; // 高位火星文映射13的倍数
}
  1. 其他情况的映射如下写:
for (int i = 1; i < 13; ++i)
{
    for (int j = 1; j < 13; ++j)
    {
        string str = high[i] + " " + low[j]; // 火星文
        numToStr[i * 13 + j] = str; // 数字转火星文
        strToNum[str] = i * 13 + j; // 火星文转数字
    }
}

注意点:

  • 用 getline() 来读取一行字符,详情可见PTA 算法笔记重点总结(一)中的第十点。由于 getline() 以回车键作为字符串输入结束的标志,因为 n 输入完后要记得用 getchar() 读取回车。
  • 字符串转数字的做法要记住,括号最好加上,以免出问题。
#include <iostream>
#include <string>
#include <map>
using namespace std;

const int maxl = 170; // 字符串最大长度
// [0, 12] 的火星文
string low[13] = {"tret", "jan", "feb", "mar", "apr", "may", "jun", "jly", "aug", "sep", "oct", "nov", "dec"};
// 13的 [0, 12] 倍的火星文
string high[13] = {"tret", "tam", "hel", "maa", "huh", "tou", "kes", "hei", "elo", "syy", "lok", "mer", "jou"};
string numToStr[170]; // 数字转火星文
map<string, int> strToNum; // 数字转火星文

void init()
{
    for (int i = 0; i < 13; ++i)
    {
        // 第一种情况
        numToStr[i] = low[i]; // 十进制数映射低位火星文
        strToNum[low[i]] = i; // 低位火星文映射十进制数
        // 第二种情况
        numToStr[i * 13] = high[i]; // 13的倍数映射高位火星文
        strToNum[high[i]] = i * 13; // 高位火星文映射13的倍数
    }
    for (int i = 1; i < 13; ++i)
    {
        for (int j = 1; j < 13; ++j)
        {
            string str = high[i] + " " + low[j]; // 火星文
            numToStr[i * 13 + j] = str; // 数字转火星文
            strToNum[str] = i * 13 + j; // 火星文转数字
        }
    }
}

int main()
{
    init();
    int n;
    cin >> n; // n 行整数
    getchar(); // 读取输入 n 后的回车,防止被 getling 读取
    while (n--)
    {
        string str;
        getline(cin, str); // 查询的数
        if (str[0] >= '0' && str[0] <= '9') // 如果是数字
        {
            int num = 0; // 字符串转成数字
            for (int i = 0; i < str.length(); ++i)
                num = num * 10 + (str[i] - '0');
            cout << numToStr[num] << endl; // 直接查表
        }
        else // 如果是火星文
            cout << strToNum[str] << endl;
    }
}


一定要自己写一遍哦~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值