本文参考算法导论完成。

模板题在此 QwQ

优化的过程比较长,还请读者耐心阅读,认真理解。

最初的想法

我会暴力!

用一个 \(size\) 数组维护每个元素出现的次数

不细讲,时间复杂度 \(O(um)\)。

树形结构

不难发现,刚刚的算法的本质是将值域划分成了值域大小块

将 \(0\sim u-1\) 看做一个结点,它有 \(u\) 个儿子,第 \(i\) 个儿子所代表的值域为 \(i-1\sim i-1\)。

给每个结点编号,稍稍改变一下 \(size\) 数组的定义。此时 \(size_i\) 表示第 \(i\) 个结点所代表的值域中每个值出现的次数之和。

可以发现,这是一棵两层的树,除叶子结点外,每个结点都有 \(u\) 个儿子。

而这个算法的瓶颈就在于找出一个结点第一棵、最后一棵有值的子树,一棵子树之前、之后第一棵有值的子树。

那是否可以将结点的儿子数量减小,从而得到更优的复杂度呢?答案是肯定的。

如果儿子变成 \(2\) 个,那这就是一棵权值线段树,瓶颈变成了树的深度,时间复杂度 \(O(m\log u)\)。如果儿子变成 \(\lceil\sqrt u\rceil\) 个,瓶颈依旧在于儿子数量,时间复杂度 \(O(m\sqrt u)\)。

前者会更加优秀,但正是因为后者,才有了之后的改进。

递归结构

总结一下前一节的内容,树形结构遇到了两方面的瓶颈,即树的深度和儿子数量。而我们很难在树的深度上获得什么突破,那就只好限制树的深度了。

考虑采用递归结构,对于每个结点,我们都将它的值域分成值域大小的平方根块,然后递归下去,直到值域大小小于等于 \(2\)。

根据开方的性质,树的深度被限制在了 \(\log\log u\)。

解决完深度后,如何突破儿子数量的瓶颈呢?

考虑对于每一个非叶结点,除了它的儿子,再新建一棵 \(summary\) 子树,结构与其它子树相同,值域大小为其它子树的个数,维护其它子树内是否有值

注意,这同样是递归结构,\(summary\) 子树内依旧可能会有 \(summary\) 子树。

所以求一个结点第一棵、最后一棵有值的子树,一棵子树之前、之后第一棵有值的子树就转化成了求 \(summary\) 子树内同样的问题,而 \(summary\) 子树的值域大小在原树的基础上有很大的缩减。

于是原型 van Emde Boas 结构就出现了,具体内容会在下一节中介绍。

构建原型 van Emde Boas 结构

令 \(p=\lceil\sqrt u\rceil\)。

定义值域 \(0\sim u-1\) 为一个原型 van Emde Boas 结构或 proto-vEB 结构,记做 \(proto-vEB(u)\),称一个结构为一个

  • \(u=2\),\(proto-vEB(u)\) 是叶子结点,包含一个大小为 \(2\) 的数组 \(a_{0\sim 1}\),表示值 \(0\) 和 \(1\) 的出现次数。

  • \(u>2\),\(proto-vEB(u)\) 包含一个大小为 \(\lceil\frac{u}{p}\rceil\) 的数组 \(cluster_{0\sim\lceil\frac{u}{p}\rceil-1}\) 表示 \(proto-vEB(u)\) 的 \(\lceil\frac{u}{p}\rceil\) 个子簇的编号,前 \(\lfloor\frac{u}{p}\rfloor\) 个子簇都是一个 \(proto-vEB(p)\),如果最后多出来的一个子簇存在那么它的大小为 \(\max(u\bmod p,2)\)。

    另外,\(proto-vEB(u)\) 还包含一个大小为 \(\lceil\frac{u}{p}\rceil\) 的子簇 \(summary\),\(summary\) 也是一个 \(proto-vEB\) 结构。

    为了方便快捷地支持删除值后 \(summary\) 的更新问题(不能直接删,因为对应簇中可能有多个值),\(proto-vEB(u)\) 还有一个 \(size\) 表示值域中每个值出现的次数之和。

但这样会出现一个新问题:如何得知当前值所在的位置?或者说,如何得知某个位置所代表的值?

首先注意一点:值都是相对于当前簇的,要知道真实的意义必须向上运算至最近的 \(summary\) 的顶部(此时是对上一层值的一个统计)或整个簇(全域)的顶部(此时是真实元素)。

