图的最短路径之Floyd算法

参考:https://wangkuiwu.github.io/2013/04/15/floyd-cplus

Floyd算法介绍

弗洛伊德 Floyd 算法和 Dijkstra 算法一样,也是用于寻找给定的加权图中顶点间的最短路径算法。该算法名称以创始人——1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。

算法思想

通过 Floyd 计算图 G=(V, E) 中各个顶点的最短路径时,需要引入一个矩阵 S,矩阵 S 中的元素 a[i][j] 表示顶点 i(第 i 个顶点)到顶点 j(第 j 个顶点)的距离。初始时,矩阵 S 中顶点 a[i][j] 的距离为顶点 i 到顶点 j 的权值,如果 i 和 j 不相邻,则 a[i][j] = ∞,如果 i == j,则 a[i][j] = 0

任意节点 i 到节点 j 的最短路径有两种可能:

  1. 直接从 i 到 j。
  2. 从 i 经过若干个节点 k 到 j。

如果我们用 a[i][j] 表示节点 i 到节点 j 的最短路径的距离,对于每一个节点 k,将节点 k 作为中介点,都需要取检查 a[i][k] + a[k][j] < a[i][j]。如果该条件成立,需要设置 a[i][j] = a[i][k] + a[k][j],遍历每个节点 k,每次更新的是除了第 k 行和第 k 列的数。

单纯的理论可能比较难以理解,下面通过实例来对该算法进行说明。

Floyd算法图解

Floyd算法-图1

图 G4

以上图 G4 为例,来对 Floyd 算法进行演示。

首先初始化矩阵如下:

Floyd算法-图2

下面继续步骤演示:

Floyd算法-图3

步骤说明

第1步:以顶点 A(第零个顶点,对应下标为 0)为中介点,若 a[i][j] > a[i][0] + a[0][j],则设置 a[i][j] = a[i][0] + a[0][j]。以顶点 a[1][6] 为例,矩阵初始化时,a [1][6] = ∞,现在以 A 作为中介点时,因为 a[1][0] = 12,a[0][6] = 14,而 ∞ > a[1][0] + a[0][6] = 26,所以应该更新点 B 和点 G 之间的距离 a[1][6] = 26

第2步:以顶点 B(第一个顶点,对应下标为 1)为中介点,若 a[i][j] > a[i][1] + a[1][j],则设置 a[i][j] = a[i][1] + a[1][j]。以顶点 a[2][6] 为例,上一步后,a [2][6] = ∞,现在以 B 作为中介点时,因为 a[2][1] = 10,a[1][6] = 26,而 ∞ > a[1][0] + a[0][6] = 36,所以应该更新点 C 和点 G 之间的距离 a[2][6] = 36

第3步:以顶点 C(第二个顶点,对应下标为 2)为中介点,……

……

同理,依次将顶点 D, E, F, G 作为中介点,并更新 a[i][j] 的大小即可,最后就能得到任意两点之间的最短路径。

算法分析

Floyd 算法是一种动态规划算法,边权可正可负,此算法简单有效。

优点:理解容易,可以算出任意两个节点之间的最短距离,代码编写简单。

缺点:时间复杂度比较高,不适合计算大量数据。

算法时间复杂度为 O(n^3),空间复杂度为 O(n^2)。

Floyd算法实现

图的邻接矩阵实现Floyd算法

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

#define MAX 100
#define INF (~(0x1 << 31)) //无穷大(0x7FFFFFFF)

const int VEXNUM = 7;

struct EData
{
    char start; //边的起点
    char end;   //边的终点
    int weight; //边的权重

    EData() {}
    EData(char s, char e, int w) : start(s), end(e), weight(w) {}
};

