• 单调队列/栈从入门到精通


    单调队列/栈从入门到入土

    先把最初学到的两个板子粘到这里

    --单调队列--

    //常见模型:找出滑动窗口中的最大值/最小值
    int head=0,tail=-1;
    for(int i=0;i<n;i++){
        while(head<=tail&&check_out(q[head])) head++;//判断队头是否滑出窗口
      	//do something...
        while(head<=tail&&check(q[tail],i)) tail--;
        q[++tail]=i;
    }
    

    --单调栈--

    //常见模型:找出每个数前/后边离它最近的比它大/小的数
    int top=0;
    for(int i=1;i<=n;i++){
        while(top&&check(sta[top],i)) top--;
      	//do something...
        sta[++top]=i;
    }
    

    通过上面的注释可以看到它们都是用来干嘛的

    单调队列在应用上要比单调栈更重要,所以我们从单调队列开始说

    单调队列——基础

    找出滑动窗口中的最大值/最小值

    滑动窗口这个词从狭义上来说是一个序列中可移动的连续子串,由此引出的单调队列最基础也是最根本的一道例题:滑动窗口的最值。

    在一个序列中如果我们要求每个长度为len的连续子串中的最值,最暴力的方法就是从这个序列中依次把这些子串取出来,对每一个子串求最值,复杂度为(O(len*(n-len+1))),承受不了,但是我们发现,在这些子串中,有重叠的部分,也就是说,我们重复地扫过了一些区间,所以一定有方法避免掉这些重复,把复杂度降为(O(n)),避免掉这些重复的办法就是记录下来,并且应该记录得合理。

    如果用DP的方法记录,空间复杂度通常较差,数据结构的记录是整体记录,整体记录通常比DP记录空间复杂度优秀,但是思维含量较高,或者说数据结构的实现更加巧妙

    以最大值为例

    首先我们应该想想如何去扫一遍该序列就能够处理多个区间的答案,滑动窗口滑动窗口顾名思义,这些区间中某一区间的后缀可能是另一区间的前缀,这些区间可以通过(在前面添加一个元素并且在后面删去一个元素)或者(在前面删去一个元素并且在后面添加一个元素)完成互相之间的转化,于是,我们就可以从开头的len个数组成的区间转移到所有要处理的区间,区间的个数是(n-len+1),在实际操作时,以一个元素代表现在扫到的区间,理论上可以选择任意一个元素,但是我们常用该区间的最后一个元素代表该子串。

    e.g. 1 2 3 4 5 6 7 8 len=3

    1] 2 3 4 5 6 7 8 ……①

    1 2] 3 4 5 6 7 8 ……②

    [1 2 3] 4 5 6 7 8……③

    1 [2 3 4] 5 6 7 8 ……④

    1 2 [3 4 5] 6 7 8 ……⑤

    1 2 3 [4 5 6] 7 8 ……⑥

    1 2 3 4 [5 6 7] 8 ……⑦

    1 2 3 4 5 [6 7 8] ……⑧

    在上面举的例子中,我们把代表元素从1到n枚举了一遍,其中存在着不是我们要处理的区间,比如说①②,为什么我们也要枚举呢?接下来我们会说

    在扫过这个区间的时候,我们可以同时处理多个区间的答案,准确地来说,是在处理该区间的时候,同时考虑该区间的答案对下一个区间的答案产生的贡献,我们可以看到,对于某一个区间来说,它上一个枚举的区间的答案对这个区间答案有影响,即

    ⒈当这个区间新加进来的元素比上个区间的答案大,那么这个区间的答案就是新加进来的这个元素;

    ⒉当(这个区间新加进来的元素比上个区间的答案小)并且(上个区间的答案元素不是被删去的那个元素)时,那么这个区间的答案就是上个区间的答案;

    ⒊当(这个区间新加进来的元素比上个区间的答案小)并且(上个区间的答案元素是被删去的那个元素)时,那么这个区间的答案与上个区间的答案没有关系

    这就是为什么我们要把①②也要枚举一遍的原因——上个区间的答案对这个区间有贡献

    但是第三种情况的答案与上个区间答案没关系,需要重新枚举该区间最值,平均复杂度超出(O(n)),考虑解决办法

    这个区间的答案虽然和上个区间没关系,但是不要忘了,这个区间是可以从上一个区间通过增删元素互相转化的,也就是它们的元素除了起始和结尾元素完全相同,上个区间的答案是被删去的那个元素,那么这个区间的答案就是上一个区间的次大值与新加进来的元素取max,并且前面已经删去的答案元素对后面的答案不会再有影响,现在我们不仅要记录下区间的最值还要记录下次大值,这时DP已经显现出它的劣势。

    那考虑数据结构优化,看见我加粗的那句话了吗,删去的元素对后面的答案无影响,那我们就可以把这个元素从记录里删掉,其次我们并不需要记录次大/小值,只需要在删掉元素后维护最值就可以了,但是呢我们并不知道每一个元素它什么时候被删掉,所以每一个新进来的元素都必须加入这个记录里,以保证有答案可以取,那么我们认为比新加进来的元素小的元素就没有用了,可以删去,所以这个记录里的元素有一定的单调性,不仅体现在加入时间的单调,还体现在元素大小的单调,用一个支持单边添加,双边删除的数据结构,就是双端队列,因为有单调性,所以称单调队列。

    流程:

    从1到n枚举

    ​ 不在该区间的元素出队

    ​ 答案是队头

    ​ 队里比新加进来的元素小的元素出队

    ​ 新加进来的元素进队

    至此我们就完成了单调队列的模板题

    单调队列——进阶

    这部分我们讲讲单调队列的应用,总体上是有关DP的优化,分为单调队列优化多重背包,单调队列优化DP,斜率优化DP

    单调队列优化多重背包

    我们先了解一下多重背包问题的普通解法。

    多重背包:给出若干种有体积有价值的物品,每种物品有一定数量,求在给定的背包容量下能获得的最大价值。

    最普通的转移方程为

    [f[i][j]=max_{k=1}^{k<=min(num[i],lfloorfrac{j}{v[i]} floor)}(f[i-1][j-k*v[i]]+k*w[i]) ]

    复杂的式子看不出来什么,举个例子

    我们令(num[i]=2),写出来枚举(j)时的式子:

    [egin{cases} f[i][j]&=max(f[i-1][j],&f[i-1][j-v[i]]+w[i],&f[i-1][j-2*v[i]]+2*w[i])\ f[i][j-v[i]]&=max(f[i-1][j-v[i]],&f[i-1][j-2*v[i]]+w[i],&f[i-1][j-3*v[i]]+2*w[i])\ f[i][j-2*v[i]]&=max(f[i-1][j-2*v[i]],&f[i-1][j-3*v[i]]+w[i],&f[i-1][j-4*v[i]]+2*w[i])\ end{cases} ]

    我们发现,max中的值是有限的,前面的答案是可以影响到后面的答案且影响是单调的,非常像上面讲解的滑动窗口问题,如果要让它们更像,设(st=j\%v[i],p=lfloorfrac{j}{v[i]} floor),题目即变为

    (\,f[i-1][st]\,,\,f[i-1][st+v[i]]\,,\,f[i-1][st+2*v[i]]\,,\,...\,,\,f[i-1][st+p*v[i]]\,)这个序列中求每一个长度为2的区间中的最大值

    一共有(p)个这样的序列,挨个处理即可,省去了枚举k的时间,时间复杂度是(O(nm))的,足够优秀

    有细心的同学可能发现了,上面的三个状态转移方程中有很讨厌的常数项,元素在队列中待着的时候会变,但是我们仔细一看,这些变化并不会引起队列的单调性的变化,我们只要在每一次做完出队的操作后把队里每个元素加上(w[i])就行了,这样一来刚入队的元素是不带有(w[i])的,而队列里的每个数随着其入队时间的增加(w[i])的系数是递增的

    单调队列优化DP

    这事*Miracle*最近给我们讲的,复习一下

    *Miracle*的ppt上写了这么几句话:

    需要推出DP式子

    根据决策和备选区间来优化

    使得可能贡献给当前及之后的决策点之间有单调性

    入门题:滑动窗口

    这里神仙之所以把滑动窗口归入单调队列优化DP中是因为我们一开始在考虑解法时也是从DP的思想开始的,还记得上面我列出的几种转移情况吗,只不过我们发现DP不可做,所以应用了一种巧妙的数据结构来全局维护了一个答案序列而已。

    其实这就没啥好讲的了,来看一道例题把awa

    P2569[SCOI2010]股票交易

    题意:你初始时有好多好多钱,但是每天持有的股票不超过(Maxp)。有(T)天,你知道每一天的买入价格(AP[i]),卖出价格((BP[i])), 买入数量限制((AS[i])),卖出数量限制((BS[i]))。 并且两次交易之间必须间隔(w)天。 现在问你(T)天结束后,最大收益是多少。

    (f[i][j])为在前(i)天手里剩下(j)股时,可以列出最简单的DP式子来:

    [egin{cases} f[i][j]=max(f[i-w-1][k]+(k-j)*BP[i])&(jleq kleq j+BS[i]);①\ f[i][j]=max(f[i-w-1][k]-(j-k)*AP[i])&(j-AS[i]leq kleq j);②\ end{cases} ]

    ①为卖出,②为买入

    发现枚举的(k)是有范围的,前面的答案对后面有影响且影响是单调的,还是转换一下题目

    (g[x]=f[i-w-1][x],AS[i]=3,BS[i]=2)

    [egin{cases} f[i][j]&=max(g[j];,&;g[j+1]+BP[i];,&;g[j+2]+2*BP[i])\ f[i][j]&=max(g[j];,&;g[j-1]-AP[i];,&;g[j-2]-2*AP[i];,&;g[i-3]-3*AP[i])\\ f[i][j+1]&=max(g[j+1];,&;g[j+2]+BP[i];,&;g[j+3]+2*BP[i])\ f[i][j+1]&=max(g[j+1];,&;g[j]-AP[i];,&;g[j-1]-2*AP[i];,;&g[i-2]-3*AP[i])\\ f[i][j+2]&=max(g[j+2];,&;g[j+3]+BP[i];,&;g[j+4]+2*BP[i])\ f[i][j+2]&=max(g[j+2];,&;g[j+1]-AP[i];,&;g[j]-2*AP[i];,&;g[i-1]-3*AP[i])\ end{cases} ]

    求在(g[1],g[2],g[3],...,g[Maxp])这个序列中所有长度为(AS[i])(BS[i])的区间的最大值

    共有(T-w-1)个序列,可以想想为什么

    需要维护两个单调队列,常数项的操作与多重背包优化一样

    还有一道综合性比较强的题:[IOI2008]Island (还没写,看到的请小窗敲我,不然我会咕的qaq)

    斜率优化DP

    *Miracle*也讲力

    在这部分单调队列的应用主要体现在求凸壳的过程,我们慢慢来

    凸包:一个凸多边形(封闭图形)

    凸壳:一个凸多边形的一部分(不封闭)

    凸壳

    这就是凸壳(确信

    这跟单调队列有什么关系呢?

    我们有顺序地看每条直线,它们的斜率是有单调性的

    来一张色彩鲜艳的图

    凸包的单调性

    以水平直线(红线)为x轴,从左往右看直线,在x轴上半部分(黄色凸壳),直线斜率单调递减,称为上凸壳,在x轴下半部分(蓝色凸壳),直线斜率单调递增,称为下凸壳,如果第一条黄线标号为①,然后顺时针把直线依次标到⑩,那么①到⑤的斜率是单调递减的,⑥到⑩的斜率也是单调递减的

    引入我们的斜率优化DP,给出一个模型:

    已知(y[j]=k[i]*x[j]+ans[i]+b[i]),求(ans[i])的最小值 ((k[i])单调递增)

    (P_j(x[j],y[j]))中的部分点形成一个凸包,有些点会落在凸包内部

    题目要求求(ans[i])的最小值,即要求一条斜率为(k[i])的,过点(P_j(x[j],y[j]))的直线的最小截距减去(b[i]),易知,将一条斜率为(k[i])的直线从下往上移,第一次遇见点P,当前直线就是截距最小的直线,看图

    image-20210601193130482

    这可以和凸包联系起来,因为凸包上的点有单调性,我们又发现红线与和它相交的两条直线①②也有关系,①的斜率比红线斜率大,②的斜率比红线斜率小,所以我们维护一个下凸壳,找到凸壳中第一个大于红线斜率的直线就可以了,而又因为每条红线的斜率是单调递增的,那么凸壳中斜率小于当前红线的直线就可以扔掉了

    注:若ans前的系数为负的或让求最大值,应改为维护上凸壳,具体情况具体分析

    比较麻烦的一点是凸壳内部的点,它会让求出来的直线斜率变大,也就是说我们要尽可能地让凸壳上面的直线斜率更小一些,使所有的点要么在凸包内要么在凸包上,若一个凸壳上的点连到了多条直线那么斜率最小的一条必在凸壳上,要维护这样一个凸壳可以用单调队列

    这时单调队列里的元素不再是数而是点,每个点和下一个点确定了一条直线且斜率单调递增,我们要保证每条直线都在凸壳上:

    考虑前i个点

    1. 凸壳中小于第i条直线斜率的直线都出队(不要的扔掉)

    2. 转移DP式子

    3. 凸包上斜率大于新进来的点与队尾点确定的直线斜率的直线都出队(保证凸包上面的直线斜率尽可能小)

    4. 新点入队

    斜率优化DP的板子题:[HNOI2008]玩具装箱

    单调栈——基础

    单调栈要解决的问题是找出每个数前/后边离它最近的比它大/小的数

    我们可以从每个数开始找,但是太慢了

    以找前面最近的比它大的数为例,我们发现,对于一个数,

    ⒈如果它前面的数比它大,那么答案是它前面的数

    ⒉如果它前面的数比它小且它前面数的答案比它大,那么答案就是它前面的数的答案

    ⒊如果它前面的数比它小且它前面数的答案比它小,那么答案与它前面的数的答案无关

    还是考虑数据结构优化,举个例子

    e.g. 7 2 1 4 6 3 5

    线性扫一遍序列,发现如果后面一个数比前面一个数大,那么前面那个数就可以扔掉了,因为后面那个数比前面那个数更优,那么我们每枚举到一个数,挨个扔掉前面比这个数小的数,刚好没有被扔掉的那个数就是答案,因为每次扔掉了小的数,所以这个序列是单调的,而且先进先出,用栈来实现,叫做单调栈

    流程:

    从1到n枚举

    ​ 比这个数小的数弹出

    ​ 答案是栈顶

    ​ 新元素入栈

    单调栈——进阶

    与单调栈有关系的知识点还有笛卡尔树和虚树,单调栈还可以用来离线求RMQ以及求LIS

    离线RMQ

    利用单调栈里的元素都是单调的这一性质可以用来做RMQ

    RMQ:给出一个序列,求若干区间最值

    处理询问的时候我们先按区间右端点排序,这样遍历一遍就能处理所有询问,在遍历的时候跑单调栈,如果遇到询问就在栈中查询区间左端点,栈中第一个下标大于等于左端点的元素就是答案

    码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<string>
    #include<cmath>
    #include<vector>
    #include<map>
    #include<queue>
    #include<deque>
    #include<set>
    #include<stack>
    #include<bitset>
    #include<cstring>
    #define ll long long
    #define max(a,b) ((a>b)?a:b)
    #define min(a,b) ((a<b)?a:b)
    using namespace std;
    const int INF=0x3f3f3f3f,N=100010;
    
    inline int read(){
        int x=0,y=1;char c=getchar();
        while (c<'0'||c>'9') {if (c=='-') y=-1;c=getchar();}
        while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
        return x*y;
    }
    
    struct query{
        int l,r,id,ans;
    }que[N];
    
    bool cmp(query a,query b){return a.r<b.r;}
    
    int a[N],n,q,sta[N],top=0,ans[N];
    
    int main(){
    
        n=read();q=read();
        for(int i=1;i<=n;i++) a[i]=read();
        for(int i=1;i<=q;i++) que[i].l=read(),que[i].r=read(),que[i].id=i;
        sort(que+1,que+1+q,cmp);
    
        int k=1;
        for(int i=1;i<=n;i++){
            while(top&&a[sta[top]]<a[i]) top--;
            sta[++top]=i;//下标存入栈中,方便查找左端点
            while(que[k].r==i){
                int res=que[k].l;
                int l=1,r=top;
                while(l<r){//二分查左端点
                    int mid=(l+r)>>1;
                    if(sta[mid]>=res) r=mid;
                    else l=mid+1;
                }
                que[k].ans=a[sta[l]];
                k++;
            }
        }
    
        for(int i=1;i<=q;i++) ans[que[i].id]=que[i].ans;
        for(int i=1;i<=q;i++) printf("%d ",ans[i]);
    
        return 0;
    }
    

    LIS

    LIS:最长上升子序列,给定一个序列,求其子序列中满足单调上升的子序列的最大长度

    这里对序列跑单调栈的时候要有一些改变,维护一个从栈底至栈顶单调递增的栈,我们在遇到小于栈顶元素的值时,操作是让这个数替换掉栈中第一个大于等于该数的元素,那么对于(sta[i])来说,它就有一个意义了:长度为i的上升子序列中末尾的数最小是多少,我们去替换也就是使该长度的上升子序列末尾的数更小,结果更优,这是贪心的思想

    码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<string>
    #include<cmath>
    #include<vector>
    #include<map>
    #include<queue>
    #include<deque>
    #include<set>
    #include<stack>
    #include<bitset>
    #include<cstring>
    #define ll long long
    #define max(a,b) ((a>b)?a:b)
    #define min(a,b) ((a<b)?a:b)
    using namespace std;
    const int INF=0x3f3f3f3f,N=100010;
    
    inline int read(){
        int x=0,y=1;char c=getchar();
        while (c<'0'||c>'9') {if (c=='-') y=-1;c=getchar();}
        while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
        return x*y;
    }
    
    int a[N],n,sta[N],top=0;
    
    int main(){
    
        n=read();
        for(int i=1;i<=n;i++) a[i]=read();
    
        for(int i=1;i<=n;i++){
            if(a[i]>sta[top]) sta[++top]=a[i];
            else{
                int pos=lower_bound(sta+1,sta+1+top,a[i])-sta;
                sta[pos]=a[i];
            } 
        }
    
        printf("%d
    ",top);
        
        return 0;
    }
    

    显然,单调队列也可以用来求LIS

    笛卡尔树

    笛卡尔树类似于Treap

    每个节点有两个参数(pos)(val),一般认为(pos)是下标,(val)是点权,(pos)满足二叉搜索树的性质,(val)满足堆的性质,

    考虑怎么建这棵树才能达到最优复杂度

    以小根堆为例,我们发现在最右边一条链上,它的(pos)(val)值都是单调递增的,我们可以先维护一个满足(pos)的最右链,然后把链上不满足(val)的元素向上跳,记作A,上面的元素下标都是小于A点下标的,所以它在跳到某个点时,记作B,可以直接继承B点并且把A点左儿子设成B点以满足二叉搜索树的性质,维护用单调栈

    码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<string>
    #include<cmath>
    #include<vector>
    #include<map>
    #include<queue>
    #include<deque>
    #include<set>
    #include<stack>
    #include<bitset>
    #include<cstring>
    #define ll long long
    #define max(a,b) ((a>b)?a:b)
    #define min(a,b) ((a<b)?a:b)
    using namespace std;
    const int INF=0x3f3f3f3f,N=100010;
    
    int n,a[N],rt,ls[N],rs[N],sta[N],top=0;
    
    void build(){//维护最右链
        for(int i=1;i<=n;i++){
            int pos;
            while(top&&a[sta[top]]>a[i]){//往上爬
                pos=sta[top];//爬到哪了
                top--;
            }
            if(!top) rt=i;//爬到顶了
            else rs[sta[top]]=i;//没爬到顶,继承原来的节点
            ls[i]=pos;//更新左儿子
            sta[++top]=i;
        }
    }
    
    int main(){
    
        scanf("%d",&n);
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        
        build();
    
        return 0;
    }
    

    来道例题:

    截屏2021-06-03 下午8.18.24

    解:二分答案+笛卡尔树验证

    对于某一段区间相似,它们的笛卡尔树同构,实现时可以不用把树建出来,我们可以(O(n))扫一遍序列,每次判断栈中元素个数是否相等就行了

    还有一道:Beautiful Pair

    虚树

    给你一棵树,和一些关键点,其他的节点没有多大用处时,在树上做一些事情就很麻烦,我们可以只保留关键点和它们之间的lca,形成一棵新的树,这样的树就叫虚树

    如果n是关键点个数的话,虚树的空间复杂度是(O(n))的,因为(n)个点之间的lca最多有(n-1)个,所以虚树上的节点最多有(2n-1)

    将关键点及其lca也就是虚树上所有的点求出来,按照原树的dfs序进行排序,保证虚树与原树的形态一样,在加边的时候,我们有两种情况,若该点在栈顶子树里,那么入栈,若该点不在栈顶子树里,那么弹掉栈顶,直到该点在栈顶元素的子树里,该点入栈,判断在不在子树里可以用bitset

    码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<string>
    #include<cmath>
    #include<vector>
    #include<map>
    #include<queue>
    #include<deque>
    #include<set>
    #include<stack>
    #include<bitset>
    #include<cstring>
    #define ll long long
    #define max(a,b) ((a>b)?a:b)
    #define min(a,b) ((a<b)?a:b)
    using namespace std;
    const int INF=0x3f3f3f3f,N=100010;
    
    int e[N],ne[N],h[N],idx;
    int ve[N],vh[N],vne[N],vidx;
    int key[N],id[N],idd=0;
    int sta[N],top=0;
    int n,m;
    vector<int> p;
    bitset<N> son[N];
    
    bool cmp(int a,int b){
        return id[a]<id[b];
    }
    
    inline int read(){
        int x=0,y=1;char c=getchar();
        while (c<'0'||c>'9') {if (c=='-') y=-1;c=getchar();}
        while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
        return x*y;
    }
    
    void add(int a,int b){
        e[idx]=b,ne[idx]=h[a],h[a]=idx++;
    }
    
    void vadd(int a,int b){
        ve[vidx]=b,vne[vidx]=vh[a],vh[a]=vidx++;
    }
    
    void dfsid(int x,int fa){//处理dfs序和子树bitset
        id[x]=++idd;
        son[x][x]=1;
        for(int i=h[x];~i;i=ne[i]){
            int j=e[i];
            if(j==fa) continue;
            dfsid(j,x);
            son[x]|=son[j];
        }
    }
    
    namespace lca{
        int d[N],f[N][21];
        void dfs(int x,int fa){
            d[x]=d[fa]+1;
            f[x][0]=fa;
            for(int i=1;i<=19;i++) f[x][i]=f[f[x][i-1]][i-1];
            for(int i=h[x];~i;i=ne[i]) if(e[i]!=fa) dfs(e[i],x);
        }
        int lca(int x,int y){
            if(x==y) return x;
            if(d[x]<d[y]) swap(x,y);
            for(int i=19;i>=0;i--){
                if(d[f[x][i]]>=d[y]) x=f[x][i];
            }
            if(x==y) return x;
            for(int i=20;i>=0;i--){
                if(f[x][i]!=f[y][i]){
                    x=f[x][i];
                    y=f[y][i];
                }
            }
            return f[x][0];
        }
    }
    
    int main(){
    
        memset(h,-1,sizeof h);
        memset(vh,-1,sizeof vh);
    
        n=read(),m=read();
        for(int i=1,a,b;i<n;i++) a=read(),b=read(),add(a,b),add(b,a);
        dfsid(1,0);
        for(int i=1;i<=m;i++) key[i]=read(),p.push_back(key[i]);
        sort(key+1,key+1+m,cmp);
        lca::dfs(1,0);
        for(int i=1,pos;i<m;i++){
            pos=lca::lca(key[i],key[i+1]);
            p.push_back(pos);
        }
        sort(p.begin(),p.end(),cmp);
        p.erase(unique(p.begin(),p.end()),p.end());//去重
        for(int i=0;i<p.size();i++){
            while(top&&!son[sta[top]][p[i]]) top--;
            if(top) vadd(sta[top],p[i]),vadd(p[i],sta[top]);
            sta[++top]=p[i];
        }
    
        return 0;
    }
    
  • 相关阅读:
    Python:软科中国大学排名爬虫(2021.11.5)
    服务外包系统软件需求分析+原型
    JFinal极速开发框架
    Mapreduce实例——MapReduce自定义输入格式
    利用jieba分析词语频数
    Mapreduce实例——Reduce端join
    Mapreduce实例——Map端join
    Mapreduce实例——MapReduce自定义输出格式
    打卡
    Mongo数据库实验
  • 原文地址:https://www.cnblogs.com/wsyunine/p/14851199.html
Copyright © 2020-2023  润新知