DLX AlgorithmX

本文介绍了精确覆盖问题及其解决方案——Knuth的Algorithm X及基于此的DLX算法。通过矩阵表示问题,并采用高效的回溯搜索策略寻找所有可能的覆盖方案。

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

AlgorithmX精确覆盖:
https://en.wikipedia.org/wiki/Knuth%27s_Algorithm_X
DLX的基础算法
https://zh.wikipedia.org/wiki/%E8%88%9E%E8%B9%88%E9%93%BE
论文:
https://arxiv.org/pdf/cs/0011047v1.pdf
中文版:
http://io.sqybi.com/dlxcn/#p11
精确覆盖的思想是选取n行中的x行,以至于m列都恰好一个1,可以求出所有解,也可以根据需要求最优解
为什么这玩意能解决精确覆盖呢,比如有nm的矩形让你用k个小矩形恰好覆盖,不相互覆盖
这里由于algorithmX解决的是所有列都恰好覆盖,好像不对啊,所以可以把n
m的矩形拉成一条直线,每个格子都是一列,这样就可以恰好覆盖所有列,也就是恰好覆盖n*m的矩形了
然后这里每行对应的列,就是原来每个小矩形对应的列,映射一下,构图,然后就可以用DLX解决了

#include <cstdio>
#include <memory>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <cassert>
#include <string>
#include <ctime>
#include <map>
#include <queue>
#include <algorithm>
#include <iostream>
#include <cassert>
#include <iomanip> 
#include <set>
#include <iterator>  
using namespace std;
#define REP(i,n) for(int i=0;i<n;i++)
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define req(i,a,b) for(int i=a;i>=b;i--)
#define rp(i,a) for(int i=head[a];i+1;i=edge[i].next)
#define cl(a,b) memset(a,b,sizeof a);
#define ll long long
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define mod 10007
const int inf = ~0u >> 2;
const ll INF = (1LL << 62) - 1;
double eps = 1e-12;
const int N = 200005 + 5;
const int M = 505;
int s[N];

int n, m;
int a[M][M];
class AlgorithmX {
private:
    bool dfs(vector<int> rows,vector<int> cols) {
        if (rows.size() == 0 && cols.size() == 0) {
            return true;
        }
        Content content(rows, cols);
        int minum = content.findMinColIndex(a);
        if (content.calColOneCount(minum) == 0)
            return false;
        vector<int> removeRows;
        vector<int> removeCols;
        for (int x = 0; x < rows.size(); x++) {
            int i = rows[x];
            if (a[i][minum] == 1) {
                tmpAnsRows.push_back(i);
                for (int y = 0; y < cols.size(); y++)
                {
                    int j = cols[y];
                    if (a[i][j] == 1)
                        removeCols.push_back(j);
                }
                for (int y = 0; y < removeCols.size(); y++) {
                    int j = removeCols[y];
                    for (int x2 = 0; x2 < rows.size(); x2++) {
                        int i2 = rows[x2];
                        if (a[i2][j] == 1) {
                            removeRows.push_back(i2);
                        }
                    }
                }
                unique(removeRows.begin(), removeRows.end());
                vector<int> lessRows;
                set_difference(rows.begin(), rows.end(), removeRows.begin(), removeRows.end(), insert_iterator<vector<int>>(lessRows,lessRows.begin()));
                vector<int> lessCols;
                set_difference(cols.begin(), cols.end(), removeCols.begin(), removeCols.end(), insert_iterator<vector<int>>(lessCols,lessCols.begin()));
                if (dfs(lessRows, lessCols)) {
                    if (ansRows.size() == 0 || ansRows.size() < tmpAnsRows.size())
                        ansRows = tmpAnsRows;
                    cnt++;
                }
                tmpAnsRows.pop_back();
            }
        }
        return false;
    }
public:
    void init(int n) {
        for (int i = 0; i < n; i++)
            rows.push_back(i);
        for (int j = 0; j < n; j++)
            cols.push_back(j);
    }
    class Content {
    public:
        Content(vector<int> rows, vector<int> cols) {
            this->rows = rows;
            this->cols = cols;
        }
    private:
        vector<int> rows;
        vector<int> cols;
    public:
        int calColOneCount(int pos) {
            int num = 0;
            for (int j = 0; j < n; j++)
                num += a[pos][j];
            return num;
        }
        int findMinColIndex(int a[M][M]) {
            int nums[M] = { 0 };
            int minum = cols[0];
            for (int y = 0; y < cols.size(); y++)
            {
                int j = cols[y];
                nums[j] = 0;
                for (int x = 0; x < rows.size(); x++) {
                    int i = rows[x];
                    nums[j] += a[i][j];
                }
                if (nums[j] < nums[minum])
                    minum = j;
            }
            return minum;
        }
    };
    void algorithmX() {
        dfs(rows,cols);
    }
    vector<int> rows;
    vector<int> cols;
    vector<int> tmpAnsRows;
    vector<int> ansRows;
    int cnt = 0;
}x;
int main() {
    int top = 0;
    cin >> n;   
    for(int i=0;i<n;i++)
        for (int j = 0; j < n; j++) {
            cin >> a[i][j];
        }
    x.init(n);
    x.algorithmX();
    return 0;
}