class MatrixUDG
{
private:
    char mVexs[MAX];       //顶点集合,如:{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
    int mVexNum;           //顶点数,如:7
    int mEdgNum;           //边数
    int mMatrix[MAX][MAX]; //邻接矩阵
public:
    //创建图(手动输入)
    MatrixUDG();
    //创建图(用提供的矩阵)
    MatrixUDG(char *vexs, int vNum, int matrix[][VEXNUM]);
    ~MatrixUDG();
    //打印邻接表
    void print();
    //Floyd最短路径算法
    void Floyd(int path[][VEXNUM], int dist[][VEXNUM]);

private:
    //输入一个合法字母
    char readChar();
    //获得一个字母在顶点数组的下标
    int getPosition(char ch);
    //返回顶点v的第一个邻接顶点的索引,失败返回-1
    int firstVertex(int v);
    //范围顶点v相对于w的下一个邻接顶点的索引,失败返回-1
    //比如: A和C, D, E有连接,则A相对于C的下一个结点为D,即返回D的索引
    int nextVertex(int v, int w);
    //获取图中的边
    EData *getEdge();
    //对边按照权值从小到大进行排序
    void sortEdges(EData *edges, int eNum);
    //获取i的终点
    int getEnd(int *vEnds, int i);
};

MatrixUDG::MatrixUDG()
{
    char c1, c2;
    int p1, p2;
    int i, j;
    int weight;

    cout << "输入顶点数:";
    cin >> mVexNum;
    cout << "输入边数:";
    cin >> mEdgNum;
    if (mVexNum < 1 || mEdgNum < 1 || (mEdgNum > (mVexNum * (mVexNum - 1))))
    {
        cout << "输入有误!" << endl;
        return;
    }
    //初始化顶点
    for (i = 0; i < mVexNum; ++i)
    {
        cout << "vertex(" << i << "):";
        mVexs[i] = readChar();
    }
    //初始化边的权值
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            if (i == j)
                mMatrix[i][j] = 0;
            else
                mMatrix[i][j] = INF;
        }
    }
    //初始化边的权值,根据用户输入进行初始化
    for (i = 0; i < mEdgNum; ++i)
    {
        cout << "edge(" << i << "):";
        c1 = readChar();
        c2 = readChar();
        cin >> weight;

        p1 = getPosition(c1);
        p2 = getPosition(c2);
        if (p1 == -1 || p2 == -1)
        {
            cout << "输入的边错误!" << endl;
            return;
        }
        mMatrix[p1][p2] = weight;
        mMatrix[p2][p1] = weight;
    }
}

MatrixUDG::MatrixUDG(char *vexs, int vNum, int matrix[][VEXNUM])
{
    if (!vexs || !*matrix)
        return;
    int i, j;
    mVexNum = vNum;
    //初始化顶点
    for (i = 0; i < mVexNum; ++i)
        mVexs[i] = vexs[i];

    //初始化边
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            mMatrix[i][j] = matrix[i][j];
        }
    }

    //统计边的数目
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            if (i != j && mMatrix[i][j] != INF)
            {
                ++mEdgNum;
            }
        }
    }
    mEdgNum /= 2;
}

MatrixUDG::~MatrixUDG()
{
}

int MatrixUDG::getPosition(char ch)
{
    for (int i = 0; i < mVexNum; ++i)
    {
        if (mVexs[i] == ch)
            return i;
    }
    return -1;
}

char MatrixUDG::readChar()
{
    char ch;
    do
    {
        cin >> ch;
    } while (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')));
    return ch;
}

/*
返回顶点v的第一个邻接顶点的索引,失败返回-1
*/
int MatrixUDG::firstVertex(int v)
{
    if (v < 0 || v > (mVexNum - 1))
        return -1;
    for (int i = 0; i < mVexNum; ++i)
    {
        if (mMatrix[v][i] != 0 && mMatrix[v][i] != INF)
            return i;
    }
    return -1;
}
/*
返回顶点v相对于w的下一个邻接顶点的索引,失败则返回-1
比如:A和C,D,E有连接,则A相对于C的下一个结点为D,即返回D的索引
*/
int MatrixUDG::nextVertex(int v, int w)
{
    if (v < 0 || v > (mVexNum - 1) || w < 0 || w > (mVexNum - 1))
        return -1;
    for (int i = w + 1; i < mVexNum; ++i)
    {
        if (mMatrix[v][i] != 0 && mMatrix[v][i] != INF)
            return i;
    }
    return -1;
}

//获取图中的边
EData *MatrixUDG::getEdge()
{
    int i, j;
    int index = 0;
    EData *edges = new EData[mEdgNum];
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = i + 1; j < mVexNum; ++j)
        {
            if (mMatrix[i][j] != INF)
            {
                edges[index].start = mVexs[i];
                edges[index].end = mVexs[j];
                edges[index].weight = mMatrix[i][j];
                ++index;
            }
        }
    }
    return edges;
}

