LeetCode刷题笔记(Java)---第481-500题

前言

需要开通vip的题目暂时跳过

笔记导航

点击链接可跳转到所有刷题笔记的导航链接

481. 神奇字符串

神奇的字符串 S 只包含 ‘1’ 和 ‘2’,并遵守以下规则:

字符串 S 是神奇的,因为串联字符 ‘1’ 和 ‘2’ 的连续出现次数会生成字符串 S 本身。

字符串 S 的前几个元素如下:S = “1221121221221121122 …”

如果我们将 S 中连续的 1 和 2 进行分组,它将变成:

1 22 11 2 1 22 1 22 11 2 11 22 …

并且每个组中 ‘1’ 或 ‘2’ 的出现次数分别是:

1 2 2 1 1 2 1 2 2 1 2 2 …

你可以看到上面的出现次数就是 S 本身。

给定一个整数 N 作为输入,返回神奇字符串 S 中前 N 个数字中的 ‘1’ 的数目。

注意:N 不会超过 100,000。

在这里插入图片描述

  • 解答

    public int magicalString(int n) {
            StringBuilder sb = new StringBuilder("122");
            for (int i = 2, k = 1; i < n; i++, k = 3 - k){
                for (int j = sb.charAt(i) - '0'; j> 0; j--){
                    sb.append(k + "");
                }
            }
            int res = 0;
            for (int i = 0; i < n; i++){
                if (sb.charAt(i) == '1'){
                    res++;
                }
            }
            return res;
        }
    
  • 分析

    1. 根据规则构建神奇字符串。
    2. 初始化字符串“122”,从索引2开始遍历,插入字符“1”。根据当前遍历的数字,选择插入的字符的个数。1插入完之后 换成插入2,2插入完之后换成插入1.
    3. 直到遍历到n
    4. 然后统计1出现的次数。
  • 提交结果在这里插入图片描述

482. 密钥格式化

有一个密钥字符串 S ,只包含字母,数字以及 ‘-’(破折号)。其中, N 个 ‘-’ 将字符串分成了 N+1 组。

给你一个数字 K,请你重新格式化字符串,使每个分组恰好包含 K 个字符。特别地,第一个分组包含的字符个数必须小于等于 K,但至少要包含 1 个字符。两个分组之间需要用 ‘-’(破折号)隔开,并且将所有的小写字母转换为大写字母。

给定非空字符串 S 和数字 K,按照上面描述的规则进行格式化。

在这里插入图片描述

  • 解答

    public String licenseKeyFormatting(String S, int K) {
            StringBuilder sb = new StringBuilder();
            S = S.toUpperCase();
            int number = 0;
            for (int i = 0; i < S.length(); i++) {
                if (S.charAt(i) != '-') number++;
            }
            int firstNumber = number % K;
            int i = 0;
            while (firstNumber > 0) {
                char cur = S.charAt(i);
                if (cur != '-') {
                    sb.append(cur);
                    firstNumber--;
                }
                i++;
            }
            if (sb.length() > 0)
                sb.append('-');
            int temp = 0;
            for (int j = i; j < S.length(); j++) {
                char cur = S.charAt(j);
                if (cur != '-') {
                    sb.append(cur);
                    temp++;
                    if (temp == K) {
                        sb.append('-');
                        temp = 0;
                    }
                }
            }
            if(sb.length() == 0)return "";
            if (sb.charAt(sb.length() - 1) == '-') sb.deleteCharAt(sb.length() - 1);
            return sb.toString();
        }
    
  • 分析

    1. 首先计算出除了"-"以外字符的个数
    2. 根据K的取值计算出第一组字符分配的数量,除了第一组字符之外,其余每组字符的数量必须等于K
    3. 有了第一组字符的数量之后
    4. 遍历字符串S
    5. 构建第一组字符,若第一组字符的个数不为0,则在后面加一个"-"分组符号
    6. 继续遍历剩余的字符,将出了"-"以外的字符按照每组K个划分,添加到StringBuilder中
    7. 最后若答案字符串最后一位是"-",则删除它
  • 提交结果在这里插入图片描述

