python_11_并查集和图算法

博客主要介绍了用Python实现的并查集和图相关算法。包括图的结构描述、转化方法,以及图的宽度优先遍历、深度优先遍历、拓扑排序、最小生成树算法(Kruskal和Prim)、Dijkstra算法等,还提及了Dijkstra算法的堆优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 并查集

在这里插入图片描述
在这里插入图片描述

class Node:
    def __init__(self,value):
        self.val = value
        self.next = None


class UnionSett:
    def __init__(self):
        # V->节点 对应关系
        self.nodes = dict()
        # 充当往上找的行为{b:b的父节点}
        self.parents = dict()
        # 只有一个点,它是代表点,才有记录{a:4} 被指向4次
        self.sizeMap = dict()

    def UnionSet(self,lists):            # 集合初始化
        for i in range(len(lists)):
            node = Node(lists[i])                 # 按照我这种方式,数据创建了节点,next指向空
            self.nodes.update({lists[i]:node})    # 11对应{自己的值:自己创建的节点}
            self.parents.update({node:node})      # 一开始,父节点是自己
            self.sizeMap.update({node:1})

    # 从点cur开始,一直往上找,找到不能再往上的代表点,返回
    def findFather(self,cur):
        path = []
        while cur != self.parents.get(cur):
            path.append(cur)              # 栈内放cur节点,栈全是路径节点
            cur = self.parents.get(cur)   # cur向上移一位
        # cur头节点
        while path:
            self.parents.update({path.pop():cur})    # 经过所有的节点全部改为指向代表点为直接父亲
        return cur

    # a样本和b样本是否是一个集合
    def isSameSet(self,a,b):
        # 若果ab不在节点集合中
        if a not in self.nodes or b not in self.nodes:
            return False
        # 如果ab在集合中,如果a往上不能再往上的代表点和b对应的是同一个节点,返回True
        return self.findFather(self.nodes.get(a)) == self.findFather(self.nodes.get(b))

    def union(self,a,b):      # 将a,b集合合成同一个集合
        # 若果ab不在节点集合中
        if a not in self.nodes or b not in self.nodes:
            return
        # aHead指向a的代表点
        aHead = self.findFather(self.nodes.get(a))
        bHead = self.findFather(self.nodes.get(b))
        if aHead !=bHead:   # 如果a和b的代表点不是同一个代表点,如果一样说明已经在一个集合中了
            aSetSize = self.sizeMap.get(aHead)   # aHead是a的代表点,所以能在sizemap中拿到代表点被指向的数
            bSetSize = self.sizeMap.get(bHead)
            if aSetSize >= bSetSize:            # 小集合挂大集合,b挂到a上
                self.parents.update({bHead:aHead})       # bHead的父亲直接改成aHead
                self.sizeMap.update({aHead:aSetSize + bSetSize})
                self.sizeMap.pop(bHead)         # sizeMap中只有一个节点,删除bhead,现在代表点是a
            else:
                self.parents.update({aHead: bHead})  # aHead的父亲直接改成bHead
                self.sizeMap.update({bHead: aSetSize + bSetSize})
                self.sizeMap.pop(aHead)  # sizeMap中只有一个节点,删除ahead,现在代表点是b


b = [1,2,3,4]
a = UnionSett()
a.UnionSet(b)
print(a.isSameSet(1,2))

findFather函数
在这里插入图片描述
例子:
四个学生实际上为一个人,只是各种平台的账号不同
在这里插入图片描述
在这里插入图片描述

class Node:
    def __init__(self,value):
        self.val = value
        self.next = None


