竞赛常考的知识点大总结(三)搜索

基本DFS

深度优先搜索(Depth-First Search,简称DFS)是一种用于遍历或搜索树或图的算法。在DFS中,算法从一个节点开始,尽可能深地搜索图的分支。当节点v的所在边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这个过程一直进行到已发现从源节点可达的所有节点为止。

DFS的特点:

1.递归实现:DFS通常使用递归的方式实现,因为它自然地适合递归的逻辑。

2.栈空间:在非递归实现中,DFS使用栈来模拟递归过程。

3.遍历顺序:DFS可以按照前序、中序或后序遍历图或树。

4.回溯:在搜索过程中,当节点的所有邻接点都已被探寻过,搜索将回溯到发现该节点的那条边的起始节点。

5.图的连通性:DFS可以用来检测图的连通性,即判断图中任意两个节点是否连通。

DFS的常见用法:

1.图的遍历:遍历图中的所有节点。

2.图的连通性检测:检测图中任意两个节点是否连通。

3.拓扑排序:在有向无环图(DAG)中进行拓扑排序。

4.解决迷宫问题:在迷宫中找到从起点到终点的路径。

5.解决岛屿问题:在二维网格中找到所有连通的岛屿。

DFS的经典C语言例题:

题目:使用DFS遍历图的所有节点。

示例代码

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

// 图的结构体
typedef struct Graph {
     int V; // 顶点数量
     int** adjMatrix; // 邻接矩阵
} Graph;

// 创建图的函数
Graph* createGraph(int V) {
     Graph* graph = (Graph*)malloc(sizeof(Graph));
     graph->V = V;
     graph->adjMatrix = (int**)malloc(V * sizeof(int*));
     for (int i = 0; i < V; i++) {
         graph->adjMatrix[i] = (int*)malloc(V * sizeof(int));
          for (int j = 0; j < V; j++) {
               graph->adjMatrix[i][j] = 0;
           }
       }
     return graph;
}

// 添加边的函数
void addEdge(Graph* graph, int src, int dest) {
     graph->adjMatrix[src][dest] = 1;
     graph->adjMatrix[dest][src] = 1; // 无向图
}

// DFS遍历函数
void DFS(Graph* graph, int v, int visited[]) {
     visited[v] = 1;
     printf("%d ", v);
     for (int i = 0; i < graph->V; i++) {
          if (graph->adjMatrix[v][i] == 1 && !visited[i]) {
               DFS(graph, i, visited);
           }
       }
}

int main() {
     Graph* graph = createGraph(4);
     addEdge(graph, 0, 1);
     addEdge(graph, 0, 2);
     addEdge(graph, 1, 2);
     addEdge(graph, 2, 0);
     addEdge(graph, 2, 3);
     addEdge(graph, 3, 3);

     int* visited = (int*)malloc(graph->V * sizeof(int));
     for (int i = 0; i < graph->V; i++) {
          visited[i] = 0;
       }

     printf("Depth First Traversal (starting from vertex 2):\n");
     DFS(graph, 2, visited);

     free(graph->adjMatrix[0]);
     free(graph->adjMatrix);
     free(graph);
     free(visited);
     return 0;
}

例题分析:

1.创建图createGraph函数创建一个图的结构体,包括顶点数量和邻接矩阵。

2.添加边addEdge函数向图中添加边。

3.DFS遍历DFS函数实现深度优先搜索算法。函数使用一个数组visited来标记每个顶点是否被访问过。

4.主函数:在main函数中,创建了一个图,并添加了一些边。调用DFS函数从顶点2开始进行深度优先遍历,并打印遍历结果。

这个例题展示了如何在C语言中使用DFS遍历图的所有节点。通过这个例子,可以更好地理解DFS在遍历图中的应用,以及如何使用邻接矩阵来存储图的信息。DFS是一种递归算法,它从一个顶点开始,尽可能深地搜索图的分支,直到所有的顶点都被访问过。在遍历过程中,DFS会回溯到上一个顶点,然后继续探索未被访问的邻接顶点。

DFS记忆化搜索