考虑这个运算。第 \(x(0\leq x<\lceil\frac{u}{p}\rceil)\) 个子簇中值 \(y\) 相对于当前层的值是 \(xp+y\)。

反过来也一样,当前层的值 \(x\) 应该在第 \(\lfloor\frac{x}{p}\rfloor\) 个子簇中,相对于这个子簇的值为 \(x\bmod p\)。

理清楚这些概念后,就可以来康代码了。

proto-vEB 结构:

struct proto_vEB
{
vector<int>clu;
int a[2],u,sqr,sum,siz;
}t[2001005];

因为 \(cluster\) 的大小不确定所以我们用了个 vector 来实现。

建树:

void create(proto_vEB&v,int u)
{
v.u=Max(u,2);
if(u>2)
{
int i;
v.sqr=int(sqrt(Db(u)));
if(v.sqr*v.sqr<u)v.sqr++;
For(i,1,u/v.sqr)v.clu.push_back(++cnt),create(t[cnt],v.sqr);
if(u%v.sqr)v.clu.push_back(++cnt),create(t[cnt],u%v.sqr);
v.sum=++cnt;
create(t[cnt],u/v.sqr+(u%v.sqr>0));
}
}

用到的结点数量是 \(S(u)=S(\lceil\sqrt u\rceil)\times\lfloor\frac{u}{\lceil\sqrt u\rceil}\rfloor+S(u\bmod\lceil\sqrt u\rceil))+S(\lceil\frac{u}{\lceil\sqrt u\rceil}\rceil)+1\),这东西还不是单调的。有没有鸽鸽能来指教一下 \(1\sim u\) 的上界大概是多少啊/kel

原型 van Emde Boas 结构上的操作

有点复杂,直接对着代码讲解。

为了方便,我们首先用三个函数表示上文提到过的运算。

inline int high(proto_vEB&v,int x)
{
return x/v.sqr;
}
inline int low(proto_vEB&v,int x)
{
return x%v.sqr;
}
inline int index(proto_vEB&v,int x,int y)
{
return x*v.sqr+y;
}
查询某个值出现的次数
int member(proto_vEB&v,int x)
{
if(v.u==2)return v.a[x];
return member(t[v.clu[high(v,x)]],low(v,x));
}

第三行处理叶子结点的情况。

第四行在 \(x\) 所在的子簇内继续查找。

查找簇内最小值
int minimum(proto_vEB&v)
{
if(v.u==2)return(v.a[0]?0:(v.a[1]?1:-1));
int mn=minimum(t[v.sum]);
return(!~mn?-1:index(v,mn,minimum(t[v.clu[mn]])));
}

第三行处理叶子结点的情况。

第四行查找第一个有值的子簇。

第五行在该子簇内继续查找。

查找簇内最大值
int maximum(proto_vEB&v)
{
if(v.u==2)return(v.a[1]?1:(v.a[0]?0:-1));
int mx=maximum(t[v.sum]);
return(!~mx?-1:index(v,mx,maximum(t[v.clu[mx]])));
}

与最小值对称。

查找簇内某个值的前驱
int predecessor(proto_vEB&v,int x)
{
if(v.u==2)return(x&&v.a[0]?0:-1);
int rem=predecessor(t[v.clu[high(v,x)]],low(v,x));
if(~rem)return index(v,high(v,x),rem);
rem=predecessor(t[v.sum],high(v,x));
return(!~rem?-1:index(v,rem,maximum(t[v.clu[rem]])));
}

第三行处理叶子结点的情况。

第四行在 \(x\) 所在的子簇内查找前驱。

第五行判断是否找到,找到了就直接返回。

第六行在前面的子簇中查找最近的有值的子簇。

第七行在该子簇内查找最大值。

查找簇内某个值的后继
int successor(proto_vEB&v,int x)
{
if(v.u==2)return(!x&&v.a[1]?1:-1);
int rem=successor(t[v.clu[high(v,x)]],low(v,x));
if(~rem)return index(v,high(v,x),rem);
rem=successor(t[v.sum],high(v,x));
return(!~rem?-1:index(v,rem,minimum(t[v.clu[rem]])));
}

与前驱对称。