class UnionSett:
    def __init__(self):
        # V->节点 对应关系
        self.nodes = dict()
        # 充当往上找的行为{b:b的父节点}
        self.parents = dict()
        # 只有一个点,它是代表点,才有记录{a:4} 被指向4次
        self.sizeMap = dict()

    def UnionSet(self,lists):            # 集合初始化
        for i in range(len(lists)):      # len(lists) == 4
            node = Node(lists[i])                 # 按照我这种方式,数据创建了节点,next指向空
            self.nodes.update({lists[i]:node})    # 11对应{自己的值:自己创建的节点}
            self.parents.update({node:node})      # 一开始,父节点是自己
            self.sizeMap.update({node:1})

    # 从点cur开始,一直往上找,找到不能再往上的代表点,返回
    def findFather(self,cur):
        path = []
        while cur != self.parents.get(cur):
            path.append(cur)              # 栈内放cur节点,栈全是路径节点
            cur = self.parents.get(cur)   # cur向上移一位
        # cur头节点
        while path:
            self.parents.update({path.pop():cur})    # 经过所有的节点全部改为指向代表点为直接父亲
        return cur

    # a样本和b样本是否是一个集合
    def isSameSet(self,a,b):  # a,b分别接受了一个用户
        # 若果ab不在节点集合中
        if a not in self.nodes or b not in self.nodes:
            return False
        # 如果ab在集合中,如果a往上不能再往上的代表点和b对应的是同一个节点,返回True
        return self.findFather(self.nodes.get(a)) == self.findFather(self.nodes.get(b))

    def union(self,a,b):      # 将a,b集合合成同一个集合,a,b分别接受了一个用户,准备将这两个用户合并
        # 若果ab不在节点集合中
        if a not in self.nodes or b not in self.nodes:
            return
        # aHead指向a的代表点
        aHead = self.findFather(self.nodes.get(a))
        bHead = self.findFather(self.nodes.get(b))
        if aHead !=bHead:   # 如果a和b的代表点不是同一个代表点,如果一样说明已经在一个集合中了
            aSetSize = self.sizeMap.get(aHead)   # aHead是a的代表点,所以能在sizemap中拿到代表点被指向的数
            bSetSize = self.sizeMap.get(bHead)
            if aSetSize >= bSetSize:            # 小集合挂大集合,b挂到a上
                self.parents.update({bHead:aHead})       # bHead的父亲直接改成aHead
                self.sizeMap.update({aHead:aSetSize + bSetSize})
                self.sizeMap.pop(bHead)         # sizeMap中只有一个节点,删除bhead,现在代表点是a
            else:
                self.parents.update({aHead: bHead})  # aHead的父亲直接改成bHead
                self.sizeMap.update({bHead: aSetSize + bSetSize})
                self.sizeMap.pop(aHead)  # sizeMap中只有一个节点,删除ahead,现在代表点是b

    def getSetNum(self):
        return len(self.sizeMap)


class Student:
    def __init__(self,a,b,c):
        self.a = a
        self.b = b
        self.c = c

# 如果两个user,a字段一样或b字段一样或c子段一样就认为是一个人
# 请合并users,返回合并之后的用户数量
def mergeUsers(users):           # users = [student1,student2,student3,student4]
    unionFind = UnionSett()      # 创建并查集类型
    unionFind.UnionSet(users)    # 初始化并查集
    mapA = dict()       # {a字段的值:哪个用户有这个值}
    mapB = dict()       # {a字段的值:哪个用户有这个值}
    mapC = dict()       # {a字段的值:哪个用户有这个值}

    for user in users:            # user = student1
        if user.a in mapA:  # 如果该用户a字段在mapA中,student1.a在不在mapA中
            unionFind.union(user,mapA.get(user.a))    # 就将用户和用户的a字段并为一个集合
        else:   # 不在,是新字段
            mapA.update({user.a:user})

        if user.b in mapB:  # 如果该用户b字段在mapB中
            unionFind.union(user,mapB.get(user.b))    # 就将用户和用户的b字段并为一个集合
        else:   # 否则是新字段
            mapB.update({user.b:user})

        if user.c in mapC:  # 如果该用户c字段在mapC中
            unionFind.union(user,mapC.get(user.c))    # 就将用户和用户的c字段并为一个集合
        else:   # 否则是新字段
            mapC.update({user.c:user})


    # 向并查集询问,合并之后,还有多少个集合?
    return unionFind.getSetNum()

student1 = Student('123','345','567')
student2 = Student('123','777','888')
student3 = Student('111','777','999')
student4 = Student('545','781','999')
users = [student1,student2,student3,student4]
print(mergeUsers(users))

2 图

