HNOI2015解题报告

8 篇文章 0 订阅
6 篇文章 0 订阅

HNOI2015解题报告

Author: Pengyihao

Day1 T1 亚瑟王


思路

f[i][j] 表示 i 一共获得了 j 次“机会”的概率。

注意这里的“机会”,是指有多少轮中,它前面的所有卡牌,要么在之前的轮中发动过,要么在这一轮中因为运气没有发动。

这里的“机会”不与自己有没有发动相关。所以就算在之前某轮中发动过,这一轮轮到它的时候贡献依然要增加。

于是有 f[i][j]=f[i1][j+1](1(1p[i1])j)+f[i1][j](1p[i1])j

ans=ni=1rj=1f[i][j]d[i](1(1p[i])j)


代码
#include <bits/stdc++.h>

typedef long long LL;

#define FOR(i, a, b) for (int i = (a), i##_END_ = (b); i <= i##_END_; i++)
#define DNF(i, a, b) for (int i = (a), i##_END_ = (b); i >= i##_END_; i--)

template <typename Tp> void in(Tp &x) {
    char ch = getchar(); x = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
}

template <typename Tp> Tp Min(Tp x, Tp y) {return x < y ? x : y;}
template <typename Tp> Tp Max(Tp x, Tp y) {return x > y ? x : y;}
template <typename Tp> Tp chkmax(Tp &x, Tp y) {return x > y ? x : x=y;}
template <typename Tp> Tp chkmin(Tp &x, Tp y) {return x < y ? x : x=y;}

const int MAXN = 230, MAXR = 140;

int T, n, r, d[MAXN];
double p[MAXN], f[MAXN][MAXR];

double power(double x, int y)
{
    double ret = 1;
    while (y) {
        if (y & 1) ret *= x;
        x *= x;
        y >>= 1;
    }
    return ret;
}

int main()
{
    freopen("arthur.in", "r", stdin);
    freopen("arthur.out", "w", stdout);

    in(T);

    while (T--) {
        in(n); in(r);

        FOR(i, 1, n) {
            scanf("%lf", &p[i]); in(d[i]);
        }

        memset(f, 0, sizeof f);

        f[1][r] = 1;

        FOR(i, 2, n) FOR(j, 1, r) {
            f[i][j] = 0;
            f[i][j] += f[i - 1][j + 1] * (1 - power(1 - p[i - 1], j + 1));
            f[i][j] += f[i - 1][j]     * power(1 - p[i - 1], j);
        }

        double ans = 0;

        FOR(i, 1, n) FOR(j, 1, r) {
            ans += f[i][j] * (1 - power(1 - p[i], j)) * d[i];
        }

        printf("%.10lf\n", ans);
    }

    return 0;
}

Day1 T2 接水果


思路

首先,我们求出每个点的dfs序。

然后对于一条x<–>y的路径,如果有路径包含它,那么有

  1. 如果 lca(x,y)=xory ,那么只需要这条路径的一个端点在深度大的点的子树中,另一个端点不在“深度小的点往深度大的点的方向上的第一个点”的子树中即可。

  2. 否则,那么只需要这条路径的两个端点分别在 x y 的子树中即可。

我们令这条包含x<–>y的路径的两个端点中dfs序小的点为 a ,dfs序大的点为 b

对于上面两种情况中的任意一种情况,这种情况中 a b 所在的子树互不相交,为两个不相交的dfs序区间(不在子树中的情况我们可以转化为在它的补集的区间的情况)。

于是我们就把问题转化为了,每个盘子对应一对(或两对,因为有“不在子树”)区间,然后对于每个水果的两个点,两个区间分别包含其两个点的盘子中,权值第 k 小的。

我们可以把一维转化成 x 坐标,另一维转化成 y 坐标,于是问题就转化为了包含一个点的矩形中第 k 小的。

于是可以用扫描线+树状数组套主席树解决。


代码
#include <bits/stdc++.h>

using std::map;

typedef long long LL;