//对边按照权值大小进行排序(由小到大)
void MatrixUDG::sortEdges(EData *edges, int eNum)
{
    //直接调用STL排序
    std::sort(edges, edges + eNum, [](EData &edge1, EData &edge2) -> bool
              { return edge1.weight < edge2.weight; });
}

int MatrixUDG::getEnd(int *vEnds, int i)
{
    //相当于并查集的操作
    while (vEnds[i] != 0)
    {
        i = vEnds[i];
    }
    return i;
}

/*
 * Floyd最短路径算法,即统计图中各个顶点的最短路径
 * 参数说明:
 * path: 路径
 * dist: 长度数组。即dist[i][j]=sum表示,第i个顶点到第j个顶点的最短路径长度是sum
 */
void MatrixUDG::Floyd(int path[][VEXNUM], int dist[][VEXNUM])
{
    int i, j, k;
    int tmp;
    //初始化
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            dist[i][j] = mMatrix[i][j]; //第i个顶点到第j个顶点的路径长度为i到j的权值
            path[i][j] = j;             //第i个顶点到第j个顶点的最短路径是经过第j个顶点
        }
    }
    //计算最短路径
    for (k = 0; k < mVexNum; ++k)
    {
        for (i = 0; i < mVexNum; ++i)
        {
            for (j = 0; j < mVexNum; ++j)
            {
                //如果经过下标为k顶点路径比原两点间路径更短,则更新dist[i][j]和path[i][j]
                tmp = (dist[i][k] == INF || dist[k][j] == INF) ? INF : dist[i][k] + dist[k][j];
                if (dist[i][j] > tmp)
                {
                    dist[i][j] = tmp;        //i到j最短路径对应的值设为更小的一个(即经过 k)
                    path[i][j] = path[i][k]; //i到j最短路径对应的路径,经过 k
                }
            }
        }
    }
    //打印Floyd最短路径的结果
    cout << "Floyd:" << endl;
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            cout << dist[i][j] << "\t";
        }
        cout << endl;
    }
}

void MatrixUDG::print()
{
    for (int i = 0; i < mVexNum; ++i)
    {
        for (int j = 0; j < mVexNum; ++j)
        {
            if (mMatrix[i][j] == INF)
            {
                cout << "INF"
                     << "\t";
            }
            else
            {
                cout << mMatrix[i][j] << "\t";
            }
        }
        cout << endl;
    }
}

int main()
{
    char vexs[VEXNUM] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
    int vNum = sizeof(vexs) / sizeof(vexs[0]);
    int matrix[VEXNUM][VEXNUM] = {
        {0, 12, INF, INF, INF, 16, 14},
        {12, 0, 10, INF, INF, 7, INF},
        {INF, 10, 0, 3, 5, 6, INF},
        {INF, INF, 3, 0, 4, INF, INF},
        {INF, INF, 5, 4, 0, 2, 8},
        {16, 7, 6, INF, 2, 0, 9},
        {14, INF, INF, INF, 8, 9, 0}};

    // 1. 根据提供的数据生成
    MatrixUDG mudg(vexs, vNum, matrix);
    mudg.print(); //打印图
    int path[VEXNUM][VEXNUM];
    int dist[VEXNUM][VEXNUM];
    //Floyd算法获取各个顶点之间的最短距离
    mudg.Floyd(path, dist);
    //2. 手动生成
    // MatrixUDG mudg1;
    // mudg1.print();
}