DLX:

#include <cstdio>
#include <memory>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <cassert>
#include <string>
#include <ctime>
#include <map>
#include <queue>
#include <algorithm>
#include <iostream>
#include <cassert>
#include <iomanip> 
#include <set>
#include <iterator>  
using namespace std;
#define REP(i,n) for(int i=0;i<n;i++)
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define req(i,a,b) for(int i=a;i>=b;i--)
#define rp(i,a) for(int i=head[a];i+1;i=edge[i].next)
#define cl(a,b) memset(a,b,sizeof a);
#define ll long long
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define mod 10007
const int inf = ~0u >> 2;
const ll INF = (1LL << 62) - 1;
double eps = 1e-12;
const int N = 505 + 5;
const int M = 35*35;
int s[N];

int n, m, k;
//int a[M][M];
int b[M*N+M];
class DLX {
private:
    const static int maxn = M*N+M;
public:
    int l[maxn], r[maxn], u[maxn], d[maxn], s[M];
    int row[maxn],col[maxn],head[N];
    int sz = 0;
    void init(int n,int mm) {
        cnt = 0;
        ans = inf;
        level = 0;
        sz = 0;
        for (int j = 0; j <= mm; j++)
        {
            row[j] = 0;
            col[j] = j;
            s[j] = 0;
            l[j] = j - 1;
            r[j] = j + 1;
            u[j] = d[j] = j;
            sz++;
        }
        sz--;
        l[0] = mm;
        r[mm] = 0;
        for (int i = 1; i <= n; i++)
            head[i] = -1;
    }
    void remove(int y) {//y is column index
        r[l[y]] = r[y];
        l[r[y]] = l[y];
        for (int i = d[y]; i != y; i = d[i]) {
            for (int j = r[i]; j != i; j = r[j]) {
                d[u[j]] = d[j];
                u[d[j]] = u[j];
                s[col[j]]--;
            }
        }
    }
    void resume(int y) {//y is column index
        for (int i = u[y]; i != y; i = u[i]) {
            for (int j = l[i]; j != i; j = l[j]) {
                d[u[j]] = j;
                u[d[j]] = j;
                s[col[j]]++;
            }
        }
        l[r[y]] = y;
        r[l[y]] = y;
    }
    void link(int rowIndex, int colIndex) {
        col[++sz] = colIndex;
        s[colIndex]++;
        d[sz] = d[colIndex];
        u[sz] = colIndex;
        u[d[sz]] = sz;
        d[u[sz]] = sz;
        if (head[rowIndex] == -1)
        {
            l[sz] = r[sz] = sz;
            head[rowIndex] = sz;
        }
        else {
            l[sz] = head[rowIndex];
            r[sz] = r[head[rowIndex]];
            l[r[sz]] = sz;
            r[l[sz]] = sz;
        }
        
    }
    bool dance() {
        if (r[0] == 0) {
            return true;
        }
        int minum = r[0];
        for (int i = r[0]; i != 0; i = r[i]) {
            if (s[i] < s[minum])
                minum = i;
        }
        if (s[minum] == 0) {
            return false;
        }

        level++;
        if (level >= ans)
        {
            level--;
            return false;
        }
        remove(minum);

        for (int i = d[minum]; i != minum; i = d[i]) {
            for (int j = r[i]; j != i; j = r[j]) {
                remove(col[j]);
            }
            bool flag = dance();
            for (int j = l[i]; j != i; j = l[j]) {
                resume(col[j]);
            }
            if (flag||level>=ans) {
                cnt++;
                ans = min(ans, level);
                break;
            }
        }

        resume(minum);
        level--;

        return false;
    }
    int getAns() {
        return ans;
    }
private:
    int cnt = 0;
    int ans = inf;
    int level = 0;
}dlx;
int main() {
    int t;
    //std::ios::sync_with_stdio(false);
    scanf("%d", &t);
    while (t--) {
        scanf("%d%d%d", &n, &m, &k);
        int mm = n*m;
        dlx.init(k,mm);
        for (int p = 1; p <= k; p++) {
            int x, y, xx, yy;
            scanf("%d%d%d%d", &x, &y, &xx, &yy);
            
            for (int i = x + 1; i <= xx; i++)
            {
                for (int j = y + 1; j <= yy; j++) {
                    dlx.link(p, (i - 1)*m + j);
                }
            }
        }
        dlx.dance();
        printf("%d\n", dlx.getAns()==inf?-1:dlx.getAns());
    }
    return 0;
}