(1)由点的集合和边的集合构成
(2)虽然存在有向图和无向图的概念,但实际上都可以用有向图来表示
(3)边上可能带有权值
常见的表达方法:
(1)邻接表法;
(2)邻接矩阵法
(3)…

邻接表
在这里插入图片描述
邻接矩阵:
在这里插入图片描述
把图算法统一转化为自己熟悉的结构:

2.1 点、边、图结构描述

# 点结构描述
class Node:
    def __init__(self,value):
        self.value = value      # 点的value值,点的编号、点的名称、点的值
        self.ins = 0            # 入度,有多少连向自己
        self.outs = 0           # 自己出去的边有多少
        self.nexts = []         # 直接邻居集合,Node集合,注意这里是指点出发直接指向叫直接邻居,被指不算,也就是outs
        self.edges = []         # 从点出发的边 组成的集合


# 边结构描述
class Edge:        # 有向边,有向边可以表示无向边
    def __init__(self,weight,froms,to):
        self.weight = weight      # 权重
        self.froms = froms        # Node类型,指向自己的点
        self.to = to              # 指向别的点的边


# 图结构描述
class Graph:
    def __init__(self):
        self.nodes = dict()     # 编号:    编号为1的点是什么
        self.edges = set()     # 所有的边都在里面,set是去重集合,这里只放key(边类型),不需要对应关系

2.2 转化为自己的图结构

最重要是能什么算法都可以转化过来

# 转为图结构,普通矩阵,邻接矩阵都可以转为这个图结构
# matrix 所有的边
# matrix 是 N*3的矩阵
# [weight,from节点上面的值,to节点指向谁的值]

def creatGraph(matrix):
    graph = Graph()          # 建立空图,点集和边集都没有
    for i in range(len(matrix)):
        # matrix[0][0],matrix[0][1],matrix[0][2]
        weight = matrix[i][0]  # 接收权重
        froms = matrix[i][1]
        to = matrix[i][2]

        if froms not in graph.nodes:     # 如果当前点编号在图的点集和中
            graph.nodes.update({froms:Node(froms)})
        if to not in graph.nodes:        # 如果指向的点不在图的点集和中
            graph.nodes.update({to:Node(to)})
        # 现在已经有from编号所对应得点和to编号所对应得点
        # 拿出这两个点
        fromNode = graph.nodes.get(froms)   # 我将编号对应的点拿出来
        toNode = graph.nodes.get(to)        # 得到指向的那个点
        # 一条边,有froms点,to点,和权重,一条边就表示好了
        newEdge = Edge(weight,fromNode,toNode)     # 将点权重,自己点和指向的点都放到边集和中
        fromNode.nexts.append(toNode)              # 在点类型中,加入直接邻居数组

        fromNode.outs = fromNode.outs + 1          # 入度出度分别加1
        toNode.ins = toNode.ins + 1

        fromNode.edges.append(newEdge)             # 将边的信息放入这个点中
        graph.edges.add(newEdge)                # 将边的信息放入到大图边集和中

    return graph

图的算法题如何搞定:
(1)先用自己最熟练的方式,实现图结构的表达;
(2)在自己熟悉的结构上,实现所有常用的图算法作为模板;
(3)把算法题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可。

2.3 图的宽度优先遍历

宽度优先遍历 二叉树用队列,图用hashset

  1. 利用队列实现
  2. 从源节点开始依次按照宽度进队列,然后弹出
  3. 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
  4. 直到队列变空

在这里插入图片描述
b的直接邻居没有a

# 从node出发,进行宽度优先遍历
def bfs(node):   # 传入Node类型
    if node is None:
        return
    queue = []
    sets = set()
    queue.append(node)
    sets.add(node)
    while queue:
        cur = queue.pop(0)  # 将元素弹出队列
        print(cur.value)      # 这个可以根据你的业务变化
        for i in cur.nexts:    # 遍历当前节点的邻居
            if i not in sets:
                sets.add(i)
                queue.append(i)

2.4 图的深度优先遍历

用栈和set

  1. 利用栈实现
  2. 从源节点开始把节点按照深度放入栈,然后弹出
  3. 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
  4. 知道栈变空