运行结果:

$ ./test
0       12      INF     INF     INF     16      14
12      0       10      INF     INF     7       INF
INF     10      0       3       5       6       INF
INF     INF     3       0       4       INF     INF
INF     INF     5       4       0       2       8
16      7       6       INF     2       0       9
14      INF     INF     INF     8       9       0
Floyd:
0       12      22      22      18      16      14
12      0       10      13      9       7       16
22      10      0       3       5       6       13
22      13      3       0       4       6       12
18      9       5       4       0       2       8
16      7       6       6       2       0       9
14      16      13      12      8       9       0

图的邻接表实现Flody算法

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

#define MAX 100
#define INF (~(0x1 << 31)) //无穷大(0x7FFFFFFF)

const int VEXNUM = 7;

#define MAX 100
#define INF (~(0x1 << 31)) //最大值

struct EData
{
    char start; //边的起点
    char end;   //边的终点
    int weight; //边的权重

    EData() {}
    EData(char s, char e, int w) : start(s), end(e), weight(w) {}
};

class ListUDG
{
private:
    //每一条边
    struct ENode
    {
        int iVex = 0;           //指向的顶点的位置
        int weight = 0;         //该边的权
        ENode *nextEdge = NULL; //指向顶点的下一条边的指针
    };
    //数组中存储的顶点
    struct VNode
    {
        char data;
        ENode *firstEdge = NULL; //指向第一条该顶点的边
    };

private:
    int mVexNum;      //图的顶点数目
    int mEdgeNum;     //图的边的数目
    VNode mVexs[MAX]; //存储顶点
public:
    //创建邻接表对应的图(自己收入)
    ListUDG();
    //创建邻接表对应的图(用已提供的数据)
    ListUDG(char *vexs, int vNum, EData *edges[], int eNum);
    ~ListUDG();
    //打印邻接表
    void print();
    //Floyd最短路径算法
    void Floyd(int path[][VEXNUM], int dist[][VEXNUM]);

private:
    //读取一个合法的输入字符
    char readChar();
    //返回字符ch在的位置
    int getPosition(char ch);
    //将node结点链接到list的最后
    void linkLast(ENode *list, ENode *node);
    //获取边<start, end>的权值,若start和end不是连通的,则返回无穷大
    int getWeight(int start, int end);
    //获取图中的边
    EData *getEdges();
    //对边按照权值从小到大进行排序
    void sortEdges(EData *edges, int eNum);
    //获取i的终点
    int getEnd(int *vEnds, int i);
};

ListUDG::ListUDG()
{
    char c1, c2;
    int p1, p2;
    ENode *node1, *node2;
    int weight;

    cout << "输入顶点数:";
    cin >> mVexNum;
    cout << "输入边数:";
    cin >> mEdgeNum;
    if (mVexNum > MAX || mEdgeNum > MAX || mVexNum < 1 || mEdgeNum < 1 || (mEdgeNum > (mVexNum * (mVexNum - 1))))
    {
        cout << "输入有误!" << endl;
        return;
    }
    //初始化邻接表的顶点
    for (int i = 0; i < mVexNum; ++i)
    {
        cout << "vertex(" << i << "):";
        mVexs[i].data = readChar();
        mVexs[i].firstEdge = NULL;
    }
    //初始化邻接表的边
    for (int j = 0; j < mEdgeNum; ++j)
    {
        cout << "edge(" << j << "):";
        c1 = readChar();
        c2 = readChar();
        cin >> weight;

        p1 = getPosition(c1);
        p2 = getPosition(c2);

        if (p1 == -1 || p2 == -1)
        {
            cout << "输入的边有错误!" << endl;
            return;
        }
        //初始化node1
        node1 = new ENode();
        node1->iVex = p2;
        node1->weight = weight;
        //将node1链接到p1所在链表的末尾
        if (mVexs[p1].firstEdge == NULL)
            mVexs[p1].firstEdge = node1;
        else
            linkLast(mVexs[p1].firstEdge, node1);
        //初始化node2
        node2 = new ENode();
        node2->iVex = p1;
        node2->weight = weight;
        //将node2链接到p2所在链表的末尾
        if (mVexs[p2].firstEdge == NULL)
            mVexs[p2].firstEdge = node2;
        else
            linkLast(mVexs[p2].firstEdge, node2);
    }
}