插入一个值
int insert(proto_vEB&v,int x,bool rem)
{
if(v.u==2)if(rem)
{
v.a[x]++;
return 1;
}
else
{
if(v.a[x])return 0;
v.a[x]=1;
return 1;
}
else
{
int p=insert(t[v.clu[high(v,x)]],low(v,x),rem),q=insert(t[v.sum],high(v,x),0);
t[v.clu[high(v,x)]].siz+=p;
t[v.sum].siz+=q;
return p+q;
}
}

该函数的返回值表示该子簇内新插入的值的数量。

第三至十三行处理叶子结点的情况。

\(rem\) 表示当前簇是否处于某个 \(summary\) 内。

如果没有,说明此处存储的是某个元素出现的次数,可以大于 \(1\)。

否则仅仅需要判断是否出现,不可以大于 \(1\)。

第十六行往 \(x\) 所在的子簇和 \(summary\) 内插入对应的值,同时统计新插入的值的数量。

第十七和十八行更新 \(size\)。

第十九行返回当前簇新插入的值的数量。

删除一个值
int Delete(proto_vEB&v,int x)
{
if(v.u==2)if(v.a[x])
{
v.a[x]--;
return 1;
}
else return 0;
else
{
int p=Delete(t[v.clu[high(v,x)]],low(v,x)),q=0;
t[v.clu[high(v,x)]].siz-=p;
if(!t[v.clu[high(v,x)]].siz)q=Delete(t[v.sum],high(v,x));
t[v.sum].siz-=q;
return p+q;
}
}

该函数的返回值表示该子簇内新删除的值的数量。

第三至八行处理叶子结点的情况。

如果 \(a_x>0\) 说明可以删除就删除返回 \(1\),否则返回 \(0\)。

第十一行去 \(x\) 所在的子簇中删除对应的值,同时统计新删除的值的数量。

第十二行更新 \(x\) 所在的子簇的 \(size\)。

第十三行判断 \(x\) 所在的子簇是否为空,为空就在 \(summary\) 中删除对应的值,同时统计新删除的值的数量。

第十四行更新 \(summary\) 的 \(size\)。

第十五行返回当前簇新删除的值的数量。

时间复杂度分析

member:\(T(u)=T(\sqrt u)+O(1)\),花费 \(O(\log\log u)\)。

minimum maximum insert Delete:\(T(u)=2T(\sqrt u)+O(1)\),花费 \(O(\log u)\)。

predecessor successor:\(T(u)=2T(\sqrt u)+O(\log u)\),花费 \(O(\log u\log\log u)\)。

还需要继续改进。

van Emde Boas 结构成型

发现 proto-vEB 不够优秀的原因是因为有些操作我们需要多次递归,我们想要尽可能减少递归。

考虑多记录两个量,簇内的最小值 \(mn\) 和最大值 \(mx\)。

需要注意的是,\(mn\) 不出现在簇中,而 \(mx\) 出现在簇中,即该簇中(不包含该簇)不会有任何一个 \(mn\) 或 \(mx\) 等于该簇的 \(mn\),该簇中某个簇(包含该簇)的 \(mn\) 一定会等于该簇的 \(mx\)。

簇的 \(mn\) 与 \(mx\) 不包含 \(summary\) 中的值。

这样做是有原因的:

  1. minimummaximum 甚至不需要递归,可以直接用。

  2. predecessorsuccessor 可以直接判断前驱和后继是否在 \(x\) 所在的子簇中,不需要递归。

  3. 可以 \(O(1)\) 判断一个簇的状态。如果 \(mn\) 和 \(mx\) 都不存在,那么簇为空;如果 \(mn=mx\),那么簇仅包含一个值;如果 \(mn\) 和 \(mx\) 不相等,那么簇包含至少两个值。

  4. 可以 \(O(1)\) 将一个值插入一个空簇中。

van Emde Boas 结构上的操作

细节很多,依旧来看每个操作。

查询某个值出现的次数
int member(vEB&v,int x)
{
return(x==v.mn?v.a[0]:(x==v.mx?v.a[1]:(v.u==2?0:member(t[v.clu[high(v,x)]],low(v,x)))));
}

第三行先判断是否是簇内值的个数小于等于 \(2\) 或叶子结点的情况,是就返回,不是就在 \(x\) 所在的子簇内继续查找。