DFS记忆化搜索(DFS Memoization)是深度优先搜索(DFS)与记忆化(Memoization)技术的结合。记忆化是一种优化技术,用于存储子问题的解,以避免重复计算。在DFS中应用记忆化,可以显著提高算法的效率,特别是在解决具有重叠子问题的动态规划问题时。

特点:

1.记忆化存储:DFS记忆化搜索使用一个数据结构(如数组或哈希表)来存储已经解决的子问题的解。

2.避免重复计算:当遇到相同的子问题时,直接从记忆化存储中获取结果,而不是重新计算。

3.提高效率:通过避免重复计算,DFS记忆化搜索可以显著减少计算时间,提高算法效率。

4.空间换时间:记忆化存储需要额外的空间来存储子问题的解,这可能会增加空间复杂度。

常见用法:

1.动态规划:在动态规划问题中,DFS记忆化搜索用于存储中间状态的解,以避免重复计算。

2.计算斐波那契数列:使用DFS记忆化搜索可以高效地计算斐波那契数列的值。

3.解决棋盘游戏问题:如八皇后问题、N皇后问题等,可以使用DFS记忆化搜索来避免重复搜索。

4.图遍历问题:在图遍历问题中,DFS记忆化搜索可以用来避免重复访问节点。

经典C语言例题:

题目: 使用DFS记忆化搜索计算斐波那契数列的第n项。

示例代码:

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

// 计算斐波那契数列的第n项,使用DFS记忆化搜索
int fibonacci(int n, int* memo) {
    if (n <= 1) {
         return n;
      }
      // 检查是否已经计算过
      if (memo[n] != -1) {
          return memo[n];
      }
      // 计算并存储结果
      memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
      return memo[n];
}

int main() {
    int n = 10;
    int* memo = (int*)malloc((n + 1) * sizeof(int));
    memset(memo, -1, (n + 1) * sizeof(int));
    printf("Fibonacci number at position %d is %d\n", n, fibonacci(n, memo));
    free(memo);
    return 0;
}

例题分析:

1.DFS记忆化搜索函数fibonacci函数接受一个整数n和一个整数数组memo作为参数。memo数组用于存储已经计算过的斐波那契数列的值。

2.递归计算:函数首先检查n是否小于或等于1,如果是,则直接返回n。否则,函数检查memo[n]是否已经被计算过,如果是,则直接返回memo[n]

3.计算并存储结果:如果memo[n]没有被计算过,则递归地调用fibonacci函数计算n-1n-2的斐波那契数,并将结果存储在memo[n]中。

4.主函数:在main函数中,定义了一个整数n和一个整数数组memo。调用fibonacci函数计算斐波那契数列的第n项,并打印结果。

这个例题展示了如何在C语言中使用DFS记忆化搜索来计算斐波那契数列的第n项。通过这个例子,可以更好地理解DFS记忆化搜索在解决动态规划问题中的应用,以及如何使用记忆化技术来提高算法的效率。DFS记忆化搜索通过存储子问题的解,避免了重复计算,从而提高了算法的效率。

DFS剪枝

DFS剪枝(DFS Pruning)是深度优先搜索(DFS)算法中的一种优化技术,用于减少搜索空间,提高搜索效率。剪枝通常在搜索过程中根据某些条件提前终止一些不必要的搜索分支,从而避免了对这些分支的进一步探索。

特点:

1.减少搜索空间:通过剪枝,可以减少DFS需要探索的节点数量,从而减少计算量。

2.提高效率:剪枝可以显著提高DFS算法的效率,尤其是在处理大规模问题时。

3.优化条件:剪枝的条件通常是基于问题的特定性质,需要仔细设计以确保不会错过正确的解。

4.启发式搜索:剪枝常常与启发式搜索结合使用,以进一步指导搜索过程,避免无用的探索。

常见用法:

1.棋类游戏:在棋类游戏的AI中,剪枝用于提前终止不可能获胜的搜索分支。

2.路径规划:在路径规划问题中,剪枝用于避免探索到死胡同的路径。

3.组合优化问题:在解决组合优化问题时,剪枝用于避免生成无效的解。