485. 最大连续1的个数

在这里插入图片描述

  • 解答

    public int findMaxConsecutiveOnes(int[] nums) {
            int res = 0;
            int temp = 0;
            for(int i = 0;i < nums.length;i++){
                if(nums[i] == 1)temp++;
                else{
                    res = Math.max(res,temp);
                    temp = 0;
                }
            }
            return Math.max(res,temp);
        }
    
  • 分析

    1. 太简单了,略
  • 提交结果在这里插入图片描述

486. 预测赢家

给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。

给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。

在这里插入图片描述

  • 解答

    public boolean PredictTheWinner(int[] nums) {
            return total(nums, 0, nums.length - 1) >= 0;
        }
    
        public int total(int[] nums, int start, int end) {
            if (start == end) {
                return nums[start];
            }
            int getStart = nums[start] - total(nums,start + 1,end);
            int getEnd = nums[end] - total(nums,start,end - 1);
            return Math.max(getStart,getEnd);
        }
    public boolean PredictTheWinner(int[] nums) {
            int length = nums.length;
            int[][] dp = new int[length][length];
            for (int i = 0; i < length; i++) {
                dp[i][i] = nums[i];
            }
            for (int i = length - 2; i >= 0; i--) {
                for (int j = i + 1; j < length; j++) {
                    dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
                }
            }
            return dp[0][length - 1] >= 0;
        }
    
  • 分析

    1. 递归,每次选择拿取开头的数字 或者结束的数字,赢得相应的分数

    2. 拿完之后是下一个人拿,也就是输掉相应的分数。

    3. 即赢的分数 - 前输的分数,等于当前选择结果的得分。

    4. 比较拿取开头和结束的两种结果的得分,返回较大者。

    5. 若最后的结果是大于等于0,说明一号玩家胜利。

    6. 将上述递归的过程改为动态规划dp[i] [j] 表示在i-j的范围内一号玩家取得的最优比赛结果

    7. 初始化dp[i] [i] = nums[i]

    8. 状态转移方程

      dp[i] [j] = Math.max(nums[i] - dp[i+1] [j],nums[j] - dp[i] [j-1] );

    9. 最后返回dp[0] [j-1]是否大于等于0即可

  • 提交结果

    方法1在这里插入图片描述

    方法2在这里插入图片描述

488. 祖玛游戏

回忆一下祖玛游戏。现在桌上有一串球,颜色有红色®,黄色(Y),蓝色(B),绿色(G),还有白色(W)。 现在你手里也有几个球。

每一次,你可以从手里的球选一个,然后把这个球插入到一串球中的某个位置上(包括最左端,最右端)。接着,如果有出现三个或者三个以上颜色相同的球相连的话,就把它们移除掉。重复这一步骤直到桌上所有的球都被移除。

找到插入并可以移除掉桌上所有球所需的最少的球数。如果不能移除桌上所有的球,输出 -1 。