ListUDG::ListUDG(char *vexs, int vNum, EData *edges[], int eNum)
{
    if (vNum > MAX || eNum > MAX)
        return;
    char c1, c2;
    int p1, p2;
    ENode *node1, *node2;
    int weight;
    //初始化顶点数和边数
    mVexNum = vNum;
    mEdgeNum = eNum;
    //初始化邻接表的顶点
    for (int i = 0; i < mVexNum; ++i)
    {
        mVexs[i].data = vexs[i];
        mVexs[i].firstEdge = NULL;
    }
    //初始化邻接表的边
    for (int j = 0; j < mEdgeNum; ++j)
    {
        //读取边的起始顶点和结束顶点
        c1 = edges[j]->start;
        c2 = edges[j]->end;
        weight = edges[j]->weight;

        p1 = getPosition(c1);
        p2 = getPosition(c2);
        if (p1 == -1 || p2 == -1)
        {
            cout << "输入的边有错误!" << endl;
            return;
        }
        //初始化node1
        node1 = new ENode();
        node1->iVex = p2;
        node1->weight = weight;
        //将node1链接到p1所在的链表末尾
        if (mVexs[p1].firstEdge == NULL)
            mVexs[p1].firstEdge = node1;
        else
            linkLast(mVexs[p1].firstEdge, node1);
        //初始化node2
        node2 = new ENode();
        node2->iVex = p1;
        node2->weight = weight;
        //将node2链接到p2所在链表末尾
        if (mVexs[p2].firstEdge == NULL)
            mVexs[p2].firstEdge = node2;
        else
            linkLast(mVexs[p2].firstEdge, node2);
    }
}

ListUDG::~ListUDG()
{
}

void ListUDG::linkLast(ENode *list, ENode *node)
{
    ENode *p = list;
    while (p->nextEdge)
        p = p->nextEdge;
    p->nextEdge = node;
}

int ListUDG::getPosition(char ch)
{
    for (int i = 0; i < mVexNum; ++i)
    {
        if (mVexs[i].data == ch)
            return i;
    }
    return -1;
}