4.搜索算法:在各种搜索算法中,剪枝用于优化搜索过程,提高搜索效率。

经典C语言例题:

题目: 使用DFS剪枝技术解决八皇后问题。

示例代码:

#include <stdio.h>
#include <stdbool.h>

// 检查皇后是否可以放置在棋盘的(x, y)位置
bool isSafe(int board[][8], int x, int y) {
    for (int i = 0; i < x; i++) {
         if (board[i][y] == 1) {
              return false;
          }
       }
    for (int i = x, j = y; i >= 0 && j >= 0; i--, j--) {
         if (board[i][j] == 1) {
              return false;
           }
       }
    for (int i = x, j = y; i >= 0 && j < 8; i--, j++) {
         if (board[i][j] == 1) {
              return false;
           }
       }
    return true;
}

// 尝试在棋盘的(x, y)位置放置皇后
bool solveNQUtil(int board[][8], int x) {
     // 如果所有皇后都已放置
     if (x >= 8) {
         return true;
      }
     // 遍历每一列
     for (int y = 0; y < 8; y++) {
          // 检查是否可以放置皇后
          if (isSafe(board, x, y)) {
               // 放置皇后
               board[x][y] = 1;
               // 递归放置下一列的皇后
               if (solveNQUtil(board, x + 1)) {
                    return true;
               }
               // 如果放置皇后失败,则移除皇后
               board[x][y] = 0;
          }
     }
     // 如果没有找到解决方案,则返回false
     return false;
}

// 主函数,用于解决八皇后问题
bool solveNQ() {
     int board[8][8] = {0};
     if (solveNQUtil(board, 0) == false) {
          printf("Solution does not exist\n");
          return false;
     }
     // 打印解决方案
     for (int i = 0; i < 8; i++) {
          for (int j = 0; j < 8; j++) {
               printf(" %d ", board[i][j]);
          }
          printf("\n");
     }
     return true;
}

int main() {
     solveNQ();
     return 0;
}

例题分析:

1.检查安全位置isSafe函数用于检查在棋盘的(x, y)位置放置皇后是否安全,即是否与之前的皇后冲突。

2.递归放置皇后solveNQUtil函数是一个递归函数,用于尝试在棋盘的每一列放置皇后,并递归地在下一列放置皇后。

3.剪枝:在solveNQUtil函数中,如果在某一列中找不到安全的位置放置皇后,则直接返回false,这就是剪枝的过程,避免了对不可能的解的进一步探索。

4.主函数:在main函数中,调用solveNQ函数来解决八皇后问题,并打印解决方案。

这个例题展示了如何在C语言中使用DFS剪枝技术来解决八皇后问题。通过这个例子,可以更好地理解DFS剪枝在搜索算法中的应用,以及如何使用剪枝来提高搜索效率。DFS剪枝通过提前终止不必要的搜索分支,避免了对这些分支的进一步探索,从而减少了搜索空间,提高了搜索效率。

基本BFS

广度优先搜索(Breadth-First Search,BFS)是一种用于遍历或搜索树或图的算法。BFS从根节点开始,先遍历所有相邻节点,然后对每个相邻节点再进行同样的操作,直到所有的节点都被访问过。

特点:

1.层次遍历:BFS按照从近到远的顺序访问所有节点,即首先访问距离根节点最近的节点。

2.队列实现:BFS通常使用队列来实现,先入队的节点先出队,保证了节点的访问顺序。

3.空间复杂度:BFS的空间复杂度通常较高,因为它需要存储所有路径上的节点。

4.适用场景:BFS适用于寻找最短路径问题,如在无权图中寻找两点间的最短路径。

常见用法:

1.最短路径问题:在无权图中寻找两点间的最短路径。

2.网络爬虫:网页的广度优先遍历。

3.社交网络分析:计算社交网络中两个用户之间的最短路径。

4.层次遍历:在树或图中进行层次遍历。

经典C语言例题:

题目: 使用BFS算法在无权图中寻找两点间的最短路径。

示例代码:

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

