华为OD机试 - SQL记录拆分 - 并查集(Python/JS/C/C++ 新系统 200分)

在这里插入图片描述

华为OD机试 新系统 统一考试题库清单(持续收录中)以及考点说明(Python/JS/C/C++)

专栏导读

本专栏收录于《华为OD机试真题(Python/JS/C/C++)》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

一、题目描述

某分布式数据库 Q 系统需要将 SQL 操作日志拆分到多个文件中,现给定一组 SQL 语句输入(数组格式),请按要求将 SQL 语句拆分到不同文件中并返回拆分后的文件数量。拆分规则如下:

  1. 单个数组成员的 SQL 语句(可能包含多条,按 ; 分隔)按同一行完整保存,不可跨文件拆分,且同一行的语句必须在同一个文件中。
  2. 部分语句带事务标签 [Tn]n 为正整数),如 [T1]A;相同事务标签的 SQL 语句必须保存于同一文件中。
  3. 当单个文件保存的行数超出限制时,需要新建文件;按语句组首次出现的行号顺序处理,优先放入当前文件,超出限制则新建文件;
  4. 特殊场景约束:若语句 A 与语句 B 必须同文件,语句 B 与语句 C 必须同文件,则语句 A、B、C 必须在同一文件(约束传递性)。约束合并后语句组计数即便超过限制也必须放在同一文件;
  5. SQL 数组为空时返回 0。

二、输入描述

输入:
参数1:单个文件限定可保存的最大 SQL 语句行数 split_line
参数2:SQL 语句数组 sql_text,每个数组成员可能包含多条 SQL 语句,用符号 ; 分隔;

三、输出描述

SQL 语句数组按规则拆分后的文件个数。

补充说明:

  • split_line 取值范围 [1, 10000],无效值返回 0;
  • sql_text 每行最多 1000 字符,总语句最多 100000 条;
  • 事务标签的范围 [1, 1000]
  • 最后一条语句可以没有分号结尾;
  • 空语句(仅含分号)按 1 行计算。## 四、测试用例

测试用例1:

1、输入

3
[T1]A;
[T2]B;
[T1]C;

2、输出

1

3、说明

第一行同时存在 T1、T2,第二行存在 T1,因此两行属于同一约束组,共 2 行。2 <= 3,只需要 1 个文件。

测试用例2:

1、输入

2
[T1]A;
[T2]B;
[T1]C;
[T2]D;

2、输出

2

3、说明

T1 对应第 1、3 行,共 2 行;T2 对应第 2、4 行,共 2 行。第一个组装满第一个文件,第二个组必须创建第二个文件。

五、解题思路

sql_text 中的每个数组成员看作一个“行节点”。同一数组成员无论包含多少个用 ; 分隔的 SQL,都必须完整保存,因此它始终只占 1 行,不需要按分号再次拆分;像 ;;; 这样的空语句所在数组成员同样按 1 行计算。

核心难点是事务标签产生的传递约束。例如第 1 行含 [T1],第 2 行同时含 [T1][T2],第 3 行含 [T2],那么三行必须全部位于同一个文件。这个问题本质上是在求“必须同文件”关系形成的连通分量,因此使用并查集 DSU。

扫描每一行,用正则提取 [Tn]。用 HashMap 记录每个事务标签第一次出现的行号;后续再次遇到同一标签时,将当前行和第一次出现的行执行 union。若一行包含多个事务标签,该行会同时参与多个 union,从而自然完成传递合并。

所有事务处理完后,统计每个并查集连通分量包含多少行,即一个不可拆分语句组的大小。

接下来必须按照“语句组首次出现的行号”处理。无需排序:直接再次按照原始行号从前往后扫描,某个并查集根节点第一次被访问时,就代表该语句组第一次出现。然后采用贪心策略:当前文件能完整容纳该组就加入;否则新建文件。若某个组自身行数已经超过 split_line,也不能拆分,仍整体占用一个文件。

时间复杂度约为 O(L + N·α(N)),其中 L 为所有 SQL 文本总字符数,N 为数组行数;空间复杂度为 O(N + T),T 为事务标签数量。