# 从node出发,进行深度优先遍历
def dfs(node):
    if node is None:
        return
    stack = []
    sets = set()
    stack.append(node)  # 节点入栈
    sets.add(node)
    print(node.value)
    while stack:
        cur = stack.pop()   # 弹出节点赋给cur
        for i in cur.nexts:   # i遍历节点的直接邻居
            if i not in sets:
                stack.append(cur)  # 栈先加入当前节点,再加入指向的节点
                stack.append(i)
                sets.add(i)        # set中加入指向的节点
                print(i.value)
                break

2.5 图的拓扑排序算法

  1. 在图中找到所有入度为0的点输出
  2. 把所有入度为0的点在图中删除,继续找入度为0的点输出,周而复始
  3. 图的所有点都被删除后,依次输出的顺序就是拓扑排序

要求:有向图且没有环
应用:事件安排、编译顺序

# 图的拓扑排序
def sortedTopology(graph):
    # key 某一个点node
    # value 剩余的入度
    inMap = dict()   # {node:这个node还剩多少入度}
    # 剩余入度为0的点,才能进这个队列
    zeroInQueue = []
    # 把所有点集拿出来,在图类里,nodes是一个字典类型,里面是{编号;点}
    for node in graph.nodes.values:    # values不是点的值而是点,而是{key:values}
        inMap.update({node:node.ins})  # 原始图放入,每个点原始入度为剩余入度
        if node.ins == 0:              # 必有一个剩余入度为0的点,第一批入度的点
            zeroInQueue.append(node)
    result = []                        # 拓扑排序的结果,依次加入result

    while zeroInQueue:
        cur = zeroInQueue.pop(0)
        result.append(cur)
        for next in cur.nexts:
            inMap.update({next:inMap.get(next) - 1})          # 所有的邻居入度-1
            if inMap.get(next) == 0:
                zeroInQueue.append(next)
                
    return result

2.6 最小生成树算法Kruskal

  1. 总是从权值最小的边开始考虑,依次考虑权值依次变大的边
  2. 当前的边要么进入最小生成树的集合,要么丢弃
  3. 如果当前的边进入最小生成树的集合中不会形成环,就要当前边
  4. 如果当前的边进入最小生成树的集合中会形成环,就不要当前边
  5. 考察完所有边之后,最小生成树的集合也得到了

用并查集算法

import heapq
# 最小生成树——Kruskal算法

# 本来图就是有结点的,小修改一下并查集,算法思路一致
class UnionFind:
    def __init__(self):           # 取消封装节点步骤
        self.fatherMap = dict()   # {Node:Node}
        self.sizeMap = dict()     # {Node:整形}

    def makeSets(self,nodes):
        self.fatherMap.clear()
        self.sizeMap.clear()           # 清空这两个集合
        for node in nodes:             # 遍历每一个节点
            self.fatherMap.update({node:node})  # 一开始父节点是自己
            self.sizeMap.update({node:1})       # 当前节点被指向次数为1

    def findFather(self,n):
        path = []
        while n is not self.fatherMap.get(n):
            path.append(n)
            n = self.fatherMap.get(n)

        while path:
            self.fatherMap.update({path.pop():n})

        return n

    def isSameSet(self,a,b):
        return self.findFather(a)  == self.findFather(b)

    def union(self,a,b):
        if a == None or b == None:
            return
        aDai = self.findFather(a)
        bDai = self.findFather(b)
        if aDai is not bDai:    # 如果ab代表点不是同一个点,说明不在一个集合中
            aSetSize = self.sizeMap.get(aDai)
            bSetSize = self.sizeMap.get(bDai)
            if aSetSize <= bSetSize:
                self.fatherMap.update({aDai:bDai})
                self.sizeMap.update({bDai,aSetSize + bSetSize})
                self.sizeMap.pop(aDai)
            else:
                self.fatherMap.update({bDai:aDai})
                self.sizeMap.update({aDai,aSetSize + bSetSize})
                self.sizeMap.pop(bDai)