// 定义图的结构体
typedef struct Graph {
    int V; // 顶点数量
    int** adjMatrix; // 邻接矩阵
} Graph;

// 创建图的函数
Graph* createGraph(int V) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->V = V;
    graph->adjMatrix = (int**)malloc(V * sizeof(int*));
    for (int i = 0; i < V; i++) {
         graph->adjMatrix[i] = (int*)malloc(V * sizeof(int));
         memset(graph->adjMatrix[i], 0, V * sizeof(int));
     }
    return graph;
}

// 添加边的函数
void addEdge(Graph* graph, int src, int dest) {
    graph->adjMatrix[src][dest] = 1;
    graph->adjMatrix[dest][src] = 1; // 无向图
}

// BFS函数
void BFS(Graph* graph, int start, int end) {
    int* visited = (int*)malloc(graph->V * sizeof(int));
    memset(visited, 0, graph->V * sizeof(int));

    int* parent = (int*)malloc(graph->V * sizeof(int));
    memset(parent, -1, graph->V * sizeof(int));

    int queue[graph->V];
    int front = 0, rear = -1;

    visited[start] = 1;
    parent[start] = -1;
    queue[++rear] = start;

    while (front <= rear) {
         int u = queue[front++];
         for (int v = 0; v < graph->V; v++) {
              if (graph->adjMatrix[u][v] && !visited[v]) {
                   visited[v] = 1;
                   parent[v] = u;
                   queue[++rear] = v;
                   if (v == end) {
                        printf("Shortest path from %d to %d is: ", start, end);
                        while (v != -1) {
                             printf("%d ", v);
                             v = parent[v];
                         }
                        printf("\n");
                        return;
                    }
               }
          }
     }
    printf("No path exists between %d and %d\n", start, end);
}

int main() {
    Graph* graph = createGraph(4);
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 2);
    addEdge(graph, 2, 0);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 3);

    BFS(graph, 0, 3);

    free(graph->adjMatrix[0]);
    free(graph->adjMatrix);
    free(graph);
    return 0;
}

例题分析:

1.创建图createGraph函数创建一个图的结构体,包括顶点数量和邻接矩阵。

2.添加边addEdge函数向图中添加边,对于无向图,需要添加两条边。

3.BFS函数BFS函数接受图、起始顶点和目标顶点作为参数。函数使用一个队列来实现BFS,首先将起始顶点加入队列,并标记为已访问。然后,不断从队列中取出顶点,将其相邻的未访问顶点加入队列,并记录每个顶点的前驱顶点。

4.打印最短路径:当找到目标顶点时,函数打印从起始顶点到目标顶点的最短路径。

5.主函数:在main函数中,创建了一个图,并添加了一些边。调用BFS函数寻找从顶点0到顶点3的最短路径,并打印结果。

这个例题展示了如何在C语言中使用BFS算法在无权图中寻找两点间的最短路径。通过这个例子,可以更好地理解BFS算法在图搜索中的应用,以及如何使用BFS来解决最短路径问题。BFS算法通过层次遍历图的每个节点,尝试找到目标节点,是一种非常有效的搜索算法。

连通性判断

连通性判断(Connectivity Testing)是指确定图中两个顶点是否连通的过程。在图论中,如果图中的两个顶点之间存在路径,则称这两个顶点是连通的。连通性判断通常用于无向图中,但在有向图中也可以进行类似的判断,称为强连通性判断。

特点:

1.路径存在性:连通性判断的核心是检查两个顶点之间是否存在路径。

2.无向图:在无向图中,如果两个顶点之间存在一条路径,则它们是连通的。

3.有向图:在有向图中,如果从一个顶点到另一个顶点存在一条路径,则称这两个顶点是弱连通的;如果存在双向路径,则称这两个顶点是强连通的。

4.效率:连通性判断的效率取决于所使用的算法,如DFS、BFS或并查集等。

常见用法:

1.社交网络分析:判断两个用户是否在社交网络中连通。

2.网络设计:在网络设计中,判断网络中的两个节点是否连通。

3.图的连通分量:找出图中的所有连通分量。