在这里插入图片描述

  • 解答

    private int result = Integer.MAX_VALUE;
    
        private int[] map = new int[26];
    
        private char[] colors = {'R', 'Y', 'B', 'G', 'W'};
    
        public int findMinStep(String board, String hand) {
            for (int i = 0; i < hand.length(); i++) {
                map[hand.charAt(i) - 'A']++;
            }
            dfs(new StringBuilder(board), 0);
            return result == Integer.MAX_VALUE ? -1 : result;
        }
    
        private void dfs(StringBuilder board, int step) {
            if (step >= result) {
                return;
            }
            if (board.length() == 0) {
                result = Math.min(step, result);
                return;
            }
            for (int i = 0; i < board.length(); i++) {
                char c = board.charAt(i);
                int j = i;
                while (j + 1 < board.length() && board.charAt(j + 1) == c) {
                    j++;
                }
                if (j == i && map[c - 'A'] >= 2) {  //只有单个球
                    StringBuilder tmp = new StringBuilder(board);
                    tmp.insert(i, c + "" + c);
                    map[c - 'A'] -= 2;
                    dfs(eliminate(tmp), step + 2);
                    map[c - 'A'] += 2;
                } else if (j == i + 1) {    //存在两个颜色相同且相邻的球
                    if (map[c - 'A'] >= 1) {
                        StringBuilder tmp = new StringBuilder(board);
                        tmp.insert(i, c);
                        map[c - 'A']--;
                        dfs(eliminate(tmp), step + 1);
                        map[c - 'A']++;
                    }
                    for (char color : colors) {
                        if (color == c) {
                            continue;
                        }
                        if (map[color - 'A'] >= 1) {
                            StringBuilder tmp = new StringBuilder(board);
                            tmp.insert(i + 1, color);   //尝试往这两个颜色相同且相邻的球中间插入一个颜色不同的球
                            map[color - 'A']--;
                            dfs(eliminate(tmp), step + 1);
                            map[color - 'A']++;
                        }
                    }
                }
            }
        }
    
        private StringBuilder eliminate(StringBuilder sb) {
            boolean flag = true;
            while (flag) {
                flag = false;
                for (int i = 0; i < sb.length(); i++) {
                    int j = i + 1;
                    while (j < sb.length() && sb.charAt(j) == sb.charAt(i)) {
                        j++;
                    }
                    if (j - i >= 3) {
                        sb.delete(i, j);
                        flag = true;
                    }
                }
            }
            return sb;
        }
    
  • 分析

    1. 回溯法

    2. 递归退出条件

      当前步数大于已得的最小结果,停止递归

      字符串全部消除,即长度为0,更新结果,停止递归

    3. 遍历字符串

    4. 以一个球为基准,判断是否有相邻相同的球

    5. 若只有单个球,则需要使用2个球来消除这一个球

    6. 若存在2个颜色相同的球,则可以使用1个相同颜色的球消除

    7. 或在两个颜色相同的球之间放入一颗颜色不同的球

    8. 处理完球的插入,调用消除函数eliminate,while循环去掉连续的3个相同颜色的球。然后递归,步数 + 1

  • 提交结果在这里插入图片描述

491. 递增子序列

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

在这里插入图片描述

  • 解答

    List<List<Integer>> res = new ArrayList<>();
        
        public List<List<Integer>> findSubsequences(int[] nums) {
            dfs(nums, -1, new ArrayList<>());
            return res;
        }
    
        private void dfs(int[] nums, int idx, List<Integer> curList) {
            if (curList.size() > 1) {
                res.add(new ArrayList<>(curList));
            }
    
            Set<Integer> set = new HashSet<>();
            for (int i = idx + 1; i < nums.length; i++) {
                // 如果 set 中已经有与 nums[i] 相同的值了,说明加上 nums[i] 后的所有可能的递增序列之前已经被搜过一遍了,因此停止继续搜索。
                if (set.contains(nums[i])) { 
                    continue;
                }
                set.add(nums[i]);
                if (idx == -1 || nums[i] >= nums[idx]) {
                    curList.add(nums[i]);
                    dfs(nums, i, curList);
                    curList.remove(curList.size() - 1);
                }
            }
        }
    
  • 分析

    1. 回溯法

    2. 用set集合,表示这一层递归不会选择到相同的数字

    3. 递归的两种情况

      第一种是curList为空,加入元素

      第二种是当前元素大雨curList中的最后一个元素值。

  • 提交结果在这里插入图片描述

492. 构造矩形

作为一位web开发者, 懂得怎样去规划一个页面的尺寸是很重要的。 现给定一个具体的矩形页面面积,你的任务是设计一个长度为 L 和宽度为 W 且满足以下要求的矩形的页面。要求:

在这里插入图片描述

  • 解答

    public int[] constructRectangle(int area) {
            int L = (int)Math.sqrt(area);
            int W = L;
            while(L * W != area){
                if(L * W < area){
                    L++;
                }else if (L * W > area){
                    W--;
                }
            }
            return new int[]{L,W};
        }
    
  • 分析

    1. 先将area开方取整。

    2. 初始化L和W

    3. while循环

      若L * W小于 area 则L+1

      若L * W大于area 则W-1;

    4. 找到等于area的L和W返回

  • 提交结果在这里插入图片描述

493. 翻转对

给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。

你需要返回给定数组中的重要翻转对的数量。

在这里插入图片描述

  • 解答

    				int N;
            long[] tr;
            public int reversePairs(int[] nums) {
                List<Long> ys = new ArrayList();
                for(int i: nums) {//离散化
                    ys.add((long)i);
                    ys.add((long)i * 2);
                }
                Collections.sort(ys);//排序
                ys = unique(ys);//去重
                N = ys.size();
                tr = new long[N + 1];//树状数组
                int ans = 0;
                for(int i = 0; i < nums.length; i++){//从左到右遍历数组
                    long target = (long)nums[i] * 2;//目标值
                    int left = binaryFind(ys, target) + 1;//寻找target的位置
                    int right = N;
                    ans += query(right) - query(left);//树状数组中 大于 target部分的个数
                    add(binaryFind(ys, nums[i]) + 1, 1);//更新树状数组的结点和
                }
                return ans;
            }
    				//更新
            public void add(int x, int c){
                for(int i = x; i <= N; i += lowBit(i)) tr[i] += c;
            }
    				//查询
            public int query(int x){
                int res = 0;
                for(int i = x; i > 0; i -= lowBit(i)) res += tr[i];
                return res;
            }
    				//计算x最右侧的1
            public int lowBit(int x){return x & -x;}
    
    				//去重
            public List<Long> unique(List<Long> list){
                List<Long> res = new ArrayList(list.size());
                for(int i = 0; i < list.size(); i++){
                    if(res.isEmpty() || res.get(res.size() - 1) - list.get(i) != 0){
                        res.add(list.get(i));
                    }
                }
                return res;
            }
    				//二分查找
            public int binaryFind(List<Long> list, long target){
                int l = 0, r = list.size() - 1;
                while(l < r){
                    int mid = l + r >> 1;
                    if(list.get(mid) >= target) r = mid;
                    else l = mid + 1;
                }
                return l;
            }
    
  • 分析

    1. 统计数组中的值以及每个值✖️2的结果,排序后去重。
    2. 根据去重后的数量,构建树状数组。
    3. 树状数组中保存的是已经遍历过的数字及其的子结点的个数。
    4. 从左往右遍历原始数组。
    5. 当前遍历的数字✖️2得到树状数组中查询的目标值。
    6. 根据这个目标值,在树状数组中找比他大的值的和,加入到答案中。
    7. 更新树状数组,将遍历的数字加入
    8. 下图是树状数组的更新和查询的过程在这里插入图片描述
  • 提交结果在这里插入图片描述

494. 目标和

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

在这里插入图片描述

  • 解答

    public int findTargetSumWays(int[] nums, int S) {
            int len = nums.length;
            int[] preSum = new int[len + 1];
            for(int i = 0;i < len;i++){
                preSum[i + 1] = preSum[i] + nums[i];
            }
            if(preSum[len] < S)return 0;
            dfs(preSum,nums,0,0,S,len);
            return res;
        }
        int res = 0;
    
        public void dfs(int[] preSum,int[] nums,int temp,int position,int S,int len){
            if(position == len && temp == S){
                res++;
                return;
            }
            if(position == len)return;
            if(temp + preSum[len] - preSum[position] < S)return;
            dfs(preSum,nums,temp - nums[position],position + 1,S,len);
            dfs(preSum,nums,temp + nums[position],position + 1,S,len);
        }
    
  • 分析

    1. 先计算前缀和
    2. 若nums的总和小于S,返回0
    3. 回溯法
    4. 剪枝条件:当前得到的数组和 加上后续数组中所有数字的和小于目标的话,剪枝
    5. 若已遍历完所有的数组且得到的和等于目标,则答案加1
    6. 递归,两种情况,添加负号或者加号
  • 提交结果在这里插入图片描述