def KruskalMST(graph):
    unionFind = UnionFind()
    unionFind.makeSets(graph.nodes.values())    # 把并查集初始化,建立起来
    priorityQueue = []

    for edge in graph.edges:   # 所有边都进入优先级队列
        heapq.heappush(priorityQueue,edge)
    result = set()
    while priorityQueue:
        edge = priorityQueue.pop(0)
        heapq.heapify(priorityQueue)
        if unionFind.isSameSet(edge.froms,edge.to):   # 这条边的两个点是不是在同一个集合中
            result.add(edge)                          # 不在就加入
            unionFind.union(edge.froms,edge.to)       # 使这两个点组成一个集合

    return result




优先队列每弹出一个值,就需要修复一下,但添加元素好像是自动修复的

2.7 最小生成树算法Prim

import main
import heapq
# 最小生成树——prim算法

def primMST(graph):
    # 解锁的边进入小根堆
    priorityQueue = []
    # 哪些点被解锁出来了
    nodeSet = set()
    result = set()   # 依次挑选的边在result里

    for node in graph.nodes.values():   # 遍历图的每一个点,字典加括号
        # node 是开始点
        if node not in nodeSet:    # 如果点不存在就把点放入点合集
            nodeSet.add(node)
            for edge in node.edges:    # 由一个点,解锁所有相连的边
                heapq.heappush(priorityQueue,edge)

            while priorityQueue:
                edge = priorityQueue.pop(0)         # 弹出解锁的边中,最小的边
                toNode = edge.to                    # 可能的一个新的点,得到最小权重边指向的点
                if toNode not in nodeSet:           # 不含有的时候,就是新的点
                    nodeSet.add(toNode)
                    result.add(edge)
                    for nextEdge in toNode.edges:      # 下一个点的附件边全解锁
                        # 把下一个点的边加入到优先级队列中
                        heapq.heappush(priorityQueue,nextEdge)   # 边加边修复优先级,保证第一个边为最小权重


    return result

2.8 Dijkstra算法

  1. Dijkstra算法必须指定一个源点
  2. 生成一个源点到各个点的最小距离表,一开始只有一条记录,即原点到自己的最小距离为0,源点到其他所有点的最小距离都为正无穷大
  3. 从距离表中拿出没拿过记录里的最小记录,通过这个点发出的边,更新源点到各个点的最小距离表,不断重复这一步
  4. 源点到所有的点记录如果都被拿过一遍,过程停止,最小距离表得到了
# Dijkstra算法

def Dijkstra1(start):
    # 从start出发到所有点的最小距离
    # key:从start出发到达key
    # value:从start出发到达key的最小距离
    # 如果在表中,没有T的记录,含义是从start出发到T这个点的距离为正无穷
    distanceMap = dict()
    distanceMap.update({start:0})
    # 已经求过距离的节点,存在selectedNodes中,以后再也不碰
    selectedNodes = set()    # 放到这个表中,表示已经被锁

    # 得到最小距离的点,并且不被锁
    minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes)

    while minNode:                             # minNode是连接点
        # 原始点 -> minNode(跳转点)最小距离distance
        distance = distanceMap.get(minNode)    # 取到最小值
        for edge in minNode.edges:             # 取边
            toNode = edge.to                   # 取出指向下一个节点的权重,边的权重
            if toNode not in distanceMap:
                distanceMap.update({toNode:distance + edge.weight})   # 第一次建立距离
            else:   # 有记录,看看能不能更新距离
                distanceMap.update({edge.to,
                                    min(distanceMap.get(toNode),distance + edge.weight)})
        selectedNodes.add(minNode)
        minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes)

    return distanceMap


def getMinDistanceAndUnselectedNode(distanceMap,touchedNodes):  # (dict,set)
    minNode = None
    minDistance = 999999999   # 取一个整数最大值
    for i in distanceMap:
        node = i          #  默认取key
        distance = distanceMap.get(i)
        # 没有碰过的点 并且 记录最小
        if node in touchedNodes and distance < minDistance:
            minNode = node
            minDistance = distance

    return minNode

改进后的代码(用堆)

# Dijkstra2算法
# 改进,利用堆排序,并且新加三种方法,新加,更新和忽视

