算法刷题重温(六): 深度优先(DFS)和广度优先(BFS)

本文详细探讨了深度优先搜索(DFS)和广度优先搜索(BFS)在解决图和树相关问题中的应用。DFS常用于判断两点间是否存在路径,而BFS则常用于寻找最短路径。文中列举了多个典型题目,如岛屿数量、岛屿周长、岛屿最大面积等,并提供了相应的DFS和BFS解题框架。同时,还介绍了单词搜索和基因变化等题目,展示了如何在不同场景下灵活运用这两种遍历方法。最后,总结了几个经典BFS题目,如最小基因变化和单词接龙,强调了在特定问题中构建图和理解BFS搜索过程的重要性。

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

1. 写在前面

今天复习深度优先和广度优先遍历,这两个也是非常重要的思想,应用最多的就是对树和图的相关遍历了。

  • 深度优先遍历DFS解决的是连通性的问题,即给定两个点,一个是起始点,一个是终止点,判断是不是有一条路径从起点到终点(这里的起点和终点也可以指起始状态和最终状态)。问题的要求并不在乎是长还是短,只在乎有没有,有时候题目也会要求把找到的路径完整的打印出来
  • 广度优先遍历BFS一般用来解决最短路径的问题。和深度优先搜索不同,广度优先的搜索是从起始点出发,一层一层地进行,每层当中的点距离起始点的步数都是相同的,当找到了目的地之后就可以立即结束。广度优先的搜索可以同时从起始点和终点开始进行,称之为双端 BFS。这种算法往往可以大大地提高搜索的效率

关于深度优先遍历和广度优先遍历,这里又可以使用框架式的思维了, 下面的三个代码框架要学会默写了哈哈。

首先,深度优先遍历的两个框架(递归和非递归):

visited = set()

def dfs(node, visited):
	if node in visited:   # terminator
		# already visited
		return
	
	visited.add(node)
	
	# process currrent node here.
	for next_node in node.children():
		if not next_code in visited:
			dfs(next_code, visited)

非递归写法需要手动维护一个栈:

def dfs(self, tree):
	
	if not tree.root:
		return
	
	visited, stack = [], [tree.root]
	while stack:
		node = stack.pop()
		visited.add(node)

		process(node)
		nodes = generated_related_nodes(node)
		stack.push(nodes)

广度优先遍历的一个框架,之前树那里其实也见识过了:

def bfs(graph, start, end):
	queue = []
	queue.append([start])
	visited.add(start)

	while queue:
		node = queue.popleft()
		visited.add(node)
		process(node)
		nodes = generate_related_nodes(node)
		queue.addend(nodes)

这三个框架式的代码默写完毕, 当然, 这里的dfs,bfs其实并不能完全用上面的这种框架解题,上面这两个框架更像是一种思路框架,而不是具体的解题框架,下面针对了具体的BFS和DFS整理了个解题框架。比如网格化的DFS和图上的BFS。下面就学习具体题目进行体会了。

PS: 下面的第二部分是我目前做过的题目整理(思路和代码),第三部分是第二部分的题目汇总,只有题目,这个方便复习用,如果是为了回顾思路和复习,直接看第三部分,如果想不起来思路和代码了,再瞟一眼第二部分哈哈,尽量不要从头开始读,那样会非常难受,并且失去了刷题复习的趣味性了!

2. 题目思路整理

2.1 DFS系列

DFS这一块, 除去树那边的那一系列, 发现最常考的就是网格类的DFS算法了, 也就是岛屿的相关问题了。四个经典岛屿题目:

我们所熟悉的DFS问题通常是在树或者图结构上进行的, 而岛屿问题的解题思路, 采用的DFS是从网络结构中进行的。下面是岛屿问题解题汇总,参考的这篇题解

网格问题的基本概念:

网格问题是由 m × n m×n m×n个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。

岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。