4.路径规划:在路径规划问题中,判断两个位置是否连通。

经典C语言例题:

题目: 使用DFS算法判断无向图中的两个顶点是否连通。

示例代码:

#include <stdio.h>
#include <stdbool.h>
#include<malloc.h>
#include<string.h>
// 图的邻接表表示
typedef struct Graph {
    int V; // 顶点数量
    struct Node** adjList; // 邻接表数组
} Graph;

// 邻接表节点
typedef struct Node {
    int vertex;
    struct Node* next;
} Node;

// 创建图的函数
Graph* createGraph(int V) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->V = V;
    graph->adjList = (struct Node**)malloc(V * sizeof(struct Node*));
    for (int i = 0; i < V; i++) {
         graph->adjList[i] = NULL;
      }
    return graph;
}

// 添加边的函数
void addEdge(Graph* graph, int src, int dest) {
    // 添加从src到dest的边
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->vertex = dest;
    newNode->next = graph->adjList[src];
    graph->adjList[src] = newNode;

    // 无向图,需要添加从dest到src的边
     newNode = (Node*)malloc(sizeof(Node));
     newNode->vertex = src;
     newNode->next = graph->adjList[dest];
     graph->adjList[dest] = newNode;
}

// DFS函数
bool DFS(Graph* graph, int v, int visited[], int start) {
     // 标记当前顶点为已访问
     visited[v] = 1;
     // 遍历邻接的顶点
     Node* adjList = graph->adjList[v];
     while (adjList != NULL) {
          int i = adjList->vertex;
          if (visited[i] == 0) {
               if (DFS(graph, i, visited, start)) {
                    return true;
               }
          }
          adjList = adjList->next;
     }
     return false;
}

// 判断连通性的函数
bool isConnected(Graph* graph, int start, int end) {
     // 初始化访问数组
     int* visited = (int*)malloc(graph->V * sizeof(int));
     memset(visited, 0, graph->V * sizeof(int));

     // 从start开始DFS
     if (DFS(graph, start, visited, start)) {
          // 如果end也被访问过,则两个顶点连通
          return visited[end];
     }
     return false;
}

int main() {
     Graph* graph = createGraph(5);
     addEdge(graph, 0, 1);
     addEdge(graph, 0, 2);
     addEdge(graph, 1, 2);
     addEdge(graph, 2, 0);
     addEdge(graph, 2, 3);
     addEdge(graph, 3, 3);

     int start = 2;
     int end = 3;
     printf("Vertex %d is %sconnected to vertex %d\n", start, isConnected(graph, start, end) ? "" : "not ", end);

     free(graph->adjList[0]);
     free(graph->adjList);
     free(graph);
     return 0;
}

例题分析:

1.创建图createGraph函数创建一个图的结构体,包括顶点数量和邻接表数组。

2.添加边addEdge函数向图中添加边,对于无向图,需要添加两条边。

3.DFS函数DFS函数是一个递归函数,用于深度优先搜索图中的所有顶点。它接受图、当前顶点、访问数组和起始顶点作为参数。

4.连通性判断isConnected函数接受图、起始顶点和目标顶点作为参数。它首先初始化一个访问数组,然后从起始顶点开始进行DFS。如果目标顶点也被访问过,则两个顶点连通。

5.主函数:在main函数中,创建了一个图,并添加了一些边。调用isConnected函数判断顶点2和顶点3是否连通,并打印结果。

这个例题展示了如何在C语言中使用DFS算法判断无向图中的两个顶点是否连通。通过这个例子,可以更好地理解DFS算法在图搜索中的应用,以及如何使用DFS来解决连通性问题。DFS算法通过深度优先遍历图的每个节点,尝试找到目标节点,是一种非常有效的搜索算法。

洪水填充

洪水填充(Flood Fill)是一种图像处理算法,用于填充图像中连通的区域,通常以特定的起始点为中心。洪水填充算法从起始点开始,将所有与起始点颜色相同的相邻像素点的颜色替换为目标颜色,直到达到边界或遇到不同颜色的像素点为止。

特点:

1.区域填充:洪水填充算法用于填充图像中连通的区域。

2.起始点选择:算法从用户指定的起始点开始填充。

3.颜色替换:将与起始点颜色相同的相邻像素点的颜色替换为目标颜色。

4.边界处理:当达到边界或遇到不同颜色的像素点时停止填充。

常见用法:

1.图像编辑:在图像编辑软件中,洪水填充用于改变图像中特定区域的颜色。

2.游戏开发:在游戏开发中,洪水填充可以用于实现水体、草地等的渲染。

3.计算机视觉:在计算机视觉领域,洪水填充可以用于图像分割、标记等任务。

经典C语言例题:

题目: 使用洪水填充算法在二维数组中填充连通的区域。

示例代码:

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

// 定义颜色
#define WHITE 0
#define RED 1
#define GREEN 2
#define BLUE 3

// 定义方向数组
int rowMove[] = {0, 1, 0, -1};
int colMove[] = {1, 0, -1, 0};

// 检查颜色是否相同
bool isSafe(int arr[][8], int x, int y, int color, int newColor, int row, int col) {
    if (x >= 0 && x < row && y >= 0 && y < col && arr[x][y] == color) {
         return true;
      }
    return false;
}

// 洪水填充算法
void floodFill(int arr[][8], int x, int y, int newColor, int color, int row, int col) {
    // 将起始点的颜色替换为目标颜色
    arr[x][y] = newColor;
     // 检查四个方向
    for (int dir = 0; dir < 4; dir++) {
         int nextX = x + rowMove[dir];
         int nextY = y + colMove[dir];
          // 如果颜色相同且未被访问过,则递归调用洪水填充
         if (isSafe(arr, nextX, nextY, color, newColor, row, col)) {
               floodFill(arr, nextX, nextY, newColor, color, row, col);
          }
     }
}

// 打印二维数组
void printArray(int arr[][8], int row, int col) {
    for (int i = 0; i < row; i++) {
         for (int j = 0; j < col; j++) {
              printf("%d ", arr[i][j]);
          }
         printf("\n");
     }
}

int main() {
    int arr[][8] = {
          {1, 1, 1, 1, 1, 1, 1, 1},
          {1, 1, 1, 1, 1, 1, 0, 0},
          {1, 0, 0, 1, 1, 0, 1, 1},
          {1, 2, 2, 2, 2, 0, 1, 0},
          {1, 1, 1, 2, 2, 0, 1, 0},
          {1, 1, 1, 1, 2, 2, 2, 0},
          {1, 1, 1, 1, 1, 1, 1, 0},
          {1, 1, 1, 1, 1, 1, 1, 1}
     };
    int row = sizeof(arr) / sizeof(arr[0]);
    int col = sizeof(arr[0]) / sizeof(arr[0][0]);
    int x = 1, y = 1, newColor = 3, color = 1;
    printf("Original array:\n");
    printArray(arr, row, col);
    floodFill(arr, x, y, newColor, color, row, col);
    printf("Modified array after flood fill:\n");
    printArray(arr, row, col);
    return 0;
}

例题分析:

1.定义颜色和方向:定义了颜色常量和方向数组,用于洪水填充算法。

2.检查颜色是否相同isSafe函数用于检查给定坐标处的颜色是否与起始点颜色相同。

3.洪水填充算法floodFill函数接受二维数组、起始点坐标、新颜色、起始点颜色、行数和列数作为参数。函数从起始点开始,将所有与起始点颜色相同的相邻像素点的颜色替换为目标颜色。

4.打印二维数组printArray函数用于打印二维数组的内容。

5.主函数:在main函数中,定义了一个二维数组,并初始化了一些颜色。调用floodFill函数从起始点开始进行洪水填充,并打印修改后的数组。

这个例题展示了如何在C语言中使用洪水填充算法在二维数组中填充连通的区域。通过这个例子,可以更好地理解洪水填充算法在图像处理中的应用,以及如何使用洪水填充来改变图像中特定区域的颜色。洪水填充算法通过递归地替换颜色,填充图像中连通的区域,是一种非常有效的图像处理算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值