另外附上Hadoop 上的DancingLinks:

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.examples.dancing;

import java.util.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A generic solver for tile laying problems using Knuth's dancing link
 * algorithm. It provides a very fast backtracking data structure for problems
 * that can expressed as a sparse boolean matrix where the goal is to select a
 * subset of the rows such that each column has exactly 1 true in it.
 * 
 * The application gives each column a name and each row is named after the
 * set of columns that it has as true. Solutions are passed back by giving the 
 * selected rows' names.
 * 
 * The type parameter ColumnName is the class of application's column names.
 */
public class DancingLinks<ColumnName> {
  private static final Log LOG = 
    LogFactory.getLog(DancingLinks.class.getName());
  
  /**
   * A cell in the table with up/down and left/right links that form doubly
   * linked lists in both directions. It also includes a link to the column
   * head.
   */
  private static class Node<ColumnName> {
    Node<ColumnName> left;
    Node<ColumnName> right;
    Node<ColumnName> up;
    Node<ColumnName> down;
    ColumnHeader<ColumnName> head;
    
    Node(Node<ColumnName> l, Node<ColumnName> r, Node<ColumnName> u, 
         Node<ColumnName> d, ColumnHeader<ColumnName> h) {
      left = l;
      right = r;
      up = u;
      down = d;
      head = h;
    }
    
    Node() {
      this(null, null, null, null, null);
    }
  }
  
  /**
   * Column headers record the name of the column and the number of rows that 
   * satisfy this column. The names are provided by the application and can 
   * be anything. The size is used for the heuristic for picking the next 
   * column to explore.
   */
  private static class ColumnHeader<ColumnName> extends Node<ColumnName> {
    ColumnName name;
    int size;
    
    ColumnHeader(ColumnName n, int s) {
      name = n;
      size = s;
      head = this;
    }
    
    ColumnHeader() {
      this(null, 0);
    }
  }
  
  /**
   * The head of the table. Left/Right from the head are the unsatisfied 
   * ColumnHeader objects.
   */
  private ColumnHeader<ColumnName> head;
  
  /**
   * The complete list of columns.
   */
  private List<ColumnHeader<ColumnName>> columns;
  
  public DancingLinks() {
    head = new ColumnHeader<ColumnName>(null, 0);
    head.left = head;
    head.right = head;
    head.up = head;
    head.down = head;
    columns = new ArrayList<ColumnHeader<ColumnName>>(200);
  }
  