#define FOR(i, a, b) for (int i = (a), i##_END_ = (b); i <= i##_END_; i++)
#define DNF(i, a, b) for (int i = (a), i##_END_ = (b); i >= i##_END_; i--)

#define debug(...) fprintf(stderr, __VA_ARGS__)

template <typename Tp> void in(Tp &x) {
    char ch = getchar(); x = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
}

template <typename Tp> Tp Min(Tp x, Tp y) {return x < y ? x : y;}
template <typename Tp> Tp Max(Tp x, Tp y) {return x > y ? x : y;}
template <typename Tp> Tp chkmax(Tp &x, Tp y) {return x > y ? x : x=y;}
template <typename Tp> Tp chkmin(Tp &x, Tp y) {return x < y ? x : x=y;}

const int MAXN = 100010;

const int appear = 0, disappear = 2, query = 1;

int n, p, q, ev_tot;
int cnt, head[MAXN], data[MAXN << 1], nxt[MAXN << 1];

int INDEX, dfn[MAXN], ed[MAXN], depth[MAXN];

int anc[MAXN][18];

int ans[MAXN];

map<int, int>M; int M_point, to[MAXN << 3];

struct Event {
    int type, num;
    int time, l, r, w;
} ev[MAXN << 3];

namespace segment_tree
{
    const int MAXS = MAXN * 250;
    int seg_tot = 0, rot[MAXN], sz[MAXS], ch[MAXS][2];

    int should_remove[250], should_add[250];

    void build(int &now, int l, int r)
    {
        now = ++seg_tot;
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(ch[now][0], l, mid);
        build(ch[now][1], mid + 1, r);
    }

    void initialize()
    {
        build(rot[0], 1, M_point);
        FOR(i, 1, n) rot[i] = rot[0];
    }

    void push(int &now, int l, int r, int x, int modi)
    {
        int tmp = now;
         now = ++seg_tot;

        sz[now] = sz[tmp];
        ch[now][0] = ch[tmp][0];
        ch[now][1] = ch[tmp][1];

        sz[now] += modi;

        if (l == r) return;

        int mid = (l + r) >> 1;
        if (x <= mid) push(ch[now][0], l, mid, x, modi);
        else push(ch[now][1], mid + 1, r, x, modi);
    }

    void getdown(int &ret, bool t)
    {
        ret = 0;
        FOR(i, 1, should_add[0]) ret += sz[ch[should_add[i]][t]];
        FOR(i, 1, should_remove[0]) ret -= sz[ch[should_remove[i]][t]];
    }

    void godown(bool t)
    {
        FOR(i, 1, should_add[0]) should_add[i] = ch[should_add[i]][t];
        FOR(i, 1, should_remove[0]) should_remove[i] = ch[should_remove[i]][t];
    }

    void _query(int w, int l, int r, int num)
    {
        if (l == r) {
            ans[num] = l;
            return;
        }

        int ldata; getdown(ldata, 0);

        int mid = (l + r) >> 1;
        if (ldata >= w) {godown(0); _query(w, l, mid, num);}
        else {godown(1); _query(w - ldata, mid + 1, r, num);}
    }

    void insert(int l, int r, int w)
    {
        if (l > r) {
            debug("WA");
        } //This line is for gdb
        while (l <= n) {
            push(rot[l], 1, M_point, w, 1);
            l += l & -l;
        }

        r++;
        while (r <= n) {
            push(rot[r], 1, M_point, w, -1);
            r += r & -r;
        }
    }

    void remove(int l, int r, int w)
    {
        while (l <= n) {
            push(rot[l], 1, M_point, w, -1);
            l += l & -l;
        }
        r++;
        while (r <= n) {
            push(rot[r], 1, M_point, w, 1);
            r += r & -r;
        }
    }

    void query(int l, int r, int w, int num)
    {
        l--;
        should_remove[0] = should_add[0] = 0;
        while (l) {
            should_remove[++should_remove[0]] = rot[l];
            l -= l & -l;
        }
        while (r) {
            should_add[++should_add[0]] = rot[r];
            r -= r & -r;
        }
        _query(w, 1, M_point, num);
    }
}