495. 提莫攻击

在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄,他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。现在,给出提莫对艾希的攻击时间序列和提莫攻击的中毒持续时间,你需要输出艾希的中毒状态总时长。

你可以认为提莫在给定的时间点进行攻击,并立即使艾希处于中毒状态。

在这里插入图片描述

  • 解答

    public int findPoisonedDuration(int[] timeSeries, int duration) {
            if(timeSeries.length == 0)return 0;
            int res = 0;
            int cur = timeSeries[0];
            for(int i = 1;i < timeSeries.length;i++){
                if(timeSeries[i] - cur > duration){
                    res += duration;
                }else{
                    res += timeSeries[i] - cur;
                }
                cur = timeSeries[i];
            }
            return res + duration;
        }
    
  • 分析

    1. 判断两次攻击之前的间隔是否大于duration 若大于 则中毒时间 + duration
    2. 若小于 则中毒时间 + 两次攻击的间隔时间
    3. 最后再加上最后一次攻击后中毒的时间 也就是duration
  • 提交结果在这里插入图片描述

496. 下一个更大元素 I

给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

在这里插入图片描述

  • 解答

    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
            int len = nums1.length;
            int[] res = new int[len];
            Map<Integer,Integer> map = new HashMap<>();
            for(int i = 0;i < nums2.length;i++){
                map.put(nums2[i],i);
            }
            for(int i = 0;i < len;i++){
                int cur = nums1[i];
                boolean flag = false;
                for(int j = map.get(cur);j < nums2.length;j++){
                    if(nums2[j] > cur){
                        res[i] = nums2[j];
                        flag = true;
                        break;
                    }
                }
                if(!flag)res[i] = -1;
            }
            return res;
        }
    
  • 分析

    1. 用map记录下数组2中 元素对应的索引位置
    2. 遍历数组1,当前遍历的数字记为cur
    3. 从map中找到数组2中对应cur的索引位置开始遍历数组2,找到第一个比当cur大的数字,记录在res当中。若没有找到则记为-1
  • 提交结果在这里插入图片描述

497. 非重叠矩形中的随机点

给定一个非重叠轴对齐矩形的列表 rects,写一个函数 pick 随机均匀地选取矩形覆盖的空间中的整数点。

提示:

  1. 整数点是具有整数坐标的点。
  2. 矩形周边上的点包含在矩形覆盖的空间中。
  3. 第 i 个矩形 rects [i] = [x1,y1,x2,y2],其中 [x1,y1] 是左下角的整数坐标,[x2,y2] 是右上角的整数坐标。
  4. 每个矩形的长度和宽度不超过 2000。
  5. 1 <= rects.length <= 100
  6. pick 以整数坐标数组 [p_x, p_y] 的形式返回一个点。
  7. pick 最多被调用10000次。