  /**
   * Add a column to the table
   * @param name The name of the column, which will be returned as part of 
   *             solutions
   * @param primary Is the column required for a solution?
   */
  public void addColumn(ColumnName name, boolean primary) {
    ColumnHeader<ColumnName> top = new ColumnHeader<ColumnName>(name, 0);
    top.up = top;
    top.down = top;
    if (primary) {
      Node<ColumnName> tail = head.left;
      tail.right = top;
      top.left = tail;
      top.right = head;
      head.left = top;
    } else {
      top.left = top;
      top.right = top;
    }
    columns.add(top);
  }
  
  /**
   * Add a column to the table
   * @param name The name of the column, which will be included in the solution
   */
  public void addColumn(ColumnName name) {
    addColumn(name, true);
  }
  
  /**
   * Get the number of columns.
   * @return the number of columns
   */
  public int getNumberColumns() {
    return columns.size();
  }
  
  /**
   * Get the name of a given column as a string
   * @param index the index of the column
   * @return a string representation of the name
   */
  public String getColumnName(int index) {
    return columns.get(index).name.toString();
  }
  
  /**
   * Add a row to the table. 
   * @param values the columns that are satisfied by this row
   */
  public void addRow(boolean[] values) {
    Node<ColumnName> prev = null;
    for(int i=0; i < values.length; ++i) {
      if (values[i]) {
        ColumnHeader<ColumnName> top = columns.get(i);
        top.size += 1;
        Node<ColumnName> bottom = top.up;
        Node<ColumnName> node = new Node<ColumnName>(null, null, bottom, 
                                                     top, top);
        bottom.down = node;
        top.up = node;
        if (prev != null) {
          Node<ColumnName> front = prev.right;
          node.left = prev;
          node.right = front;
          prev.right = node;
          front.left = node;
        } else {
          node.left = node;
          node.right = node;
        }
        prev = node;
      }
    }
  }
  
  /**
   * Applications should implement this to receive the solutions to their 
   * problems.
   */
  public interface SolutionAcceptor<ColumnName> {
    /**
     * A callback to return a solution to the application.
     * @param value a List of List of ColumnNames that were satisfied by each
     *              selected row
     */
    void solution(List<List<ColumnName>> value);
  }
  
  /**
   * Find the column with the fewest choices.
   * @return The column header
   */
  private ColumnHeader<ColumnName> findBestColumn() {
    int lowSize = Integer.MAX_VALUE;
    ColumnHeader<ColumnName> result = null;
    ColumnHeader<ColumnName> current = (ColumnHeader<ColumnName>) head.right;
    while (current != head) {
      if (current.size < lowSize) {
        lowSize = current.size;
        result = current;
      }
      current = (ColumnHeader<ColumnName>) current.right;
    }
    return result;
  }
  
  /**
   * Hide a column in the table
   * @param col the column to hide
   */
  private void coverColumn(ColumnHeader<ColumnName> col) {
    LOG.debug("cover " + col.head.name);
    // remove the column
    col.right.left = col.left;
    col.left.right = col.right;
    Node<ColumnName> row = col.down;
    while (row != col) {
      Node<ColumnName> node = row.right;
      while (node != row) {
        node.down.up = node.up;
        node.up.down = node.down;
        node.head.size -= 1;
        node = node.right;
      }
      row = row.down;
    }
  }
  
  /**
   * Uncover a column that was hidden.
   * @param col the column to unhide
   */
  private void uncoverColumn(ColumnHeader<ColumnName> col) {
    LOG.debug("uncover " + col.head.name);
    Node<ColumnName> row = col.up;
    while (row != col) {
      Node<ColumnName> node = row.left;
      while (node != row) {
        node.head.size += 1;
        node.down.up = node;
        node.up.down = node;
        node = node.left;
      }
      row = row.up;
    }
    col.right.left = col;
    col.left.right = col;
  }
  