六、Python算法源码

import sys
import re


class DSU:
    """
    并查集:
    用于维护由于事务标签约束而必须放入同一个文件的 SQL 行。
    """

    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        """
        查找当前节点所在集合的根节点。
        路径压缩后,后续查询的效率会更高。
        """
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])

        return self.parent[x]

    def union(self, a, b):
        """
        将两个 SQL 行所在的集合进行合并。
        """
        root_a = self.find(a)
        root_b = self.find(b)

        if root_a == root_b:
            return

        # 按秩合并,尽量降低并查集树的高度
        if self.rank[root_a] < self.rank[root_b]:
            self.parent[root_a] = root_b
        elif self.rank[root_a] > self.rank[root_b]:
            self.parent[root_b] = root_a
        else:
            self.parent[root_b] = root_a
            self.rank[root_a] += 1


def split_file_count(split_line, sql_text):
    # split_line 非法或者没有 SQL 数据,直接返回 0
    if split_line < 1 or split_line > 10000 or not sql_text:
        return 0

    n = len(sql_text)

    # 每一个 sql_text 成员对应一个不可拆分的 SQL 行
    dsu = DSU(n)

    """
    key   : 事务编号,例如 [T10] 对应 10
    value : 该事务第一次出现的 SQL 行下标
    """
    first_line_by_transaction = {}

    # 匹配 [T1]、[T20]、[T1000] 等事务标签
    tag_pattern = re.compile(r'\[T(\d+)\]')

    for i, line in enumerate(sql_text):

        for matcher in tag_pattern.finditer(line):
            tag = int(matcher.group(1))

            # 事务编号只处理 [1, 1000]
            if tag < 1 or tag > 1000:
                continue

            if tag not in first_line_by_transaction:
                # 当前事务第一次出现
                first_line_by_transaction[tag] = i
            else:
                """
                相同事务标签的 SQL 行必须位于同一个文件。

                当前行如果同时出现多个事务标签,
                会分别执行多次 union,从而自动实现约束传递:
                A 与 B 同组,B 与 C 同组
                => A、B、C 最终属于同一个并查集。
                """
                dsu.union(
                    i,
                    first_line_by_transaction[tag]
                )

    # group_size[root] 表示一个不可拆分 SQL 组一共有多少行
    group_size = [0] * n

    for i in range(n):
        root = dsu.find(i)
        group_size[root] += 1

    """
    按原始 SQL 行号从前向后扫描。
    第一次遇到某个根节点时,就相当于按照
    “该 SQL 组第一次出现的行号”进行处理。
    """
    processed = [False] * n

    file_count = 0
    used_lines = 0

    for i in range(n):
        root = dsu.find(i)

        if processed[root]:
            continue

        processed[root] = True

        size = group_size[root]

        if file_count == 0:
            # 第一个 SQL 组创建第一个文件
            file_count = 1
            used_lines = size

        elif used_lines + size <= split_line:
            # 当前文件能够完整容纳该 SQL 组
            used_lines += size

        else:
            """
            当前文件不能完整容纳该 SQL 组,需要创建新文件。
            即使 size 本身大于 split_line,也不能拆分事务组。
            """
            file_count += 1
            used_lines = size

    return file_count


def main():
    # splitlines() 可以正确处理 Windows/Linux 换行符
    lines = sys.stdin.read().splitlines()

    if not lines:
        return

    split_line = int(lines[0].strip())

    sql_text = []

    """
    严格保持原 Java main 方法的行为:
    后续行如果不包含 ";",立即结束读取;
    当前不包含 ";" 的这一行不会加入 sql_text。
    """
    for line in lines[1:]:
        if ';' not in line:
            break

        sql_text.append(line)

    print(split_file_count(split_line, sql_text))


if __name__ == '__main__':
    main()

七、JavaScript算法源码

const fs = require('fs');


class DSU {

    constructor(n) {
        this.parent = Array.from(
            { length: n },
            (_, i) => i
        );

        this.rank = new Array(n).fill(0);
    }