在这里插入图片描述

  • 解答

    int[][] rects;
        List<Integer> psum = new ArrayList<>();
        int tot = 0;
        Random rand = new Random();
    
        public Solution(int[][] rects) {
            this.rects = rects;
            for (int[] x : rects){
                tot += (x[2] - x[0] + 1) * (x[3] - x[1] + 1);
                psum.add(tot);
            }
        }
    
        public int[] pick() {
            int targ = rand.nextInt(tot);
    
            int lo = 0;
            int hi = rects.length - 1;
            while (lo != hi) {
                int mid = (lo + hi) / 2;
                if (targ >= psum.get(mid)) lo = mid + 1;
                else hi = mid;
            }
    
            int[] x = rects[lo];
            int width = x[2] - x[0] + 1;
            int height = x[3] - x[1] + 1;
            int base = psum.get(lo) - width * height;
            return new int[]{x[0] + (targ - base) % width, x[1] + (targ - base) / width};
        }
    
  • 分析

    1. 根据每个矩形的面积占比,得到每个矩形被选择的概率。
    2. 所以需要先计算每个矩形的面积,然后累加起来的结果存起来。这样每个区间对应一个矩形。
    3. 在所有面积和的范围内随机生成一个数targ
    4. 通过二分查找得到targ所在的矩形区间。也就确定了在哪一个矩形中
    5. 然后基于targ 生成坐标点
  • 提交结果在这里插入图片描述

498. 对角线遍历

给定一个含有 M x N 个元素的矩阵(M 行,N 列),请以对角线遍历的顺序返回这个矩阵中的所有元素,对角线遍历如下图所示。

在这里插入图片描述

  • 解答

    public int[] findDiagonalOrder(int[][] matrix) {
            if(matrix.length == 0 || matrix[0].length == 0)return new int[0];
            int r = matrix.length;
            int l = matrix[0].length;
            int[] res = new int[r * l];
            int rowIndex = 0;
            int colIndex = 0;
            boolean flag = true;
            int index = 0;
            while(true){
                res[index++] = matrix[rowIndex][colIndex];
                if(rowIndex == r-1 && colIndex == l-1)break;
                if(flag){
                    if(rowIndex - 1 >= 0 && colIndex + 1< l){
                        rowIndex--;
                        colIndex++;
                    }else if(colIndex + 1 < l){
                        colIndex++;
                        flag = !flag;
                    }else{
                        rowIndex++;
                        flag = !flag;
                    }
                }else{
                    if(rowIndex + 1 < r && colIndex - 1 >=0){
                        rowIndex++;
                        colIndex--;
                    }else if(rowIndex + 1 < r){
                        rowIndex++;
                        flag = !flag;
                    }else{
                        colIndex++;
                        flag = !flag;
                    }
                }
            }
            return res;
        }
    
  • 分析

    1. 对角线遍历有两个方向,所以用flag来表示遍历的方向
    2. 每种遍历方向上有3种情况
      1. 换行换列
      2. 只换列
      3. 只换行
    3. 直到遍历到右下角结束
  • 提交结果在这里插入图片描述

500. 键盘行

给定一个单词列表,只返回可以使用在键盘同一行的字母打印出来的单词。键盘如下图所示。

在这里插入图片描述

  • 解答

    public String[] findWords(String[] words) {
            int[] ls = new int[]{2,3,3,2,1,2,2,2,1,2,2,2,3,3,1,1,1,1,2,1,1,3,1,3,1,3};
            List<String> list = new ArrayList<>();
            for(String str:words){
                char[] ch = str.toCharArray();
                int temp = -1;
                if(ch[0] >= 'A' && ch[0] <= 'Z')temp = ls[ch[0] - 'A'];
                if(ch[0] >= 'a' && ch[0] <= 'z')temp = ls[ch[0] - 'a'];
                for(int i = 1;i < ch.length;i++){
                    char cur = ch[i];
                    int num =  - 1;
                    if(ch[i] >= 'A' && ch[i] <= 'Z')num = ls[ch[i] - 'A'];
                    if(ch[i] >= 'a' && ch[i] <= 'z')num = ls[ch[i] - 'a'];
                    if(num != temp){
                        temp = -1;
                        break;
                    }
                }
                if(temp != -1)list.add(str);
            }
            return list.toArray(new String[list.size()]);
        }
    
  • 分析

    1. 数组ls记录字母出现在键盘上的行号
    2. 遍历words
    3. 依次的判断每个字符串中的字符是否在同一行出现,是的话加入list
    4. 最后将list转换成数组输出
  • 提交结果在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值