char ListUDG::readChar()
{
    char ch;
    do
    {
        cin >> ch;
    } while (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')));
    return ch;
}

int ListUDG::getWeight(int start, int end)
{
    if (start == end)
        return 0;
    ENode *node = mVexs[start].firstEdge;
    while (node != NULL)
    {
        if (end == node->iVex)
        {
            return node->weight;
        }
        node = node->nextEdge;
    }
    return INF;
}

//获得图中的边
EData *ListUDG::getEdges()
{
    int i;
    int index = 0;
    ENode *node = NULL;
    EData *edges = new EData[mEdgeNum];
    for (i = 0; i < mVexNum; ++i)
    {
        node = mVexs[i].firstEdge;
        while (node != NULL)
        {
            if (node->iVex > i)
            {
                edges[index].start = mVexs[i].data;
                edges[index].end = mVexs[node->iVex].data;
                edges[index].weight = node->weight;
                ++index;
            }
            node = node->nextEdge;
        }
    }
    return edges;
}

//对边按照权值大小进行排序(由小到大)
void ListUDG::sortEdges(EData *edges, int eNum)
{
    if (!edges)
        return;
    //直接调用STL排序
    std::sort(edges, edges + eNum, [](EData &edge1, EData &edge2) -> bool
              { return edge1.weight < edge2.weight; });
}

int ListUDG::getEnd(int *vEnds, int i)
{
    //相当于并查集的操作
    while (vEnds[i] != 0)
    {
        i = vEnds[i];
    }
    return i;
}

/*
 * Floyd最短路径算法,即统计图中各个顶点的最短路径
 * 参数说明:
 * path: 路径
 * dist: 长度数组。即dist[i][j]=sum表示,第i个顶点到第j个顶点的最短路径长度是sum
 */
void ListUDG::Floyd(int path[][VEXNUM], int dist[][VEXNUM])
{
    int i, j, k;
    int tmp;
    //初始化
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            dist[i][j] = getWeight(i, j); //第i个顶点到第j个顶点的路径长度为i到j的权值
            path[i][j] = j;               //第i个顶点到第j个顶点的最短路径是经过第j个顶点
        }
    }
    //计算最短路径
    for (k = 0; k < mVexNum; ++k)
    {
        for (i = 0; i < mVexNum; ++i)
        {
            for (j = 0; j < mVexNum; ++j)
            {
                //如果经过下标为k顶点路径比原两点间路径更短,则更新dist[i][j]和path[i][j]
                tmp = (dist[i][k] == INF || dist[k][j] == INF) ? INF : dist[i][k] + dist[k][j];
                if (dist[i][j] > tmp)
                {
                    dist[i][j] = tmp;        //i到j最短路径对应的值设为更小的一个(即经过 k)
                    path[i][j] = path[i][k]; //i到j最短路径对应的路径,经过 k
                }
            }
        }
    }
    //打印Floyd最短路径的结果
    cout << "Floyd:" << endl;
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            cout << dist[i][j] << "\t";
        }
        cout << endl;
    }
}

void ListUDG::print()
{
    ENode *node;
    for (int i = 0; i < mVexNum; ++i)
    {
        cout << i << "(" << mVexs[i].data << "):";
        node = mVexs[i].firstEdge;
        while (node != NULL)
        {
            cout << node->iVex << "(" << mVexs[node->iVex].data << ")";
            node = node->nextEdge;
        }
        cout << endl;
    }
}

int main(int argc, char **argv)
{
    char vexs[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
    EData *edges[] = {
        new EData('A', 'B', 12),
        new EData('A', 'F', 16),
        new EData('A', 'G', 14),
        new EData('B', 'C', 10),
        new EData('B', 'F', 7),
        new EData('C', 'D', 3),
        new EData('C', 'E', 5),
        new EData('C', 'F', 6),
        new EData('D', 'E', 4),
        new EData('E', 'F', 2),
        new EData('E', 'G', 8),
        new EData('F', 'G', 9),
    };
    int vNum = sizeof(vexs) / sizeof(vexs[0]);
    int eNum = sizeof(edges) / sizeof(edges[0]);
    //1. 根据提供的数据生成
    ListUDG ludg(vexs, vNum, edges, eNum);
    ludg.print();
    int path[VEXNUM][VEXNUM];
    int dist[VEXNUM][VEXNUM];
    //Floyd算法获取各个顶点之间的最短距离
    ludg.Floyd(path, dist);
    //2. 手动输入
    // ListUDG ludg;
    // ludg.print();
    return 0;
}

运行结果:

$ ./test
0(A):1(B)5(F)6(G)
1(B):0(A)2(C)5(F)
2(C):1(B)3(D)4(E)5(F)
3(D):2(C)4(E)
4(E):2(C)3(D)5(F)6(G)
5(F):0(A)1(B)2(C)4(E)6(G)
6(G):0(A)4(E)5(F)
Floyd:
0       12      22      22      18      16      14
12      0       10      13      9       7       16
22      10      0       3       5       6       13
22      13      3       0       4       6       12
18      9       5       4       0       2       8
16      7       6       6       2       0       9
14      16      13      12      8       9       0
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值