  /**
   * Get the name of a row by getting the list of column names that it 
   * satisfies.
   * @param row the row to make a name for
   * @return the list of column names
   */
  private List<ColumnName> getRowName(Node<ColumnName> row) {
    List<ColumnName> result = new ArrayList<ColumnName>();
    result.add(row.head.name);
    Node<ColumnName> node = row.right;
    while (node != row) {
      result.add(node.head.name);
      node = node.right;
    }
    return result;
  }
  
  /**
   * Find a solution to the problem.
   * @param partial a temporary datastructure to keep the current partial 
   *                answer in
   * @param output the acceptor for the results that are found
   * @return the number of solutions found
   */
  private int search(List<Node<ColumnName>> partial, SolutionAcceptor<ColumnName> output) {
    int results = 0;
    if (head.right == head) {
      List<List<ColumnName>> result = new ArrayList<List<ColumnName>>(partial.size());
      for(Node<ColumnName> row: partial) {
        result.add(getRowName(row));
      }
      output.solution(result);
      results += 1;
    } else {
      ColumnHeader<ColumnName> col = findBestColumn();
      if (col.size > 0) {
        coverColumn(col);
        Node<ColumnName> row = col.down;
        while (row != col) {
          partial.add(row);
          Node<ColumnName> node = row.right;
          while (node != row) {
            coverColumn(node.head);
            node = node.right;
          }
          results += search(partial, output);
          partial.remove(partial.size() - 1);
          node = row.left;
          while (node != row) {
            uncoverColumn(node.head);
            node = node.left;
          }
          row = row.down;
        }
        uncoverColumn(col);
      }
    }
    return results;
  }
  
  /**
   * Generate a list of prefixes down to a given depth. Assumes that the 
   * problem is always deeper than depth.
   * @param depth the depth to explore down
   * @param choices an array of length depth to describe a prefix
   * @param prefixes a working datastructure
   */
  private void searchPrefixes(int depth, int[] choices, 
                              List<int[]> prefixes) {
    if (depth == 0) {
      prefixes.add(choices.clone());
    } else {
      ColumnHeader<ColumnName> col = findBestColumn();
      if (col.size > 0) {
        coverColumn(col);
        Node<ColumnName> row = col.down;
        int rowId = 0;
        while (row != col) {
          Node<ColumnName> node = row.right;
          while (node != row) {
            coverColumn(node.head);
            node = node.right;
          }
          choices[choices.length - depth] = rowId;
          searchPrefixes(depth - 1, choices, prefixes);
          node = row.left;
          while (node != row) {
            uncoverColumn(node.head);
            node = node.left;
          }
          row = row.down;
          rowId += 1;
        }
        uncoverColumn(col);
      }
    }
  }
  
  /**
   * Generate a list of row choices to cover the first moves.
   * @param depth the length of the prefixes to generate
   * @return a list of integer arrays that list the rows to pick in order
   */
  public List<int[]> split(int depth) {
    int[] choices = new int[depth];
    List<int[]> result = new ArrayList<int[]>(100000);
    searchPrefixes(depth, choices, result);
    return result;
  }

  /**
   * Make one move from a prefix
   * @param goalRow the row that should be choosen
   * @return the row that was found
   */
  private Node<ColumnName> advance(int goalRow) {
    ColumnHeader<ColumnName> col = findBestColumn();
    if (col.size > 0) {
      coverColumn(col);
      Node<ColumnName> row = col.down;
      int id = 0;
      while (row != col) {
        if (id == goalRow) {
          Node<ColumnName> node = row.right;
          while (node != row) {
            coverColumn(node.head);
            node = node.right;
          }
          return row;
        }
        id += 1;
        row = row.down;
      }
    }
    return null;
  }
  
  /**
   * Undo a prefix exploration
   * @param row
   */
  private void rollback(Node<ColumnName> row) {
    Node<ColumnName> node = row.left;
    while (node != row) {
      uncoverColumn(node.head);
      node = node.left;
    }
    uncoverColumn(row.head);
  }
  