    /**
     * 查找节点所在集合的根节点。
     * 使用路径压缩优化后续查询。
     */
    find(x) {
        if (this.parent[x] !== x) {
            this.parent[x] =
                this.find(this.parent[x]);
        }

        return this.parent[x];
    }

    /**
     * 合并两个 SQL 行所在的集合。
     */
    union(a, b) {
        let rootA = this.find(a);
        let rootB = this.find(b);

        if (rootA === rootB) {
            return;
        }

        // 按秩合并,避免树结构过深
        if (this.rank[rootA] < this.rank[rootB]) {

            this.parent[rootA] = rootB;

        } else if (
            this.rank[rootA] > this.rank[rootB]
        ) {

            this.parent[rootB] = rootA;

        } else {

            this.parent[rootB] = rootA;
            this.rank[rootA]++;
        }
    }
}


function splitFileCount(splitLine, sqlText) {

    // 参数非法或 SQL 数组为空
    if (
        splitLine < 1 ||
        splitLine > 10000 ||
        sqlText.length === 0
    ) {
        return 0;
    }

    const n = sqlText.length;

    const dsu = new DSU(n);

    /*
     * key   : 事务编号
     * value : 该事务第一次出现的行号
     */
    const firstLineByTransaction = new Map();

    // 匹配 [T1]、[T12]、[T1000]
    const tagPattern = /\[T(\d+)\]/g;

    for (let i = 0; i < n; i++) {

        /*
         * JavaScript 的全局正则具有 lastIndex 状态,
         * 每处理新的一行之前必须重置。
         */
        tagPattern.lastIndex = 0;

        let matcher;

        while (
            (matcher = tagPattern.exec(sqlText[i])) !== null
        ) {

            const tag = Number(matcher[1]);

            if (tag < 1 || tag > 1000) {
                continue;
            }

            if (!firstLineByTransaction.has(tag)) {

                // 当前事务第一次出现
                firstLineByTransaction.set(tag, i);

            } else {

                /*
                 * 相同事务标签必须同文件,因此合并。
                 *
                 * 如果当前 SQL 行同时出现 [T1] 和 [T2],
                 * 当前行会分别连接 T1、T2 的已有 SQL 行,
                 * 因此并查集能够自然实现约束的传递性。
                 */
                dsu.union(
                    i,
                    firstLineByTransaction.get(tag)
                );
            }
        }
    }

    // 统计每个不可拆分 SQL 组的行数
    const groupSize = new Array(n).fill(0);

    for (let i = 0; i < n; i++) {
        const root = dsu.find(i);

        groupSize[root]++;
    }

    /*
     * 按原 SQL 行号顺序处理。
     * 一个根节点第一次被访问的位置,
     * 就是该 SQL 组第一次出现的位置。
     */
    const processed = new Array(n).fill(false);

    let fileCount = 0;
    let usedLines = 0;

    for (let i = 0; i < n; i++) {

        const root = dsu.find(i);

        if (processed[root]) {
            continue;
        }

        processed[root] = true;

        const size = groupSize[root];

        if (fileCount === 0) {

            fileCount = 1;
            usedLines = size;

        } else if (
            usedLines + size <= splitLine
        ) {

            // 当前文件能够完整容纳该 SQL 组
            usedLines += size;

        } else {

            /*
             * 当前文件装不下完整 SQL 组,
             * 必须创建新文件。
             *
             * 即使 size > splitLine,
             * 根据事务约束仍然不能拆开。
             */
            fileCount++;
            usedLines = size;
        }
    }

    return fileCount;
}


// 读取标准输入
let input = fs.readFileSync(0, 'utf8');

// 统一 Windows/Linux 换行符
input = input
    .replace(/\r\n/g, '\n')
    .replace(/\r/g, '\n');

let lines = input.split('\n');

/*
 * 文件最后通常会有一个换行符。
 * split('\n') 会因此产生一个额外的空字符串,
 * 这个空字符串并不是 Scanner.nextLine() 实际读取的一行,
 * 所以需要删除。
 */
if (
    lines.length > 0 &&
    lines[lines.length - 1] === ''
) {
    lines.pop();
}