查找簇内某个值的后继
int successor(vEB&v,int x)
{
if(v.u==2)return(!x&&v.mx==1?1:-1);
if(x<v.mn)return v.mn;
int rem=t[v.clu[high(v,x)]].mx;
if(~rem&&low(v,x)<rem)return index(v,high(v,x),successor(t[v.clu[high(v,x)]],low(v,x)));
rem=successor(t[v.sum],high(v,x));
return(!~rem?-1:index(v,rem,t[v.clu[rem]].mn));
}

第三行处理叶子结点的情况。

第四行处理后继是 \(mn\) 的情况。

第六行判断后继是否在 \(x\) 所在的子簇内,是就在 \(x\) 所在的子簇内继续查找后继。

第七行在之后的子簇内查找最近有值的子簇。

第八行返回该子簇的 \(mn\)。

查找簇内某个值的前驱
int predecessor(vEB&v,int x)
{
if(v.u==2)return(x&&!v.mn?0:-1);
if(~v.mx&&x>v.mx)return v.mx;
int rem=t[v.clu[high(v,x)]].mn;
if(~rem&&low(v,x)>rem)return index(v,high(v,x),predecessor(t[v.clu[high(v,x)]],low(v,x)));
rem=predecessor(t[v.sum],high(v,x));
return(!~rem?(v.mn<x?v.mn:-1):index(v,rem,t[v.clu[rem]].mx));
}

同后继,但要注意一种情况,即之前的子簇中都无值。

此时 \(x\) 的前驱可能是 \(mn\)(因为 \(mn\) 不在簇中嘛),在第八行中多判断了一下。

插入一个值
void insert(vEB&v,int x,int y,bool bo)
{
if(!bo)y=1;
if(!~v.mn)
{
v.mn=v.mx=x;
v.a[0]=v.a[1]=y;
return;
}
if(x==v.mn)
{
if(bo)
{
v.a[0]+=y;
if(v.mn==v.mx)v.a[1]+=y;
}
return;
}
if(x<v.mn)
{
if(v.mn==v.mx&&bo)v.a[1]=0;
swap(x,v.mn),swap(v.a[0],y);
}
if(v.u>2)
{
int rem=v.clu[high(v,x)];
if(!~t[rem].mn)insert(t[v.sum],high(v,x),y,0);
insert(t[rem],low(v,x),y,bo);
}
if(x>v.mx)v.mx=x,v.a[1]=y;
else if(x==v.mx&&bo)v.a[1]+=y;
}

