FP-Tree算法的实现

在关联规则挖掘领域最经典的算法法是Apriori,其致命的缺点是需要多次扫描事务数据库。于是人们提出了各种裁剪(prune)数据集的方法以减少I/O开支,韩嘉炜老师的FP-Tree算法就是其中非常高效的一种。

支持度和置信度

严格地说Apriori和FP-Tree都是寻找频繁项集的算法,频繁项集就是所谓的“支持度”比较高的项集,下面解释一下支持度和置信度的概念。

设事务数据库为:

复制代码
A  E  F  G

A  F  G

A  B  E  F  G

E  F  G
复制代码

则{A,F,G}的支持度数为3,支持度为3/4。

{F,G}的支持度数为4,支持度为4/4。

{A}的支持度数为3,支持度为3/4。

{F,G}=>{A}的置信度为:{A,F,G}的支持度数 除以 {F,G}的支持度数,即3/4

{A}=>{F,G}的置信度为:{A,F,G}的支持度数 除以 {A}的支持度数,即3/3

强关联规则挖掘是在满足一定支持度的情况下寻找置信度达到阈值的所有模式。

FP-Tree算法

我们举个例子来详细讲解FP-Tree算法的完整实现。

事务数据库如下,一行表示一条购物记录:

复制代码
牛奶,鸡蛋,面包,薯片

鸡蛋,爆米花,薯片,啤酒

鸡蛋,面包,薯片

牛奶,鸡蛋,面包,爆米花,薯片,啤酒

牛奶,面包,啤酒

鸡蛋,面包,啤酒

牛奶,面包,薯片

牛奶,鸡蛋,面包,黄油,薯片

牛奶,鸡蛋,黄油,薯片
复制代码

我们的目的是要找出哪些商品总是相伴出现的,比如人们买薯片的时候通常也会买鸡蛋,则[薯片,鸡蛋]就是一条频繁模式(frequent pattern)。

FP-Tree算法第一步:扫描事务数据库,每项商品按频数递减排序,并删除频数小于最小支持度MinSup的商品。(第一次扫描数据库)

薯片:7鸡蛋:7面包:7牛奶:6啤酒:4                       (这里我们令MinSup=3)

以上结果就是频繁1项集,记为F1。

第二步:对于每一条购买记录,按照F1中的顺序重新排序。(第二次也是最后一次扫描数据库)

复制代码
薯片,鸡蛋,面包,牛奶

薯片,鸡蛋,啤酒

薯片,鸡蛋,面包

薯片,鸡蛋,面包,牛奶,啤酒

面包,牛奶,啤酒

鸡蛋,面包,啤酒

薯片,面包,牛奶

薯片,鸡蛋,面包,牛奶

薯片,鸡蛋,牛奶
复制代码

第三步:把第二步得到的各条记录插入到FP-Tree中。刚开始时后缀模式为空。

插入第一条(薯片,鸡蛋,面包,牛奶)之后

插入第二条记录(薯片,鸡蛋,啤酒)

插入第三条记录(面包,牛奶,啤酒)

估计你也知道怎么插了,最终生成的FP-Tree是:

上图中左边的那一叫做表头项,树中相同名称的节点要链接起来,链表的第一个元素就是表头项里的元素。

如果FP-Tree为空(只含一个虚的root节点),则FP-Growth函数返回。

此时输出表头项的每一项+postModel,支持度为表头项中对应项的计数。

第四步:从FP-Tree中找出频繁项。

遍历表头项中的每一项(我们拿“牛奶:6”为例),对于各项都执行以下(1)到(5)的操作:

(1)从FP-Tree中找到所有的“牛奶”节点,向上遍历它的祖先节点,得到4条路径:

复制代码
薯片:7,鸡蛋:6,牛奶:1

薯片:7,鸡蛋:6,面包:4,牛奶:3

薯片:7,面包:1,牛奶:1

面包:1,牛奶:1
复制代码

对于每一条路径上的节点,其count都设置为牛奶的count

复制代码
薯片:1,鸡蛋:1,牛奶:1