在这里插入图片描述
在这样一个设定下,就出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。不过这些问题,基本都可以用 DFS 遍历来解决。 当然有些也可以用BFS来解决, 但现在的情况是哪个能记住就用哪个了哈哈。

DFS的基本结构:

网格结构要比二叉树结构稍微复杂一些,它其实是一种简化版的图结构。要写好网格上的 DFS 遍历,我们首先要理解二叉树上的 DFS 遍历方法,再类比写出网格结构上的 DFS 遍历。我们写的二叉树 DFS 遍历一般是这样的:

def traverse(root):
	
	if root == NULL:
		return
    # 访问相邻节点: 左子节点和右子节点
	traverse(root.left)
	traverse(root.right)

可以看到,二叉树的DFS有两个要素: 访问相邻节点和判断base case:

  • 第一个要素是访问相邻结点。二叉树的相邻结点非常简单,只有左子结点和右子结点两个。二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的 DFS 遍历只需要递归调用左子树和右子树即可。
  • 第二个要素是 判断base case。一般来说,二叉树遍历的 base case 是 root == null。这样一个条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。另一方面,在 root == null 的时候及时返回,可以让后面的 root.leftroot.right 操作不会出现空指针异常。

对于网格上的DFS, 我们可以参考二叉树的, 写出网格DFS的两个要素:

  1. 首先, 网格结构中的格子有多少相邻节点? 答案是上下左右四个。 对于格子(r,c)来说(r和c分别代表行坐标和列坐标), 四个相邻格子分别是(r-1,c), (r+1, c), (r, c-1), (r, c+1)
    在这里插入图片描述
  2. 网格中的base case是什么? 从二叉树的 base case 对应过来,应该是网格中不需要继续遍历、grid[r][c]会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。
    在这里插入图片描述
    这一点稍微有些反直觉,坐标竟然可以临时超出网格的范围?这种方法称为「先污染后治理」—— 甭管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。

这样, 就得到了网格DFS遍历的框架代码:

# 判断是否越界
def InArea(self, grid, r, c):
	return r >= 0 and r < len(grid) and c >= 0 and c < len(grid[0])

# 网格内的DFS算法
def dfs(self, visited, grid, r, c):
	
	# 结束条件
	if not self.InArea(grid, r, c):
		return 
	
	# 访问相邻陆地
	self.dfs(visited, grid, r-1, c)   # 上
	self.dfs(visited, grid, r+1, c)   # 下
	self.dfs(visited, grid, r, c-1)   # 左
	self.dfs(visited, grid, r, c+1)   # 右

但上面有个问题就是还没有处理重复遍历,所以还需要标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

  • 0 —— 海洋格子
  • 1 —— 陆地格子(未遍历过)
  • 2 —— 陆地格子(已遍历过)

当然, 我们这里不想直接改里面的数, 所以采用了一个visited来标记是否已经被访问过。 这里一定要慎重改人家原来的数组。 所以最后的框架代码如下(这个也是应该会默写)

# 网格内的DFS算法
def dfs(self, visited, grid, r, c):

    # 结束条件
    if not self.InArea(grid, r, c):
        return

    # 如果是海洋 后者是已经被访问过的陆地, 那么直接返回
    if grid[r][c] == "0" or visited[r][c]:
        return 

    # 访问当前陆地
    visited[r][c] = True

    # 访问相邻陆地
    self.dfs(visited, grid, r-1, c)       # 上
    self.dfs(visited, grid, r+1, c)       # 下
    self.dfs(visited, grid, r, c-1)       # 左
    self.dfs(visited, grid, r, c+1)       # 右