int lca(int x, int y)
{
    if (depth[x] < depth[y]) x ^= y ^= x ^= y;
    if (depth[x] != depth[y]) {
        int delta = depth[x] - depth[y];
        DNF(i, 17, 0) if (delta >> i & 1) x = anc[x][i];
    }
    if (x == y) return x;
    DNF(i, 17, 0) if (anc[x][i] && anc[y][i] && anc[x][i] != anc[y][i]) {
        x = anc[x][i]; y = anc[y][i];
    }
    return anc[x][0];
}

void work(int now)
{
    if (ev[now].type == appear) {
        segment_tree::insert(ev[now].l, ev[now].r, ev[now].w);
    }
    else if (ev[now].type == disappear) {
        segment_tree::remove(ev[now].l, ev[now].r, ev[now].w);
    }
    else {
        segment_tree::query(1, ev[now].l, ev[now].w, ev[now].num);
    }
}

bool cmp(const Event &a, const Event &b)
{
    return a.time < b.time || a.time == b.time && a.type < b.type;
}

bool cmp2(const Event &a, const Event &b)
{
    return a.w < b.w;
}

void add(int x, int y)
{
    nxt[cnt] = head[x]; data[cnt] = y; head[x] = cnt++;
    nxt[cnt] = head[y]; data[cnt] = x; head[y] = cnt++;
}

void dfs(int now, int pa)
{
    depth[now] = depth[pa] + 1;

    anc[now][0] = pa;
    for (int i = 1; anc[now][i - 1]; i++)
        anc[now][i] = anc[anc[now][i - 1]][i - 1];

    dfn[now] = ++INDEX;
    for (int i = head[now]; i != -1; i = nxt[i]) {
        if (data[i] != pa) dfs(data[i], now);
    }
    ed[now] = INDEX;
}

int find_fa(int now, int depth)
{
    FOR(i, 0, 17) if (depth >> i & 1) now = anc[now][i];
    return now;
}

int main()
{
    freopen("fruit.in", "r", stdin);
    freopen("fruit.out", "w", stdout);

    memset(head, -1, sizeof head);

    in(n); in(p); in(q);

    FOR(i, 1, n - 1) {
        int u, v; in(u); in(v); add(u, v);
    }

    dfs(1, 0);

    FOR(i, 1, p) {
        int u, v, w;
        in(u); in(v); in(w);

        int anc = lca(u, v);

        if (dfn[u] > dfn[v]) u ^= v ^= u ^= v;

        if (u == anc) {
            int nodes = find_fa(v, depth[v] - depth[u] - 1);
            int ss = dfn[nodes], tt = ed[nodes];

            if (ss != 1) {
                ev[++ev_tot].time = 1;
                ev[ev_tot].l = dfn[v]; ev[ev_tot].r = ed[v];
                ev[ev_tot].type = appear; ev[ev_tot].w = w;

                ev[++ev_tot].time = ss - 1;
                ev[ev_tot].l = dfn[v]; ev[ev_tot].r = ed[v];
                ev[ev_tot].type = disappear; ev[ev_tot].w = w;
            }

            if (tt != n) {
                ev[++ev_tot].time = dfn[v];
                ev[ev_tot].l = tt + 1; ev[ev_tot].r = n;
                ev[ev_tot].type = appear; ev[ev_tot].w = w;

                ev[++ev_tot].time = ed[v];
                ev[ev_tot].l = tt + 1; ev[ev_tot].r = n;
                ev[ev_tot].type = disappear; ev[ev_tot].w = w;
            }
        }

        else {
            ev[++ev_tot].time = dfn[u]; ev[ev_tot].l = dfn[v];
            ev[ev_tot].r = ed[v]; ev[ev_tot].type = appear; ev[ev_tot].w = w;
            ev[++ev_tot].time = ed[u]; ev[ev_tot].l = dfn[v];
            ev[ev_tot].r = ed[v]; ev[ev_tot].type = disappear; ev[ev_tot].w = w;
        }
    }

    std::sort(ev + 1, ev + ev_tot + 1, cmp2);

    FOR(i, 1, ev_tot) {
        if (i != 1 && ev[i].w == ev[i - 1].w) continue;
        M[ev[i].w] = ++M_point;
        to[M_point] = ev[i].w;
    }

    FOR(i, 1, ev_tot) {
        ev[i].w = M[ev[i].w];
    }

    FOR(i, 1, q) {
        int u, v, w;
        in(u); in(v); in(w);
        if (dfn[u] > dfn[v]) u ^= v ^= u ^= v;

        ev[++ev_tot].time = dfn[u];
        ev[ev_tot].l = dfn[v]; ev[ev_tot].w = w;
        ev[ev_tot].num = i; ev[ev_tot].type = query;
    }

    std::sort(ev + 1, ev + ev_tot + 1, cmp);

    segment_tree::initialize();

    FOR(i, 1, ev_tot) work(i);

    FOR(i, 1, q) printf("%d\n", to[ans[i]]);

    return 0;
}