薯片:3,鸡蛋:3,面包:3,牛奶:3

薯片:1,面包:1,牛奶:1

面包:1,牛奶:1
复制代码

因为每一项末尾都是牛奶,可以把牛奶去掉,得到条件模式基(Conditional Pattern Base,CPB),此时的后缀模式是:(牛奶)。

复制代码
薯片:1,鸡蛋:1

薯片:3,鸡蛋:3,面包:3

薯片:1,面包:1

面包:1
复制代码

(2)我们把上面的结果当作原始的事务数据库,返回到第3步,递归迭代运行。

没讲清楚,你可以参考这篇博客,直接看核心代码吧:

复制代码
public void FPGrowth(List<List<String>> transRecords,
        List<String> postPattern,Context context) throws IOException, InterruptedException {
    // 构建项头表,同时也是频繁1项集
    ArrayList<TreeNode> HeaderTable = buildHeaderTable(transRecords);
    // 构建FP-Tree
    TreeNode treeRoot = buildFPTree(transRecords, HeaderTable);
    // 如果FP-Tree为空则返回
    if (treeRoot.getChildren()==null || treeRoot.getChildren().size() == 0)
        return;
    //输出项头表的每一项+postPattern
    if(postPattern!=null){
        for (TreeNode header : HeaderTable) {
            String outStr=header.getName();
            int count=header.getCount();
            for (String ele : postPattern)
                outStr+="\t" + ele;
            context.write(new IntWritable(count), new Text(outStr));
        }
    }
    // 找到项头表的每一项的条件模式基,进入递归迭代
    for (TreeNode header : HeaderTable) {
        // 后缀模式增加一项
        List<String> newPostPattern = new LinkedList<String>();
        newPostPattern.add(header.getName());
        if (postPattern != null)
            newPostPattern.addAll(postPattern);
        // 寻找header的条件模式基CPB,放入newTransRecords中
        List<List<String>> newTransRecords = new LinkedList<List<String>>();
        TreeNode backnode = header.getNextHomonym();
        while (backnode != null) {
            int counter = backnode.getCount();
            List<String> prenodes = new ArrayList<String>();
            TreeNode parent = backnode;
            // 遍历backnode的祖先节点,放到prenodes中
            while ((parent = parent.getParent()).getName() != null) {
                prenodes.add(parent.getName());
            }
            while (counter-- > 0) {
                newTransRecords.add(prenodes);
            }
            backnode = backnode.getNextHomonym();
        }
        // 递归迭代
        FPGrowth(newTransRecords, newPostPattern,context);
    }
}
复制代码

对于FP-Tree已经是单枝的情况,就没有必要再递归调用FPGrowth了,直接输出整条路径上所有节点的各种组合+postModel就可了。例如当FP-Tree为:

我们直接输出:

3  A+postModel

3  B+postModel

3  A+B+postModel

就可以了。

如何按照上面代码里的做法,是先输出:

3  A+postModel

3  B+postModel

然后把B插入到postModel的头部,重新建立一个FP-Tree,这时Tree中只含A,于是输出

3  A+(B+postModel)

两种方法结果是一样的,但毕竟重新建立FP-Tree计算量大些。

Java实现

FP树节点定义

挖掘频繁模式

输入文件

复制代码
牛奶,鸡蛋,面包,薯片
鸡蛋,爆米花,薯片,啤酒
鸡蛋,面包,薯片
牛奶,鸡蛋,面包,爆米花,薯片,啤酒
牛奶,面包,啤酒
鸡蛋,面包,啤酒
牛奶,面包,薯片
牛奶,鸡蛋,面包,黄油,薯片
牛奶,鸡蛋,黄油,薯片
复制代码

输出

复制代码
6    薯片    鸡蛋
5    薯片    面包
5    鸡蛋    面包
4    薯片    鸡蛋    面包
5    薯片    牛奶
5    面包    牛奶
4    鸡蛋    牛奶
4    薯片    面包    牛奶
4    薯片    鸡蛋    牛奶
3    面包    鸡蛋    牛奶
3    薯片    面包    鸡蛋    牛奶
3    鸡蛋    啤酒
3    面包    啤酒
复制代码