铺垫结束, 下面开始干上面的四个题目了。

  • LeetCode200: 岛屿数量: 有了上面的DFS网格遍历框架,这个题目就比较简单了, 思路就是遍历网格内的每一块, 如果发现是陆地且没有被访问过它, 那么就dfs这块陆地,得到一块岛屿,岛屿数量加1,这样最后就能统计出岛屿的数量来了。
    在这里插入图片描述

  • LeetCode463: 岛屿周长: 这个题目可以用上面的这个代码框架解决,这里要明确个问题,就是岛屿的周长计算方式:岛屿的周长是计算岛屿全部的「边缘」,而这些边缘就是我们在 DFS 遍历中,dfs函数返回的位置
    在这里插入图片描述

    • 当我们的 dfs 函数因为「坐标 (r, c) 超出网格范围」返回的时候,实际上就经过了一条黄色的边;
    • 而当函数因为「当前格子是海洋格子」返回的时1候,实际上就经过了一条蓝色的边。


    所以上面的代码拿过来,修改三个地方就可以啦,框架式思维的强大之处:

    在这里插入图片描述
    当然,这个方法并不是解决这个题的最快方式,因为该岛屿是封闭的,所以每一条上边必定对应一条下边, 每一条左边必定对应一条右边。 因此只需要关注上边和左边即可。而上面和左边的条件是:

    1. 遍历到的块本身得是陆地
    2. 在上面的条件下, 它上面是grid的边界或者是水, 或者它左边是grid的边界或者是水

    所以代码可以是这样:

     def islandPerimeter(self, grid: List[List[int]]) -> int:
            # 记录row和cols
            rows = len(grid)
            if rows == 0:
                return 0
            cols = len(grid[0])
            circum = 0
            # 遍历一遍
            for i in range(rows):
                for j in range(cols):
                    # 如果遇到了陆地 land++
                    if grid[i][j] == 1:
                        # 上面的判断: 边界或者他上面是水
                        if i == 0 or grid[i-1][j] == 0:
                            circum += 2
                        # 左边判断: 边界或者左边是水
                        if j == 0 or  grid[i][j-1] == 0:
                            circum += 2
            return circum
    

    拓展思路可以,但通用性不强。

  • LeetCode695: 岛屿的最大面积: 这个题目依然是网格化的dfs遍历框架, 对于每一块岛屿,要返回面积了,然后更新最大面积即可。
    在这里插入图片描述
    先看个变形题目。

  • 剑指offer13: 机器人的运动范围: 这个题目和上面这个求面积的非常像,这里机器人也是上下左右移动,求的是从(0,0)点出发可以达到的最大格子数,其实就是求从(0,0)出发,围成的区域的最大面积,所以上面的代码稍微改一下就可以搞定。 首先,起止点只有(0,0),所以主函数里面的格子双重遍历不用。其次是这里的可行范围,加了一个数位坐标之和不能大于给定的k,这个需要判断。
    在这里插入图片描述

  • LeetCode827: 最大人工岛: 这个题目要分为两步:

    1. 对格子中的每个块进行遍历, 用dfs算法找到所有的岛屿, 并在dfs的同时, 对每个格子用岛屿的索引下标进行标记, 且还需要计算每个岛屿的面积, 建立索引 --> 面积的映射, 这个用上面的dfs框架就可以搞定

    2. 对格子中的每个块再进行遍历一遍, 对于每个块,尝试填充, 然后计算可以获得的最大面积, 返回最大的那个。 这里面涉及到了计算获得的最大面积, 这个的思路也很简单,对于当前的海洋块,看他的上下左右, 是否是陆地, 如果是的话, 把陆地的面积加入到set集合里面, 最后算出填上它之后的大陆总面积。

      在这里插入图片描述
      这个代码看起来挺多,其实理解起来并不是多么困难。主要是填海的这个思路比较巧妙。