Day1 T3 菜肴制作


思路

按照逆序拓扑排序,然后构造字典序最大的解。


代码
#include <bits/stdc++.h>

using std::priority_queue;

typedef long long LL;

#define FOR(i, a, b) for (int i = (a), i##_END_ = (b); i <= i##_END_; i++)
#define DNF(i, a, b) for (int i = (a), i##_END_ = (b); i >= i##_END_; i--)

template <typename Tp> void in(Tp &x) {
    char ch = getchar(); x = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
}

template <typename Tp> Tp Min(Tp x, Tp y) {return x < y ? x : y;}
template <typename Tp> Tp Max(Tp x, Tp y) {return x > y ? x : y;}
template <typename Tp> Tp chkmax(Tp &x, Tp y) {return x > y ? x : x=y;}
template <typename Tp> Tp chkmin(Tp &x, Tp y) {return x < y ? x : x=y;}

const int MAXN = 100010;

int T, n, m, chose, du[MAXN], order[MAXN];

int cnt, head[MAXN], data[MAXN << 1], nxt[MAXN << 1];

priority_queue<int>q;

void add(int x, int y)
{
    nxt[cnt] = head[x]; data[cnt] = y; head[x] = cnt++;
    nxt[cnt] = head[y]; data[cnt] = x; head[y] = cnt++;
}

int main()
{
    freopen("dishes.in", "r", stdin);
    freopen("dishes.out", "w", stdout);

    in(T);

    while (T--) {
        memset(du, 0, sizeof du);
        memset(head, -1, sizeof head); cnt = 0;

        in(n); in(m); chose = n;

        FOR(i, 1, m) {
            int x, y; in(x); in(y); add(y, x);
            du[x]++;
        }

        FOR(i, 1, n) {
            if (!du[i]) q.push(i);
        }

        order[0] = 0;

        while (!q.empty()) {
            int now = q.top(); q.pop(); chose--;
            order[++order[0]] = now;
            for (int i = head[now]; i != -1; i = nxt[i]) {
                du[data[i]]--;
                if (!du[data[i]]) {
                    q.push(data[i]);
                }
            }
        }

        if (chose) puts("Impossible!");
        else {
            DNF(i, n, 1) printf("%d ", order[i]);
            putchar(10);
        }
    }

    return 0;
}

Day2 T1 落忆枫音


思路

考虑没有环的情况。

每个点(除了 1 号点)任选一条入边,则构成一棵脉络树。

所有方案数为 ni=2degree[i]

但是有环,所以答案要减去它。

S y x 的路径。

remove=Sni=2[iS]degree[i]

这个可以dp……

f[i] i x 的路径求出的remove。

f[i]=j>if[j]/degree[i]


代码
#include <bits/stdc++.h>

using std::queue;

typedef long long LL;

#define FOR(i, a, b) for (LL i = (a), i##_END_ = (b); i <= i##_END_; i++)
#define DNF(i, a, b) for (LL i = (a), i##_END_ = (b); i >= i##_END_; i--)