用Hadoop来实现

在上面的代码我们把整个事务数据库放在一个List<List<String>>里面传给FPGrowth,在实际中这是不可取的,因为内存不可能容下整个事务数据库,我们可能需要从关系关系数据库中一条一条地读入来建立FP-Tree。但无论如何 FP-Tree是肯定需要放在内存中的,但内存如果容不下怎么办?另外FPGrowth仍然是非常耗时的,你想提高速度怎么办?解决办法:分而治之,并行计算。

按照论文《FP-Growth 算法MapReduce 化研究》中介绍的方法,我们来看看语料中哪些词总是经常出现,一句话作为一个事务,这句话中的词作为项。

MR_FPTree.java

import imdm.bean.TreeNode;
import ioformat.EncryptFieInputFormat;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.LineReader;
import org.wltea.analyzer.dic.Dictionary;

import text.outservice.WordSegService;

public class MR_FPTree {

    private static final int minSuport = 30; // 最小支持度

    public static class GroupMapper extends
            Mapper<LongWritable, Text, Text, Text> {

        LinkedHashMap<String, Integer> freq = new LinkedHashMap<String, Integer>(); // 频繁1项集

        org.wltea.analyzer.cfg.Configuration cfg = null;
        Dictionary ikdict = null;

        /**
         * 读取频繁1项集
         */
        @Override
        public void setup(Context context) throws IOException {
            // 初始化IK分词器
            cfg = org.wltea.analyzer.cfg.DefaultConfig.getInstance();
            ikdict = Dictionary.initial(cfg);
            // 从HDFS文件读入频繁1项集,即读取IMWordCount的输出文件,要求已经按词频降序排好
            Configuration conf = context.getConfiguration();
            FileSystem fs = FileSystem.get(conf);
            Calendar cad = Calendar.getInstance();
            cad.add(Calendar.DAY_OF_MONTH, -1); // 昨天
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
            String yes_day = sdf.format(cad.getTime());
            Path freqFile = new Path("/dsap/resultdata/content/WordCount/"
                    + yes_day + "/part-r-00000");

            FSDataInputStream fileIn = fs.open(freqFile);
            LineReader in = new LineReader(fileIn, conf);
            Text line = new Text();
            while (in.readLine(line) > 0) {
                String[] arr = line.toString().split("\\s+");
                if (arr.length == 2) {
                    int count = Integer.parseInt(arr[1]);
                    // 只读取词频大于最小支持度的
                    if (count > minSuport) {
                        String word = arr[0];
                        freq.put(word, count);
                    }
                }
            }
            in.close();

        }

        @Override
        public void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            String[] arr = value.toString().split("\\s+");
            if (arr.length == 4) {
                String content = arr[3];
                List<String> result = WordSegService.wordSeg(content);
                List<String> list = new LinkedList<String>();
                for (String ele : result) {
                    // 如果在频繁1项集中
                    if (freq.containsKey(ele)) {
                        list.add(ele.toLowerCase()); // 如果包含英文字母,则统一转换为小写
                    }
                }

                // 对事务项中的每一项按频繁1项集排序
                Collections.sort(list, new Comparator<String>() {
                    @Override
                    public int compare(String s1, String s2) {
                        return freq.get(s2) - freq.get(s1);
                    }
                });

                /**
                 * 比如对于事务(中国,人民,人民,广场),输出(中国,人民)、(中国,人民,广场)
                 */
                List<String> newlist = new ArrayList<String>();
                newlist.add(list.get(0));
                for (int i = 1; i < list.size(); i++) {
                    // 去除list中的重复项
                    if (!list.get(i).equals(list.get(i - 1))) {
                        newlist.add(list.get(i));
                    }
                }
                for (int i = 1; i < newlist.size(); i++) {
                    StringBuilder sb = new StringBuilder();
                    for (int j = 0; j <= i; j++) {
                        sb.append(newlist.get(j) + "\t");
                    }
                    context.write(new Text(newlist.get(i)),
                            new Text(sb.toString()));
                }
            }
        }
    }

    public static class FPReducer extends
            Reducer<Text, Text, Text, IntWritable> {
        public void reduce(Text key, Iterable<Text> values, Context context)
                throws IOException, InterruptedException {
            List<List<String>> trans = new LinkedList<List<String>>(); // 事务数据库
            while (values.iterator().hasNext()) {
                String[] arr = values.iterator().next().toString()
                        .split("\\s+");
                LinkedList<String> list = new LinkedList<String>();
                for (String ele : arr)
                    list.add(ele);
                trans.add(list);
            }
            List<TreeNode> leafNodes = new LinkedList<TreeNode>(); // 收集FPTree中的叶节点
            buildFPTree(trans, leafNodes);
            for (TreeNode leaf : leafNodes) {
                TreeNode tmpNode = leaf;
                List<String> associateRrule = new ArrayList<String>();
                int frequency = 0;
                while (tmpNode.getParent() != null) {
                    associateRrule.add(tmpNode.getName());
                    frequency = tmpNode.getCount();
                    tmpNode = tmpNode.getParent();
                }
                // Collections.sort(associateRrule); //从根节点到叶节点已经按F1排好序了,不需要再排序了
                StringBuilder sb = new StringBuilder();
                for (String ele : associateRrule) {
                    sb.append(ele + "|");
                }
                // 因为一句话可能包含重复的词,所以即使这些词都是从F1中取出来的,到最后其支持度也可能小于最小支持度
                if (frequency > minSuport) {
                    context.write(new Text(sb.substring(0, sb.length() - 1)
                            .toString()), new IntWritable(frequency));
                }
            }
        }

        // 构建FP-Tree
        public TreeNode buildFPTree(List<List<String>> records,
                List<TreeNode> leafNodes) {
            TreeNode root = new TreeNode(); // 创建树的根节点
            for (List<String> record : records) { // 遍历每一项事务
                // root.printChildrenName();
                insertTransToTree(root, record, leafNodes);
            }
            return root;
        }

        // 把record作为ancestor的后代插入树中
        public void insertTransToTree(TreeNode root, List<String> record,
                List<TreeNode> leafNodes) {
            if (record.size() > 0) {
                String ele = record.get(0);
                record.remove(0);
                if (root.findChild(ele) != null) {
                    root.countIncrement(1);
                    root = root.findChild(ele);
                    insertTransToTree(root, record, leafNodes);
                } else {
                    TreeNode node = new TreeNode(ele);
                    root.addChild(node);
                    node.setCount(1);
                    node.setParent(root);
                    if (record.size() == 0) {
                        leafNodes.add(node); // 把叶节点都放在一个链表中
                    }
                    insertTransToTree(node, record, leafNodes);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException,
            InterruptedException, ClassNotFoundException {
        Configuration conf = new Configuration();
        String[] argv = new GenericOptionsParser(conf, args).getRemainingArgs();
        if (argv.length < 2) {
            System.err
                    .println("Usage: MR_FPTree EcryptedChartContent AssociateRules");
            System.exit(1);
        }

        FileSystem fs = FileSystem.get(conf);
        Path inpath = new Path(argv[0]);
        Path outpath = new Path(argv[1]);
        fs.delete(outpath, true);

        Job FPTreejob = new Job(conf, "MR_FPTree");
        FPTreejob.setJarByClass(MR_FPTree.class);

        FPTreejob.setInputFormatClass(EncryptFieInputFormat.class);
        EncryptFieInputFormat.addInputPath(FPTreejob, inpath);
        FileOutputFormat.setOutputPath(FPTreejob, outpath);

        FPTreejob.setMapperClass(GroupMapper.class);
        FPTreejob.setMapOutputKeyClass(Text.class);
        FPTreejob.setMapOutputValueClass(Text.class);

        FPTreejob.setReducerClass(FPReducer.class);
        FPTreejob.setOutputKeyClass(Text.class);
        FPTreejob.setOutputKeyClass(IntWritable.class);

        FPTreejob.waitForCompletion(true);
    }
}

结束语

在实践中,关联规则挖掘可能并不像人们期望的那么有用。一方面是因为支持度置信度框架会产生过多的规则,并不是每一个规则都是有用的。另一方面大部分的关联规则并不像“啤酒与尿布”这种经典故事这么普遍。关联规则分析是需要技巧的,有时需要用更严格的统计学知识来控制规则的增殖。 

原文来自:博客园(华夏35度)http://www.cnblogs.com/zhangchaoyang作者:Orisun
分类: DM,NLP,AI
6
0
    (请您对文章做出评价)   
«上一篇: 成功誓言
»下一篇: 消除博客中的代码行号

posted on 2011-10-04 15:09 Orisun 阅读(17399) 评论(37编辑 收藏

评论

#1楼2012-02-14 14:55远航的兵

文章写得很好,谢谢分享
http://pic.cnitblog.com/face/u225301.jpg?id=02133302  

#2楼2012-04-21 00:22yangleo

写的很棒!赞
http://pic.cnitblog.com/face/u363330.jpg?id=28180540  

#3楼2012-07-10 18:55shiyuzh2007

牛人,学习!
 

#4楼2012-11-29 23:33baisong_530

// 降序排列
return arg0.getValue() - arg1.getValue();
应该是升序排列~
 

#5楼2012-12-17 16:35wzStyle

顶一个,多谢剖析
 

#6楼2012-12-17 19:43wzStyle

顶一个,多谢剖析
 

#7楼2013-01-11 16:43hwqyc

讲的很好,受教了,能不能给我完整的代码呢,多谢
 

#8楼2013-01-11 16:52hwqyc

为什么我看不了代码,每次刷新后的几秒可以看
 

#9楼2013-01-12 16:48绿树鼎

学习了。
为什么我看不了代码?能共享完整的代码吗?谢谢
 

#10楼[楼主]2013-01-14 18:27Orisun

@绿树鼎
我想你现在已经看到代码了吧
https://i-blog.csdnimg.cn/blog_migrate/a372954eaa02c45cf9e632168b7aac35.png  

#11楼[楼主]2013-01-14 18:35Orisun

@hwqyc
点击“view code"就看到了呵
https://i-blog.csdnimg.cn/blog_migrate/a372954eaa02c45cf9e632168b7aac35.png  

#12楼2013-01-22 11:45许许多多life

您好,首先谢谢,写的真的很好!就是有一点问题,楼主你的
“(1)从FP-Tree中找到所有的“牛奶”节点,向上遍历它的祖先节点,得到4条路径:”
有点问题。
应该改为“(1)从FP-Tree中找到所有的“牛奶”节点,向上遍历它的祖先节点及他的子孙节点,得到4条路径:”不然的话,“最后两项输出肯定没有“(纯属自己的看法,也不知道对不对,希望楼主海涵)。
 

#13楼2013-04-20 21:27规格严格-功夫到家

我做一个标号,留着下周学习一下。
 

#14楼2013-05-27 11:01若晨辰

引用(2)我们把上面的结果当作原始的事务数据库,返回到第3步,递归迭代运行。

我觉得这里应该是先把非频繁的项,删除掉,再递归调用FP-growth。也就是返回第一步。
http://pic.cnitblog.com/face/u389117.jpg?id=16172649  

#15楼2013-05-28 15:30爱知菜

4    鸡蛋    牛奶
4    薯片    鸡蛋    牛奶

这两个频繁项一个是开的,一个是闭的,你算法有问题,没有做好子集或超集检查
 

#16楼2013-09-09 15:21xieyijiejie

你好,很感谢你分享这篇文章,请问下import ioformat.EncryptFieInputFormat;这个是引用的哪个包,谢谢!!
 

#17楼[楼主]2013-09-11 09:45Orisun

@xieyijiejie
这个包是我自己写的,因为我的输入文件是经过加密的。你的输入文件没有加密的话,可以不用这个包。
https://i-blog.csdnimg.cn/blog_migrate/a372954eaa02c45cf9e632168b7aac35.png  

#18楼2013-09-25 15:10hulalaxiaocjiajia

写的真好
 

#19楼2013-09-26 10:44wangnan45

请问java代码挖掘频繁模式中 List<List<String>> transRecords = fptree.readTransRocords("/home/orisun/test/market");后面的路径"/home/orisun/test/market"是什么路径?需要自己建立一个吗?怎么才能成功运行?初学者,非常感谢!
 

#20楼2013-09-27 11:15muzilioo

请问text.outservice.WordSegService属于哪里的jar
 

#21楼2013-10-29 16:29cherrywq

我初学java,但是比较熟悉FP-Tree算法,写的非常好,支持!mark
了!
http://pic.cnitblog.com/face/576153/20131022162150.png  

#22楼2013-11-22 16:11Vincent.zZ

@muzilioo
同求啊,而且很急用
 

#23楼2014-02-28 11:13羽衣甘蓝

你好,首先非常感谢你的分享,有一个不懂的地方想请教一下你:import text.outservice.WordSegService是引用哪的?谢谢
 

#24楼2014-03-12 11:39笔记本本

怎么没看到输出呢?你的main函数写的null参数,该如何才能输出?
 

#25楼2014-03-19 11:41smilefacee

求问楼主wordsegservice来自何处,非常感谢!
 

#26楼2014-06-03 11:28规格严格-功夫到家

写的真好,但是有一个疑问,想问问楼主:
1. map阶段的代码,emit出来每一个key和后面模式
2. reduce阶段的话,会不会出现key对应的数组过大,导致内存溢出的可能?

另外在reduce阶段,直接生成模式了,这样的话,存到hdfs后,是不是还需要另外的代码做规则挖掘生成行为,类似单机版本的buildSet功能。
 

#27楼2014-06-03 11:29规格严格-功夫到家

总之,很感谢,从你的代码,学到了不少,自己也有思路了。
 

#28楼2014-06-17 16:11zakiel

如果我要照资料原来的顺序输出的话,像是你第一笔<6    薯片    鸡蛋>变成原来顺序<6    鸡蛋    薯片>是该修改的F1还是?
 

#29楼2014-06-27 17:33李永辉

问楼主,List<String> result = WordSegService.wordSeg(content);
这段代码是作何用途?可否赐教
 

#30楼[楼主]2014-06-28 10:24Orisun

@李永辉
分词
https://i-blog.csdnimg.cn/blog_migrate/a372954eaa02c45cf9e632168b7aac35.png  

#31楼2014-06-28 14:38zakiel

楼主想问一下关于输出他是否会照cou​​nt的顺序大小排序,我跑了一些测资发现他似乎会没照着大到小排序,有需要的话我这边也​​能站内信提供测资,希望能讨论一下
 

#32楼[楼主]2014-06-28 16:05Orisun

@zakiel
确实不能按照支持数排序输出
https://i-blog.csdnimg.cn/blog_migrate/a372954eaa02c45cf9e632168b7aac35.png  

#33楼2014-06-28 16:13zakiel

@Orisun
感谢楼主的回覆!
也就是說依照支持数排序輸出這方面可能無法再fpg實作嗎? 如果要做也只能自己對結果的getcount在做一次掃描重新排序? 還是說有更好的想法..
 

#34楼2014-06-30 12:52李永辉

最终结果就是阀值去掉一部分商品,按照一定顺序排列,排列组合出现的次数。
 

#35楼2014-06-30 12:53李永辉

在实际应用中我们需要要采用增量的方式计算吧
 

#36楼2014-12-03 21:21William_Liu

同24楼

那个 PostPattern 指是什么 ? 在main函数里面要传什么参数?
@Orisun
@李永辉
@笔记本本
 

#37楼30824832014/12/10 8:46:212014-12-10 08:46bingtel

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Collections.sort(al, new Comparator<Map.Entry<String, Integer>>() {
           @Override
           public int compare(Entry<String, Integer> arg0,
                   Entry<String, Integer> arg1) {
               // 降序排列
               return arg0.getValue() - arg1.getValue();
           }
       });

不应该是return arg1.getValue() - arg0.getValue();保证降序吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值