class NodeHeap:
    def __init__(self):
        # 实际的堆结构
        self.nodes = []
        # {某一个节点node:上面数组中的位置},如果位置是-1,说明之前进过堆
        self.heapIndexMap = dict()
        # {某一个节点,value 从源节点出发到该节点的目前最小距离}
        self.distanceMap = dict()
        # 堆上有多少个点,记录位置
        self.size = 0

    # 是否进入过堆结构
    def isEntered(self,node):
        return node in self.heapIndexMap

    # 判断一个节点是否在堆上
    def inHeap(self,node):
        # 如果在堆上且堆上位置不等于-1,说明在堆上
        return self.isEntered(node) and self.heapIndexMap.get(node) != -1

    # 有一个点叫node,现在发现了一个从源节点出发达到node的距离为distance
    # 判断要不要更新,如果需要的话就更新
    # node有一条新距离是distance
    # 如果没有记录就新加,有老记录的话要比对最小值作为它的距离,其他忽视
    def addOrUpdateOrIgnore(self,node,distance):
        if self.inHeap(node):   # 节点在堆上
            self.distanceMap.update({node:min(self.distanceMap.get(node),distance)}) # 更新
            self.insertHeapify(node,self.heapIndexMap.get(node))    # 从现在的位置,往上修复
        if not self.isEntered(node):        # 从来没有进入堆
            self.nodes.append(node)
            self.heapIndexMap.update({node:self.size})
            self.distanceMap.update({node:distance})
            self.insertHeapify(node,self.size)        # 插入节点并修复成小根堆
            self.size = self.size + 1
    # 如果节点进来过,但不在堆上就直接忽视,直接返回

    # 既要在堆上交换,也要在堆位置表上交换
    def swap(self,index1,index2):
        self.heapIndexMap.update({self.nodes[index1]:index2})    # 使heapIndexMap保持小根堆
        self.heapIndexMap.update({self.nodes[index2],index1})
        tmp = self.nodes[index1]                                 # 使节点数组保证小根堆
        self.nodes[index1] = self.nodes[index2]
        self.nodes[index2] = tmp

    # 小根堆
    def pop(self):
        nodeRecord = NodeRecord(self.nodes[0],self.distanceMap.get(self.nodes[0]))   # 记录点
        self.swap(0,self.size - 1)    # 第一位和最后一位做交换
        self.heapIndexMap.update({self.nodes[self.size - 1]:-1})   #  -1是标志,表示曾经进来过
        self.distanceMap.pop(self.nodes[self.size - 1])
        # c语言和c++就需要把原本堆顶点析构
        self.nodes[self.size - 1] = None   # 节点数组减少一个
        self.size = self.size - 1          # 节点数量减少
        self.heapify(0,self.size)    # 重新调整为小根堆,毕竟删除了数
        return nodeRecord

    def insertHeapify(self,node,index):   # 插入小根堆需要根据数值,进行顺序插入
        while self.distanceMap.get(self.nodes[index]) < self.distanceMap.get(self.nodes[(index - 1) / 2]):
            self.swap(index,(index - 1) / 2)
            index = (index - 1) / 2

    def heapify(self,index,size):
        left = index * 2 + 1
        while left < size:
            if (left + 1 < size and
                    self.distanceMap.get(self.nodes[left + 1]) <
                    self.distanceMap.get(self.nodes[left])):
                smallest = left + 1
            else:
                smallest = left
            if self.distanceMap.get(self.nodes[smallest]) >= self.distanceMap.get(self.nodes[index]):
                smallest = index

            if smallest == index:
                break
            self.swap(smallest,index)
            index = smallest
            left = index * 2 + 1

    def isEmpty(self):
        return self.size == 0

class NodeRecord:
    def __init__(self,node,distance):
        self.node = node
        self.distance = distance


def Dijkstra2(start):
    nodeHeap = NodeHeap()    # 建立堆结构,初始化
    nodeHeap.addOrUpdateOrIgnore(start,0)   # 把起始点传过去
    result = dict()           # 结果是个字典类型
    while nodeHeap.isEmpty():           # 起始点已经进入,开始遍历剩下的点
        record = nodeHeap.pop()    # record是NodeRecord类型
        cur = record.node          # 对当前节点进行操作
        distance = record.distance
        for edge in cur.edges:
            nodeHeap.addOrUpdateOrIgnore(edge.to,edge.weight + distance)
        result.update({cur:distance})

    return result

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值