if (lines.length > 0) {

    const splitLine = Number(lines[0].trim());

    const sqlText = [];

    /*
     * 严格保持 Java 原代码行为:
     * 遇到不包含 ";" 的一行,直接停止。
     */
    for (let i = 1; i < lines.length; i++) {

        if (!lines[i].includes(';')) {
            break;
        }

        sqlText.push(lines[i]);
    }

    console.log(
        splitFileCount(splitLine, sqlText)
    );
}

八、C算法源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_LINE_LEN 1105


/**
 * 并查集结构。
 */
typedef struct {
    int *parent;
    int *rankValue;
} DSU;


/**
 * 初始化并查集。
 */
void dsuInit(DSU *dsu, int n) {

    dsu->parent =
        (int *)malloc(
            sizeof(int) * n
        );

    dsu->rankValue =
        (int *)calloc(
            n,
            sizeof(int)
        );

    for (int i = 0; i < n; ++i) {
        dsu->parent[i] = i;
    }
}


/**
 * 查找根节点,并进行路径压缩。
 */
int dsuFind(DSU *dsu, int x) {

    if (dsu->parent[x] != x) {

        dsu->parent[x] =
            dsuFind(
                dsu,
                dsu->parent[x]
            );
    }

    return dsu->parent[x];
}


/**
 * 合并两个 SQL 行所在集合。
 */
void dsuUnion(
    DSU *dsu,
    int a,
    int b
) {

    int rootA = dsuFind(dsu, a);
    int rootB = dsuFind(dsu, b);

    if (rootA == rootB) {
        return;
    }

    // 按秩合并
    if (
        dsu->rankValue[rootA] <
        dsu->rankValue[rootB]
    ) {

        dsu->parent[rootA] = rootB;

    } else if (
        dsu->rankValue[rootA] >
        dsu->rankValue[rootB]
    ) {

        dsu->parent[rootB] = rootA;

    } else {

        dsu->parent[rootB] = rootA;
        ++dsu->rankValue[rootA];
    }
}


/**
 * 删除 fgets 读取进来的 \\n / \\r。
 */
void removeNewLine(char *str) {

    size_t len = strlen(str);

    while (
        len > 0 &&
        (
            str[len - 1] == '\n' ||
            str[len - 1] == '\r'
        )
    ) {
        str[--len] = '\0';
    }
}


/**
 * 扫描一行字符串中的所有 [Tn] 标签。
 *
 * C 标准库没有直接对应 Java Pattern 的正则接口,
 * 因此直接寻找:
 *
 * '[' -> 'T' -> 一段数字 -> ']'
 */
void processTags(
    const char *line,
    int row,
    DSU *dsu,
    int firstLine[1001]
) {

    size_t len = strlen(line);

    for (
        size_t i = 0;
        i + 3 < len;
        ++i
    ) {

        // 必须以 "[T" 开头
        if (
            line[i] != '[' ||
            line[i + 1] != 'T'
        ) {
            continue;
        }

        size_t j = i + 2;

        // T 后面至少要有一位数字
        if (
            j >= len ||
            !isdigit(
                (unsigned char)line[j]
            )
        ) {
            continue;
        }

        long tag = 0;

        while (
            j < len &&
            isdigit(
                (unsigned char)line[j]
            )
        ) {

            tag =
                tag * 10 +
                (line[j] - '0');

            ++j;
        }

        /*
         * 合法事务标签必须:
         * 1. 数字后面紧跟 ']'
         * 2. 编号范围为 [1,1000]
         */
        if (
            j < len &&
            line[j] == ']' &&
            tag >= 1 &&
            tag <= 1000
        ) {

            if (firstLine[tag] == -1) {

                // 当前事务第一次出现
                firstLine[tag] = row;

            } else {

                /*
                 * 相同事务标签必须位于同一个文件,
                 * 因此合并当前行和事务第一次出现的行。
                 *
                 * 同一 SQL 行中存在多个标签时,
                 * 会发生多次合并,实现约束的传递性。
                 */
                dsuUnion(
                    dsu,
                    row,
                    firstLine[tag]
                );
            }

            i = j;
        }
    }
}