template <typename Tp> void in(Tp &x) {
    char ch = getchar(); x = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
}

template <typename Tp> Tp Min(Tp x, Tp y) {return x < y ? x : y;}
template <typename Tp> Tp Max(Tp x, Tp y) {return x > y ? x : y;}
template <typename Tp> Tp chkmax(Tp &x, Tp y) {return x > y ? x : x=y;}
template <typename Tp> Tp chkmin(Tp &x, Tp y) {return x < y ? x : x=y;}

const LL MOD = 1000000007;
const LL MAXN = 200010;

LL n, m, x, y, ans = 1;

LL f[MAXN], du[MAXN], rdu[MAXN];
LL head[MAXN], data[MAXN], nxt[MAXN], cnt;
LL head1[MAXN], data1[MAXN], nxt1[MAXN], cnt1;

queue<LL>q;

void add(LL x, LL y)
{
    nxt[cnt] = head[x]; data[cnt] = y; head[x] = cnt++;
}

void add2(LL x, LL y)
{
    nxt1[cnt1] = head1[x]; data1[cnt1] = y; head1[x] = cnt1++;
}

LL power(LL x, LL y)
{
    LL ret = 1;
    while (y) {
        if (y & 1) ret = 1ll * ret * x % MOD;
        x = 1ll * x * x % MOD;
        y >>= 1;
    }
    return ret;
}

int main()
{
    freopen("maple.in", "r", stdin);
    freopen("maple.out", "w", stdout);

    memset(head, -1, sizeof head);
    memset(head1, -1, sizeof head1);

    in(n); in(m); in(x); in(y);

    FOR(i, 1, m) {
        LL u, v;
        in(u); in(v); add(u, v); add2(v, u);
        du[v]++; rdu[v]++;
    }

    FOR(i, 2, n)
        if (i != y)
            ans = 1ll * ans * du[i] % MOD;

    if (x == y || y == 1) {
        if (y != 1)
            ans = 1ll * ans * du[x] % MOD;
        printf("%lld\n", ans);
        return 0;
    }

    LL tmp = ans;
    ans = 1ll * ans * (du[y] + 1) % MOD;

    FOR(i, 1, n) {
        if (!du[i]) q.push(i);
    }

    while (!q.empty()) {
        LL now = q.front(); q.pop();
        if (now == y) {
            f[now] = tmp;
        }
        else {
            f[now] = 0;
            for (LL i = head1[now]; i != -1; i = nxt1[i]) {
                f[now] = (f[now] + f[data1[i]]) % MOD;
            }
            f[now] = 1ll * f[now] * power(rdu[now], MOD - 2) % MOD;
        }

        for (LL i = head[now]; i != -1; i = nxt[i]) {
            du[data[i]]--;
            if (!du[data[i]]) {
                q.push(data[i]);
            }
        }
    }

    printf("%lld\n", (ans - f[x] + MOD) % MOD);

    return 0;
}

Day2 T2


这是一道动态点分治的题目。但因为我还没有搞这个专题所以暂且跳过。


Day2 T3


思路

这个题目还是很容易的。

因为所有的 x 互不相同, 所以可以把大小关系看作是图论中的边。

那么构成的就是一座森林(把相等的点用并差集缩起来)。

然后就可以树形dp辣!

因为有等于,所以我们不知道每棵子树中有多少“块”元素。

我们把相等的元素看成一块,那么一棵子树就是若干个块的大小关系。

我们设第 i 个点的子树中有 j 个块的方案数为 f[i][j]

则两棵子树合并的结果 g[i]

g[i]=j=1nk=1n[max(j,k)ij+k]f[son1][j]f[son2][j](ij)(jj+ki)


代码
#include <bits/stdc++.h>

typedef long long LL;

#define FOR(i, a, b) for (int i = (a), i##_END_ = (b); i <= i##_END_; i++)
#define DNF(i, a, b) for (int i = (a), i##_END_ = (b); i >= i##_END_; i--)

template <typename Tp> void in(Tp &x) {
    char ch = getchar(); x = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
}