再进一步, DFS+回溯的经典题目还有,要和上面的岛屿问题进行对比。

  • LeetCode79: 单词搜索: 这个题对应剑指offer的剑指offer12: 矩阵中的路径, 是在一个二维的表格中,看有没有一条字符组成的路径正好组成某个给定的单词序列。 这个题的解题思路需要网格化的DFS+回溯的思路。 网格化的DFS是在某个字符出发,上下左右去查相邻字符,而回溯是为了之前的字符重用。 因为这个题目里面字符的顺序不一致,并且可以从任意位置出发,这就需要保证字符的重用性,也就是换个起始点之后,要保证所有单词依然是都没有使用过才行。 这个和岛屿那个不同。 那个找完了一块岛屿之后,就不用管了,直接找下一块即可。所以那里面并没有回溯。 整理思路依然是要有一个visited数组标记是否已经访问,然后遍历每个格子,看看以当前格子的字符为起始字符最终能不能找到给定单词。 dfs里面的逻辑就是如果当前位置不合法,返回,如果到了word的最后一个字符,看看是否相等。如果当前不匹配返回。 遍历所有可能,然后回溯。

    在这里插入图片描述

2.2 BFS系列

BFS这一块的大部分经典题目都在二叉树那里总结了,那些都是常考的在二叉树上BFS遍历的题目, 这里整理三道不太一样的广度优先遍历题目。

  • LeetCode433: 最小基因变化:给定一个初始串,一个终止串,起始串的每个位置只有四种变化的可能,且针对于一次可能,比如位于基因库, 问能够变化到终止串的最小步数。 这其实是个典型的广度优先遍历。 思路就是从头开始, 每个元素位置从["A", "G", "C", "T"]四个里面变化, 每次变化一个字符, 然后去基于库里面找是否有, 如果有的话, 把该元素从基因库里面删除, 把这个新的序列加入到队列当中, 步数加1.当出队的字符串等于了终止字符串, 返回结果即可。
    在这里插入图片描述

这个题目可能一开始没有往BFS上想,其实DFS+回溯也可以搞定,但感觉那种不太好想。这个题目其实也可以画个图看看的。现在的问题是这个图怎么画出来呢? 就是上面的遍历位置,然后不断尝试了。只要有了这个图,BFS就搞定, 如果这个体会不明显,再看看下面这个就深刻了。
在这里插入图片描述

  • LeetCode127: 单词接龙: 这个和上面类似的题目,依然是BFS遍历。只不过这里的尝试可能性从4到了26。所以拿上面的代码稍微改下就能A掉这个题目。
    在这里插入图片描述

    如何构造图是本题的关键。我们把每个单词看成图中的顶点, 如果两个顶点之间相差一个字母, 这时候就可以连一条线构成边,表示这两个节点之间构成一条路,即题目中的改变一个字母就能进行转换。 这时候我们就可以得到像下面这样的无向图:
    在这里插入图片描述
    再往上一层,就是这两个题的BFS遍历的特点:对于当前的每个节点,我们是无法直接获取到其相邻节点的。这个不像普通的bfs遍历,是直接能够找到相邻节点的(比如二叉树的root.left, root.right,N叉树的root.children),而这里的这两个题目, 是需要通过遍历的方式去找相邻节点的,也就是一开始我们并不知道当前节点的相邻节点是啥,只知道相邻节点存在于某些可能里面。我们只有遍历这很多种可能才能找到相邻节点,进而套用我们的BFS框架。

所以这里,我又基于前面泛化的那个BFS框架进行了一些补充,形成自己的一个BFS框架:

from collections import deque
def bfs(graph, start, end):
	# 定义队列和标记变量
	d =deque([start])
	visited = set(start)
	step = 1

	while d:
		# 记录当前圈能遍历的个数
		cur_size = len(d)
		for _ in range(cur_size):
			node = d.popleft()
			# 处理当前层的逻辑 
			process(node)
			# 寻找与当前节点的相邻节点,也就是下一圈的所有节点,然后入队
			# 这里不同的题目就不太一样了 一般也是进行搜索
			next_nodes = generate_related_nodes(node)  # 这里面要避免重复访问 visited控制
			d.append(next_nodes)
			visited.add(next_nodes)

当然,这里还有种更为强大的思路叫做双向的BFS搜索。已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集。