/**
 * 计算需要拆分出的文件数量。
 */
int splitFileCount(
    int splitLine,
    char **sqlText,
    int n
) {

    if (
        splitLine < 1 ||
        splitLine > 10000 ||
        n == 0
    ) {
        return 0;
    }

    DSU dsu;
    dsuInit(&dsu, n);

    /*
     * firstLine[t]:
     * 事务 Tt 第一次出现的行号。
     */
    int firstLine[1001];

    for (int i = 0; i <= 1000; ++i) {
        firstLine[i] = -1;
    }

    for (int i = 0; i < n; ++i) {

        processTags(
            sqlText[i],
            i,
            &dsu,
            firstLine
        );
    }

    // 统计每个不可拆分 SQL 组的行数
    int *groupSize =
        (int *)calloc(
            n,
            sizeof(int)
        );

    for (int i = 0; i < n; ++i) {

        int root =
            dsuFind(&dsu, i);

        ++groupSize[root];
    }

    /*
     * 标记一个并查集是否已经进行过文件分配。
     */
    unsigned char *processed =
        (unsigned char *)calloc(
            n,
            sizeof(unsigned char)
        );

    int fileCount = 0;
    int usedLines = 0;

    for (int i = 0; i < n; ++i) {

        int root =
            dsuFind(&dsu, i);

        if (processed[root]) {
            continue;
        }

        processed[root] = 1;

        int size = groupSize[root];

        if (fileCount == 0) {

            fileCount = 1;
            usedLines = size;

        } else if (
            usedLines + size <= splitLine
        ) {

            // 当前文件还能完整容纳当前 SQL 组
            usedLines += size;

        } else {

            /*
             * 无法完整放入当前文件,创建新文件。
             * 即使 size > splitLine,也不允许拆事务组。
             */
            ++fileCount;
            usedLines = size;
        }
    }

    free(dsu.parent);
    free(dsu.rankValue);
    free(groupSize);
    free(processed);

    return fileCount;
}


int main(void) {

    char buffer[MAX_LINE_LEN];

    // 读取 splitLine
    if (
        fgets(
            buffer,
            sizeof(buffer),
            stdin
        ) == NULL
    ) {
        return 0;
    }

    removeNewLine(buffer);

    int splitLine = atoi(buffer);

    /*
     * 动态保存 SQL 行。
     * 当容量不足时按 2 倍进行扩容。
     */
    int capacity = 16;
    int n = 0;

    char **sqlText =
        (char **)malloc(
            sizeof(char *) * capacity
        );

    while (
        fgets(
            buffer,
            sizeof(buffer),
            stdin
        ) != NULL
    ) {

        removeNewLine(buffer);

        /*
         * 与 Java 原代码保持一致:
         * 当前行只要不包含 ";" 就直接停止。
         */
        if (
            strchr(buffer, ';') == NULL
        ) {
            break;
        }

        if (n == capacity) {

            capacity *= 2;

            sqlText =
                (char **)realloc(
                    sqlText,
                    sizeof(char *) * capacity
                );
        }

        /*
         * 为当前 SQL 行单独申请空间,
         * 保存完整内容,不能根据 ';' 拆分。
         */
        sqlText[n] =
            (char *)malloc(
                strlen(buffer) + 1
            );

        strcpy(sqlText[n], buffer);

        ++n;
    }

    printf(
        "%d\n",
        splitFileCount(
            splitLine,
            sqlText,
            n
        )
    );

    for (int i = 0; i < n; ++i) {
        free(sqlText[i]);
    }

    free(sqlText);

    return 0;
}

九、C++算法源码

#include <iostream>
#include <vector>
#include <string>
#include <regex>
#include <unordered_map>

using namespace std;


/**
 * 并查集:
 * 维护哪些 SQL 行由于事务标签约束而必须同文件。
 */
class DSU {

private:
    vector<int> parent;
    vector<int> rankValue;

public:

    explicit DSU(int n)
        : parent(n), rankValue(n, 0) {

        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }

    /**
     * 查询根节点,并使用路径压缩优化。
     */
    int find(int x) {

        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }

        return parent[x];
    }

    /**
     * 合并两个 SQL 行所在集合。
     */
    void unite(int a, int b) {

        int rootA = find(a);
        int rootB = find(b);

        if (rootA == rootB) {
            return;
        }

        // 按秩合并,避免并查集退化
        if (rankValue[rootA] < rankValue[rootB]) {

            parent[rootA] = rootB;

        } else if (
            rankValue[rootA] > rankValue[rootB]
        ) {

            parent[rootB] = rootA;

        } else {

            parent[rootB] = rootA;
            ++rankValue[rootA];
        }
    }
};


int splitFileCount(
    int splitLine,
    const vector<string>& sqlText
) {

    // split_line 非法或没有 SQL 数据
    if (
        splitLine < 1 ||
        splitLine > 10000 ||
        sqlText.empty()
    ) {
        return 0;
    }

    int n = static_cast<int>(sqlText.size());

    DSU dsu(n);

    /*
     * key   : 事务编号
     * value : 该事务第一次出现的 SQL 行号
     */
    unordered_map<int, int>
        firstLineByTransaction;

    // 匹配 [T1]、[T10]、[T1000]
    regex tagPattern(R"(\[T(\d+)\])");

    for (int i = 0; i < n; ++i) {

        sregex_iterator begin(
            sqlText[i].begin(),
            sqlText[i].end(),
            tagPattern
        );

        sregex_iterator end;

        for (
            auto it = begin;
            it != end;
            ++it
        ) {

            int tag =
                stoi((*it)[1].str());

            if (tag < 1 || tag > 1000) {
                continue;
            }

            auto pos =
                firstLineByTransaction.find(tag);

            if (
                pos ==
                firstLineByTransaction.end()
            ) {

                // 当前事务第一次出现
                firstLineByTransaction[tag] = i;

            } else {

                /*
                 * 同一个事务标签对应的 SQL 行必须同文件。
                 *
                 * 如果一行同时含有多个事务标签,
                 * 会执行多次 unite,从而通过当前行
                 * 把不同事务集合连接起来,实现传递约束。
                 */
                dsu.unite(
                    i,
                    pos->second
                );
            }
        }
    }

    // 统计每个不可拆分 SQL 组的总行数
    vector<int> groupSize(n, 0);

    for (int i = 0; i < n; ++i) {

        int root = dsu.find(i);

        ++groupSize[root];
    }

    /*
     * 顺序遍历原始 SQL 行。
     * 第一次遇到某个根节点时,
     * 就是对应 SQL 组第一次出现的位置。
     */
    vector<bool> processed(n, false);

    int fileCount = 0;
    int usedLines = 0;

    for (int i = 0; i < n; ++i) {

        int root = dsu.find(i);

        if (processed[root]) {
            continue;
        }

        processed[root] = true;

        int size = groupSize[root];

        if (fileCount == 0) {

            fileCount = 1;
            usedLines = size;

        } else if (
            usedLines + size <= splitLine
        ) {

            // 当前文件能够完整装下该组
            usedLines += size;

        } else {

            /*
             * 当前文件无法完整容纳该组,
             * 创建新文件。
             *
             * size 即使超过 splitLine,
             * 事务组也不能进行拆分。
             */
            ++fileCount;
            usedLines = size;
        }
    }

    return fileCount;
}


int main() {

    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string firstLine;

    if (!getline(cin, firstLine)) {
        return 0;
    }

    int splitLine = stoi(firstLine);

    vector<string> sqlText;

    string input;

    /*
     * 与 Java 代码保持一致:
     * 遇到不包含 ";" 的行立即终止读取,
     * 且该行不会加入 sqlText。
     */
    while (getline(cin, input)) {

        if (
            input.find(';') == string::npos
        ) {
            break;
        }

        sqlText.push_back(input);
    }

    cout
        << splitFileCount(
            splitLine,
            sqlText
        )
        << '\n';

    return 0;
}


🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++ 新系统 200分)

🏆本文收录于,华为OD机试真题(Python/JS/C/C++)

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

哪 吒

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

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

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

打赏作者

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

抵扣说明:

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

余额充值