既然都能直接判断簇的状态了,那还要 \(size\) 何用?但是代码更长了(悲

我们依旧记录当前簇是否处于某个 \(summary\) 内,用 \(bo\) 表示。另外,\(y\) 表示插入值的个数。

第三行因为 \(summary\) 中值出现次数只能为一次,所以处理一下。

第四至九行处理簇内无值的情况。注意 \(mn\) 和 \(mx\) 的出现次数都要变成 \(y\)!

第十至十八行处理 \(x=mn\) 的情况。

第十二行判断是否在 \(summary\) 内,只有不在 \(summary\) 内才能增加出现次数(原先出现次数至少为 \(1\))。

第十四行更新 \(mn\) 的出现次数。

第十五行注意当 \(mn=mx\) 时同时更新 \(mx\) 的出现次数!

第十九至二十三行处理 \(x<mn\) 的情况。

第二十一行插个眼。

第二十二行更新 \(mn\)。注意是交换,因为如果 \(mn\) 被更新了,原先的 \(mn\) 依旧还存在,要传下去。且传下去必定能插入,因为相同的值没有被传下去过。

第二十四行判断是否是叶子结点,不是才能继续插下去。

第二十七行在 \(x\) 所在子簇为空的情况下更新 \(summary\)。

第二十八行去 \(x\) 所在的子簇内插。

第三十行更新 \(mx\)。

第三十一行更新 \(mx\) 的出现次数,此处拔起第二十一行的眼。我们发现执行第二十一行时互换了 \(x\) 和 \(mn\),如果原先 \(mn=mx\),那第三十一行实际出现次数没有变化的 \(mx\)(即原 \(mn\))的出现次数会重复叠加。所以要在第二十一行特判这种情况,提前将 \(mx\) 的出现次数清空,这样就不会受到影响了。

删除一个值
void Delete(vEB&v,int x,bool bo)
{
if(v.mn==v.mx)
{
if(!~v.mn)return;
if(x!=v.mn)return;
if(bo||v.a[0]==1)v.mn=v.mx=-1;
else v.a[0]--,v.a[1]--;
return;
}
if(v.u==2)
{
if(!x)if(bo||v.a[0]==1)v.mn=1,v.a[0]=v.a[1];
else v.a[0]--;
else if(bo||v.a[1]==1)v.mx=v.mn,v.a[1]=v.a[0];
else v.a[1]--;
return;
}
int rem;
if(x==v.mn)
{
if(!bo&&v.a[0]>1)
{
v.a[0]--;
return;
}
rem=t[v.sum].mn;
x=index(v,rem,t[v.clu[rem]].mn);
v.mn=x;
v.a[0]=t[v.clu[rem]].a[0];
bo=1;
}
rem=v.clu[high(v,x)];
Delete(t[rem],low(v,x),bo);
if(!~t[rem].mn)
{
Delete(t[v.sum],high(v,x),bo);
if(x==v.mx)
{
if(!bo&&v.a[1]>1)
{
v.a[1]--;
return;
}
rem=t[v.sum].mx;
if(!~rem)v.mx=v.mn,v.a[1]=v.a[0];
else v.mx=index(v,rem,t[v.clu[rem]].mx),v.a[1]=t[v.clu[rem]].a[1];
}
}
else if(v.mx==x)
{
if(!bo&&v.a[1]>1)
{
v.a[1]--;
return;
}
rem=v.clu[high(v,x)];
v.mx=index(v,high(v,x),t[rem].mx);
v.a[1]=t[rem].a[1];
}
}

真就长那么亿点点(

实在讲不动了,就自己理解吧,能有耐心看到这我相信各位都能自己写出来了。我只把坑点说一下。

首先,如果删除后更新了 \(mn\),那么要将簇中存在的最小值变成 \(mn\),然后删除它。此时要删完,即该值不管出现几次都清空,因为次数也跟着变成 \(mn\) 的出现次数了。所以我们用一个 \(bo\) 表示是否要删完。

第七行要同时清空 \(mn\) 和 \(mx\)。

第八行要同时更新 \(mn\) 和 \(mx\) 的次数。

第十三和十五行更新 \(mn\) 或 \(mx\) 时记得同时更新出现次数。

第三十一行在更新完 \(mn\) 后记得将 \(bo\) 置为 \(1\)。

第三十八行记得更新 \(mx\),此处的情况是 \(x\) 所在的子簇内已无值,\(mx\) 为前面最后一个有值的子簇的 \(mx\)。

第五十行记得更新 \(mx\),此处的情况是 \(x\) 所在的子簇内还有值,\(mx\) 直接为该子簇的 \(mx\)。

时间复杂度分析

member:\(T(u)=T(\sqrt u)+O(1)\),花费 \(O(\log\log u)\)。

predecessor successor:\(T(u)=T(\sqrt u)+O(1)\),花费 \(O(\log\log u)\)。

insert Delete:虽然看起来有两次调用,但分析后会发现有一个只需要花费常数时间,所以 \(T(u)=T(\sqrt u)+O(1)\),花费 \(O(\log\log u)\)。

总时间复杂度 \(O(m\log\log u)\),非常优秀。

后记

这个算法我啃了好几天,虽然在 OI 中用处不大,但也算是自己的一个增长点吧。

另外,这个算法的思想和改进思路也是值得借鉴的。限制一部分,优化一部分。

在优化这方面,为什么会想到用值域的平方根来作为下一层的大小呢?其实这取决于 \(summary\)。在想到用 \(summary\) 来优化同一层的查找后,递归就分成了两部分,一部分是 \(summary\),另一部分是 \(summary\) 中每个值对应的值域区间。这两部分当然尽可能均匀越好喽,所以就取了平方根。

最后成型的 \(mn\) 和 \(mx\) 让查询变得非常快速,\(mn\) 不会出现在簇中让更新变得非常快速,不得不说非常非常巧妙。

只剩必要的一条递归。

将信息利用到了极致。

浅谈 van Emde Boas 树——从 u 到 log log u 的蜕变的更多相关文章

  1. BZOJ 3685: 普通van Emde Boas树( 线段树 )

    建颗权值线段树就行了...连离散化都不用... 没加读入优化就TLE, 加了就A掉了...而且还快了接近1/4.... ---------------------------------------- ...

  2. bzoj3685普通van Emde Boas树 线段树

    3685: 普通van Emde Boas树 Time Limit: 9 Sec  Memory Limit: 128 MBSubmit: 1932  Solved: 626[Submit][Stat ...

  3. BZOJ_3685_普通van Emde Boas树_权值线段树

    BZOJ_3685_普通van Emde Boas树_权值线段树 Description 设计数据结构支持: 1 x  若x不存在,插入x 2 x  若x存在,删除x 3    输出当前最小值,若不存 ...

  4. bzoj 3685: 普通van Emde Boas树

    3685: 普通van Emde Boas树 Description 设计数据结构支持:1 x  若x不存在,插入x2 x  若x存在,删除x3    输出当前最小值,若不存在输出-14    输出当 ...

  5. 【bzoj3685】普通van Emde Boas树 线段树

    普通van Emde Boas树 Time Limit: 9 Sec  Memory Limit: 128 MBSubmit: 1969  Solved: 639[Submit][Status][Di ...

  6. 算法导论笔记——第二十章 van Emde Boas树

    当关键字是有界范围内的整数时,能够规避Ω(lglgn)下界的限制,那么在类似的场景下,我们应弄清楚o(lgn)时间内是否可以完成优先队列的每个操作.在本章中,我们将看到:van Emde Boas树支 ...

  7. 【BZOJ3685】【zkw权值线段树】普通van Emde Boas树

    原题传送门 因为马上要开始搞树套树了,所以学了一波权值线段树...毕竟是会点zkw线段树的,所以zkw线段树大法好! 解题思路: 介绍一下权值线段树吧,其实感觉就是线段树的本义,就是你用线段树维护了数 ...

  8. 【模板】BZOJ 3685: 普通van Emde Boas树——Treap

    传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=3685 据说神犇都是用zkw线段树水过的啊... 我蒟蒻只会写treap,加了fread之后8 ...

  9. 【权值分块】bzoj3685 普通van Emde Boas树

    权值分块,虽然渐进复杂度不忍直视,但其极小的常数使得实际运行起来比平衡树快,大多数情况和递归版权值线段树差不多,有时甚至更快.但是被zkw线段树完虐. #include<cstdio> # ...

随机推荐

  1. router-view组件在app.vue中渲染不出来怎么办

    1.在app.vue使用router-view组件直接渲染 页面什么都没显示,可能问题出在路由配置上了,检查路由是否配置完好,路由挂载那里必须使用routes属性 2.在app.vue中router- ...

  2. 关于查看本机ssh公钥以及生成公钥

    1.查看本机公钥: 打开git bush,执行  cd ~/.ssh  进入.ssh文件夹(C:\Users\Administrator\.ssh) 执行  ls  命令,查看列表 执行 cat id ...

  3. Longest common subsequence(LCS)

    问题 说明该问题在生物学中的实际意义 Biological applications often need to compare the DNA of two (or more) different ...

  4. 自己动手实现一个简单的 IOC容器

    控制反转,即Inversion of Control(IoC),是面向对象中的一种设计原则,可以用有效降低架构代码的耦合度,从对象调用者角度又叫做依赖注入,即Dependency Injection( ...

  5. Java学习的第十五天

    1.今天复习了第四章的内容 重新看了看方法参数问题,final修饰的关键字 2.今天没问题 3.明天学习多态变化

  6. win10右键打开PowerShell

    win10右键打开PowerShell 转载自:http://www.xitongzhijia.net/xtjc/20170526/98756.html 如图: 1.首先在桌面新建一个txt文文件 复 ...

  7. 842. Split Array into Fibonacci Sequence —— weekly contest 86

    题目链接:https://leetcode.com/problems/split-array-into-fibonacci-sequence/description/ 占坑. string 的数值转换 ...

  8. XJOI NOI训练2 传送

    NTT循环卷积 30分: 可以发现这是一个很明显的分层$DP$,设$dp[i][j]$表示当前走了j步走到i号节点的方案数.如果当前走的步数对节点有限制就直接将这个点的$DP$值赋成$0$ #incl ...

  9. Spring Boot API 统一返回格式封装

    今天给大家带来的是Spring Boot API 统一返回格式封装,我们在做项目的时候API 接口返回是需要统一格式的,只有这样前端的同学才可对接口返回的数据做统一处理,也可以使前后端分离 模式的开发 ...

  10. c#中简单工厂模式

    运算类 public class yunsuan { public static operation create(string operate) { operation oper = null; s ...