就拿单词接龙这个题来说,可以从两边进行BFS, 当然这个的实现过程也是交替进行,每次从单词量小的集合进行扩散。 由于双向BFS,这里的数据结构就不能用向队列了,而是换成了两个集合beginVisited和endVisited。当然那个标记是否访问的visited还在,每次扩散也要加入到这个里面去。下面就看下这个代码,里面加了详细的注释:
在这里插入图片描述
单向BFS和双向BFS的感觉:
在这里插入图片描述
下面再看看单词接龙升级版本,单词接龙II,这个需要在上一个版本的基础上改一些地方。这个题后面的题解中有DFS+BFS的结合思路,但这里我不整理这个了,代码量太大,到时候不一定能写出来,还是看个新的方法。

  • LeetCode126: 单词接龙II: 这里依然是上面的代码框架,但是需要改些地方, 首先,这里的队列里面,存放的并不是某个节点, 而是直接某条路径。 这样的话,在遍历的同时,就能把路径都给存储下来。下面是整体的一个思路:

    1. 在单词接龙的基础上,需要将找到的最短路径存储下来
    2. 之前的队列只用来存储每层的元素,那么现在就能存储每层添加元素之后的结果:比如起始点"ab", 终止点"if", 词典{"cd", "af", "ib", "if"},则队列里面的元素是下面这种了:
      1. 第一层:{“ab”}
      2. 第二层:{“ab”, “af”}, {“ab”, “ib”}
      3. 第三层: {“ab”, “af”, “if”}, {“ab”, “ib”, “if”}
    3. 如果该层添加的某个单词符合目标单词,则该路径为最短路径,该层为最短路径所在的层,但此时不能直接返回结果,必须将该层遍历完,将该层符合的结果都添加进结果集
    4. 每层添加单词的时候,不能直接添加到总的已访问单词集合中,需要每层有一个单独的该层访问的单词集,该层结束之后,再汇合到总的已访问的单词集合中,原因就是因为3。
      在这里插入图片描述
      所以在BFS的时候,不像单词接龙I那样, 如果找到了某个元素rex的下一层的节点tex,就把tex放到visited里面就完事, 这里还涉及到了当前层队列中的其他元素tedtex也可能有联系。 这种路径也需要考虑进去。 所以需要在寻找下一层节点的时候, 要单独的设置一个level层级的visited, 只标记某一层的节点是否被重复访问过。当遍历完当前层的所有节点之后, 再把这个层级的visited合并到总的visited里面去。

    梳理完了之后,就是写代码了:
    在这里插入图片描述
    这个要比BFS+DFS的那种慢很多,不是慢在时间复杂度上,而是慢在保存结构的时候, 列表的复制操作上,这个在保存结果的时候,path[:]是需要将列表进行复制的。 BFS+DFS的思路是在这个的基础上,不是入队列表,然后是入队单词, 但是这个会用BFS把下一层的节点保存到邻接表里,然后再用DFS进行遍历,得到结果。这个好处是可以用双向的BFS,这个就能达到最优解了。但是不想记录了,多了到时候记不住。虽然不是死记硬背,但这种双向BFS的思路,解决这种题目有很多细节很容易忘,尤其是输出所有最短路径这个, 和计算步长还不是一个难度水平,这个为了能够输出最短路径,得记录前驱后继节点,双向BFS这里还两边交替着来,前驱后继还得用标记记录下,所以细节太多了这里,即使弄明白思路,到时候真不能保证A掉,dfs到时候写的写不出来也是问题。所以先框架式思维搞定题目,在这个基础上,尝试优化一点点就好。 有没有发现, 这三个题目我都用了一个招式来写的哈哈? 只不过难度层层递进了些罢了 😉

最后这里还要补充一个图的DFS框架, 因为发现DFS和BFS这里思想都一样,但是不同的数据结构和场景, 写法还是不太一样的, 在图里面, DFS常常用来解决从起始点到终止点的路径情况。