template <typename Tp> Tp Min(Tp x, Tp y) {return x < y ? x : y;}
template <typename Tp> Tp Max(Tp x, Tp y) {return x > y ? x : y;}
template <typename Tp> Tp chkmax(Tp &x, Tp y) {return x > y ? x : x=y;}
template <typename Tp> Tp chkmin(Tp &x, Tp y) {return x < y ? x : x=y;}

const int MAXN = 110;
const int MOD = 1000000007;

struct Edge {
    int l, r;
} edge[MAXN];

int edge_lenth;

int n, m;
char compare[10];

int du[MAXN], f[MAXN][MAXN];

int head[MAXN], nxt[MAXN], data[MAXN], cnt;

int fa[MAXN];

int find(int x)
{
    int tmp = x, pre;
    while (tmp != fa[tmp]) tmp = fa[tmp];
    while (x != tmp) {
        pre = fa[x]; fa[x] = tmp; x = pre;
    }
    return tmp;
}

void merge(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx != fy) fa[fx] = fy;
}

int g[MAXN], jie[MAXN], ni[MAXN];

int C(int x, int y)
{
    if (!y) return 1;
    if (x < y) return 0;
    return 1ll * jie[x] * ni[y] % MOD * ni[x - y] % MOD;
}

void dfs(int now)
{
    for (int i = head[now]; i != -1; i = nxt[i]) dfs(data[i]);

    memset(g, 0, sizeof g);

    if (head[now] != -1)
        FOR(k, 1, n) g[k] = f[data[head[now]]][k];

    if (head[now] != -1) 
        for (int i = nxt[head[now]]; i != -1; i = nxt[i]) {
            memset(f[now], 0, sizeof f[now]);
            FOR(k, 1, n) FOR(l, 1, n) FOR(j, Max(k, l), Min(n, k + l)) {
                f[now][j] =    (
                    f[now][j] +
                    1ll * g[k] % MOD * f[data[i]][l] % MOD *
                    C(j, k) % MOD * C(k, k + l - j) % MOD
                ) % MOD;
            }
            FOR(k, 1, n) g[k] = f[now][k];
        }

    if (head[now] == -1) f[now][1] = 1;
    else FOR(i, 1, n) f[now][i] = g[i - 1];
}

int power(int x, int y)
{
    int ret = 1;
    while (y) {
        if (y & 1) ret = 1ll * ret * x % MOD;
        x = 1ll * x * x % MOD;
        y >>= 1;
    }
    return ret;
}

void add(int x, int y)
{
    nxt[cnt] = head[x]; data[cnt] = y; head[x] = cnt++;
}

int main()
{
    freopen("pairwise.in", "r", stdin);
    freopen("pairwise.out", "w", stdout);

    memset(head, -1, sizeof head);

    jie[0] = 1;
    FOR(i, 1, 100) jie[i] = 1ll * jie[i - 1] * i % MOD;

    ni[100] = power(jie[100], MOD - 2);
    DNF(i, 99, 1) ni[i] = 1ll * ni[i + 1] * (i + 1) % MOD;

    ni[0] = 1;

    in(n); in(m);

    FOR(i, 1, n) fa[i] = i;

    FOR(i, 1, m) {
        int x, y;
        in(x); scanf("%s", compare); in(y);
        if (compare[0] == '<') {
            edge[++edge_lenth].l = x; edge[edge_lenth].r = y;
        }
        else merge(x, y);
    }

    int ans = 0;

    FOR(i, 1, edge_lenth) {
        int fx = find(edge[i].l);
        int fy = find(edge[i].r);
        if (fx == fy) {
            puts("0");
            return 0;
        }
        add(fx, fy); du[fy]++;
    }

    int rot = 0;

    FOR(i, 1, n) {
        if (!du[find(i)]) rot = find(i);
    }

    if (!rot) {
        puts("0");
        return 0;
    }

    dfs(rot);

    FOR(i, 1, n) ans = (ans + f[rot][i]) % MOD;

    printf("%d\n", ans);

    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值