  /**
   * Given a prefix, find solutions under it.
   * @param prefix a list of row choices that control which part of the search
   *               tree to explore
   * @param output the output for each solution
   * @return the number of solutions
   */
  public int solve(int[] prefix, SolutionAcceptor<ColumnName> output) {
    List<Node<ColumnName>> choices = new ArrayList<Node<ColumnName>>();
    for(int i=0; i < prefix.length; ++i) {
      choices.add(advance(prefix[i]));
    }
    int result = search(choices, output);
    for(int i=prefix.length-1; i >=0; --i) {
      rollback(choices.get(i));
    }
    return result;
  }
  
  /**
   * Solve a complete problem
   * @param output the acceptor to receive answers
   * @return the number of solutions
   */
  public int solve(SolutionAcceptor<ColumnName> output) {
    return search(new ArrayList<Node<ColumnName>>(), output);
  }
  
}

转载于:https://www.cnblogs.com/HaibaraAi/p/6512364.html

基于Spring Boot搭建的一个多功能在线学习系统的实现细节。系统分为管理员和用户两个主要模块。管理员负责视频、文件和文章资料的管理以及系统运营维护;用户则可以进行视频播放、资料下载、参与学习论坛并享受个性化学习服务。文中重点探讨了文件下载的安全性和性能优化(如使用Resource对象避免内存溢出),积分排行榜的高效实现(采用Redis Sorted Set结构),敏感词过滤机制(利用DFA算法构建内存过滤树)以及视频播放的浏览器兼容性解决方案(通过FFmpeg调整MOOV原子位置)。此外,还提到了权限管理方面自定义动态加载器的应用,提高了系统的灵活性和易用性。 适合人群:对Spring Boot有一定了解,希望深入理解其实际应用的技术人员,尤其是从事在线教育平台开发的相关从业者。 使用场景及目标:适用于需要快速搭建稳定高效的在线学习平台的企业或团队。目标在于提供一套完整的解决方案,涵盖从资源管理到用户体验优化等多个方面,帮助开发者更好地理解和掌握Spring Boot框架的实际运用技巧。 其他说明:文中不仅提供了具体的代码示例和技术思路,还分享了许多实践经验教训,对于提高项目质量有着重要的指导意义。同时强调了安全性、性能优化等方面的重要性,确保系统能够应对大规模用户的并发访问需求。
标题基于SpringBoot的学生学习成果管理平台研究AI更换标题第1章引言介绍研究背景、目的、意义以及论文结构。1.1研究背景与目的阐述学生学习成果管理的重要性及SpringBoot技术的优势。1.2研究意义分析该平台对学生、教师及教育机构的意义。1.3论文方法与结构简要介绍论文的研究方法和整体结构。第2章相关理论与技术概述SpringBoot框架、学习成果管理理论及相关技术。2.1SpringBoot框架简介介绍SpringBoot的基本概念、特点及应用领域。2.2学习成果管理理论基础阐述学习成果管理的核心理论和发展趋势。2.3相关技术分析分析平台开发所涉及的关键技术,如数据库、前端技术等。第3章平台需求分析与设计详细分析平台需求,并设计整体架构及功能模块。3.1需求分析从学生、教师、管理员等角度对平台需求进行深入分析。3.2整体架构设计设计平台的整体架构,包括技术架构和逻辑架构。3.3功能模块设计具体设计平台的核心功能模块,如成果展示、数据分析等。第4章平台实现与测试阐述平台的实现过程,并进行功能测试与性能分析。4.1平台实现详细介绍平台的开发环境、关键代码实现及技术难点解决方案。4.2功能测试对平台各项功能进行全面测试,确保功能正确无误。4.3性能分析分析平台的性能指标,如响应时间、并发处理能力等。第5章平台应用与效果评估探讨平台在实际教学中的应用,并对其效果进行评估。5.1平台应用案例选取典型应用案例,展示平台在实际教学中的使用情况。5.2效果评估方法介绍平台效果评估的具体方法和指标。5.3评估结果分析根据评估数据,对平台的应用效果进行深入分析。第6章结论与展望总结论文的主要研究成果,并指出未来研究方向。6.1研究结论概括性地阐述论文的研究结论和主要贡献。6.2研究展望针对当前研究的不足之处,提出未来改进和扩展的方向。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值