def dfs(begin, end, successors, path, res):
	# 递归结束
	if begin == end:
		res.append(path[:])
		return
	
	if begin not in successors:  return 

	# 找到起始点的所有后继节点
	successors_nodes = successors[begin]
	for next_node in successors_nodes:
		path.append(next_node)
		dfs(next_node, end, successors, path, res)
		path.pop()

其实就是个回溯了。

补充一个迷宫问题的题目:

给定一个 n*m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中0表示可以走的路,1表示不可通过的墙壁。

最初,有一个人位于 start 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从 start 移动至 end 处,至少需要移动多少次。

数据保证 start 处和 end 处的数字为0,且至少存在一条通路。

输入格式
第一行包含两个整数n和m。
接下来n行,每行包含m个整数(0或1),表示完整的二维数组迷宫。

输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围1≤n,m≤1001≤n,m≤100

输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0

输出样例:8

由于这个题目是要求最少移动次数,所以这是一个最短路径的题目。而和上面图里面求最短路径的又有点不太一样, 这个是网格上的一种BFS了,思路是这样:

  1. 起始点坐标加入队列,设置距离为0
  2. 当队列不是空的时候, 出队一个节点,去他的上下左右(必须是可访问), 把相应节点加入队列,距离+1
  3. 如果到达终点,返回0

代码如下:

from collections import deque
def solve(arr, start, end, visited):
    """"BFS求最短路径,这里的start是起始坐标,end是终止坐标,(x,y)的形式"""

    queue = deque([])
    queue.append((start[0], start[1], 0))
    visited[start[0]][start[1]] = True

    dx = [-1, 0, 1, 0]
    dy = [0, -1, 0, 1]
    while queue:
        x, y, dis = queue.popleft()
        # 如果遇到终点了,返回
        if x == end[0] and y == end[1]:
            return dis
        
        # 遍历上下左右四个点
        for i in range(4):
            nx = x + dx[i]
            ny = y + dy[i]
            if 0 <= nx < len(arr) and 0 <= ny < len(arr[0]) and arr[nx][ny] == 0 and not visited[nx][ny]:
                queue.append((nx, ny, dis+1))
                visited[nx][ny] = True

if __name__ == '__main__':

    m, n = input().split(' ')
    m, n = int(m), int(n)
    arr = []
    for i in range(m):
        arr.append(list(map(int, input().split(' '))))

    # 起始点和终止点  这个代码可以到任意终点
    start, end = (0, 0), (m-1, n-1)
    visited = [[False] * n for _ in range(m)]
    res = solve(arr, start, end, visited)
    print(res)

题目要求最短路径,为什么代码中没有 m i n min min的体现呢?

答:因为广度优先搜索其实已经有寻找到最短路径的功能了,可以理解为总部同时派出多路兵马并行搜索(各路兵马在分岔口又再分出多路并进),并且总部发出命令时,每一路兵马都只前进一步,那么哪一路兵马先到达目的地,则该路线肯定是最近的。

3. 小总

dfs和bfs这边的题目变式也挺固定了,有三个非常重要的框架需要会默写, 上面的题目进行汇总如下, 方便后面的复习想思路用,分类整理如下:

dfs这里主要是网格类的dfs,复习了四个岛屿的相关问题:

bfs这里复习最短路径为主的经典题目:

DFS和BFS就简单复习这些题目,下一个专题是二分查找,这里有两个代码框架,记好了就能解题, 也快速的过一遍,今天听师兄师姐们说面试重点考动态规划专题,而这个我还一遍也没刷,而是先把前面的专题过了两遍,感觉有点跑偏了,所以过完二分查找之后,集中突击一遍动态规划,然后直接去面试了,这个怎么算也来不及重温一遍了,只能希望遗忘的速度不会那么快了哈哈。 除了动态规划,前面简单的数据结构题目(链表,队列,栈,字符串)和排序这两块, 会在学习动态规划的同时顺便穿插进来, 这样就整体上摸了一遍常考的题目。 只要整体上摸完一遍,后面反复刷这些就完事了哈哈。 继续Rush 😉

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值