线段树模板及技巧分析
来源:互联网 发布:欠淘宝贷款50万 编辑:程序博客网 时间:2024/06/06 06:59
——————ver. 2016.3.13——————
———模板建设中,如有建议欢迎指出———
关键词:空间压缩、递归优化
源码下载地址:http://pan.baidu.com/s/1o6UZ7qM
本文分析了基于比较的线段树的优化技巧,并且附带了一份封装好的源码。
0、不要吐槽代码规范和多(bu)样(yi)化(zhi)的缩进风格%>_<%
1、不要问我考场上怎么写得了这么多,重点是技巧,技巧啦%>_<%
2、求和线段树会在以后给出,目前还不(lan)支(de)持(xie)
#ifndef FUNCTOR_H#define FUNCTOR_Htemplate <class ValueType>struct GetLess{ ValueType operator () (const ValueType& x,const ValueType& y) { return x<y?x:y; }};template <class ValueType>struct Less { bool operator () (const ValueType& x,const ValueType& y) const { return x<y; }};
我们把比较和选择函数封装成了仿函数(重载()运算符)的形式。
1、仿函数可以方便地用作模板类的模板参数。(虽然理论上函数指针也可以,不过一般没人这么做)
以我们熟悉的STL的priority_queue为例。定义其对象:
std::priority_queue < int,std::vector< int >,std::greater< int > >
我们定义了一个int类型的小根堆。这三个模板参数,第一个指定数据的类型是int,第二个指定优先队列内部使用的容器是std::vector,而第三个就是< functional >头文件中提供的仿函数,用于数据之间的比较。
#include <functional>int A,B;std::greater<int> cmp;bool flag=cmp(A,B);/*————————————————*/bool greater(int x,int y) {return x>y;}bool flag=greater(A,B);
仿函数的使用方法如上:定义一个仿函数对象,然后把它当作函数一样调用。cmp仿函数的功能和greater函数是等价的。
2、仿函数一般比函数指针要快。不过要避免每次使用仿函数都建立一个临时对象,否则会拉低效率。
template <class ValueType>struct SegNode_Cmp_Primary { ValueType val; inline void assign(const ValueType& _val) { val = _val; } inline void add(const ValueType& _val) { val = val + _val; } inline void set(const ValueType& _val) { val = _val; } inline ValueType actualVal() { return val; }};template <class ValueType>struct SegNode_Cmp_Secondary : public SegNode_Cmp_Primary<ValueType> { typedef SegNode_Cmp_Primary<ValueType> BaseI; using BaseI::val; ValueType lazyTag; inline void assign(const ValueType& _val) { val = _val; lazyTag = ValueType(); } inline void add(const ValueType& _val) { lazyTag = lazyTag + _val; } inline void set(const ValueType& _val) { val = _val; lazyTag=ValueType(); } inline void reset() { val = val + lazyTag; lazyTag = ValueType(); } inline ValueType actualVal() { return val + lazyTag; }};template <class ValueType>struct SegNode_Cmp_Tertiary : public SegNode_Cmp_Secondary<ValueType> { typedef SegNode_Cmp_Primary<ValueType> BaseI; typedef SegNode_Cmp_Secondary<ValueType> BaseII; using BaseI::val; using BaseII::lazyTag; char status; inline void assign(const ValueType& _val) { val = _val; lazyTag = ValueType(); status='n'; } inline void add(const ValueType& _val) { lazyTag = lazyTag + _val; if (status == 'n') status = 'a'; } inline void set(const ValueType& _val) { lazyTag = _val; status = 's'; } inline void reset() { switch (status) { case 'n': break; case 'a': val = val + lazyTag; lazyTag = ValueType(); status = 'n'; break; case 's': val = lazyTag; lazyTag = ValueType(); status = 'n'; break; } } inline ValueType actualVal() { switch (status) { case 'n': return val; case 'a': return val + lazyTag; case 's': return lazyTag; } }};
线段树的节点。
我们以类继承的方式将节点分为了三级:
level primary:支持区间询问和单点修改(add或set),但不允许区间修改操作。
这种情况下,我们实际上不需要lazyTag,所以在一级节点中只有val一个成员变量。
level secondary:支持区间询问和单点修改,允许区间add但不允许区间set操作。
在这一级中设立了lazyTag,用来延迟修改从而提高效率。
level tertiary:支持区间询问及修改(add和set)
status变量的作用是表明lazyTag属于何种类型:
‘n’:没有lazyTag
‘a’:lazyTag的作用是区间add
‘s’:lazyTag的作用是区间set
值得注意的是lazyTag的覆盖或叠加(上层‘s’标记覆盖下层标记,或是上层’a‘标记与下层标记叠加)。详见代码。
(貌似类的成员函数默认是内联的,所以这里的inline关键字加不加都无所谓)
#ifndef SEGMENTTREE_H#define SEGMENTTREE_H#include "SegmentTree/SegNode.h"#include "ErrorAndMistake.h"#include <cstdlib>#include <cstdio>struct PrimaryTag {};struct SecondaryTag : public PrimaryTag {};struct TertiaryTag : public SecondaryTag {};template <class ValueType,class Tag>struct NodeTraits { typedef SegNode_Cmp_Primary<ValueType> SegNode;};template <class ValueType>struct NodeTraits<ValueType, SecondaryTag> { typedef SegNode_Cmp_Secondary<ValueType> SegNode;};template <class ValueType>struct NodeTraits<ValueType, TertiaryTag> { typedef SegNode_Cmp_Tertiary<ValueType> SegNode;};#define SEGTREE_CMP_TEMPLATE template \ <class ValueType,class OperationFunctor,class SegNodeTag>#define SEGTREE_CMP_TYPE SegmentTree_Cmp \ <ValueType,OperationFunctor,SegNodeTag>template <class ValueType,class OperationFunctor,class SegNodeTag>class SegmentTree_Cmp {protected: typedef typename NodeTraits<ValueType, SegNodeTag>::SegNode SegNode; typedef SegNode* DynamicArray; DynamicArray node; int right; int cpStack[32]; int rtStack[32]; int cur; OperationFunctor func; SegNodeTag tag; inline int middle(int nodeL, int nodeR) { return (nodeL + nodeR) >> 1; } inline int rightIndex(int curIdx, int nodeL, int mid) { return curIdx + ((mid + 1 - nodeL) << 1); } void init_aux(const ValueType& _val); template <class RAIterator> void init_aux(RAIterator first, int curIdx, int len); void dropLazyTag(int curIdx, int rightIdx, SecondaryTag); void dropLazyTag(int curIdx, int rightIdx, TertiaryTag); void rangeAdd_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, int stackPos, PrimaryTag); //just throw an error void rangeAdd_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, int stackPos, SecondaryTag); void rangeSet_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, int stackPos, PrimaryTag); void rangeSet_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, int stackPos, TertiaryTag); void pointSet_aux(const ValueType& _val, int curIdx, int pos, int nodeL, int nodeR, PrimaryTag); void pointSet_aux(const ValueType& _val, int curIdx, int pos, int nodeL, int nodeR, SecondaryTag); template <class Comparator> bool pointReplace_aux(const ValueType& _val, int curIdx, int pos, int nodeL, int nodeR, Comparator cmp, PrimaryTag); template <class Comparator> bool pointReplace_aux(const ValueType& _val, int curIdx, int pos, int nodeL, int nodeR, Comparator cmp, SecondaryTag); ValueType rangeAsk_aux(int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, PrimaryTag); ValueType rangeAsk_aux(int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, SecondaryTag);public: SegmentTree_Cmp(int _len) : right(_len - 1) { node = new SegNode[_len << 1]; cur = -1; } ~SegmentTree_Cmp() { delete[] node; } void init(const ValueType& _val) { init_aux(_val); } template <class RAIterator> void init(RAIterator first) { init_aux(first, 0, right + 1); } void rangeAdd(const ValueType& _val, int rangeL, int rangeR) { try { rangeAdd_aux(_val, 0, rangeL, rangeR, 0, right, -1, tag); } catch (IllegalOperation) { printf("\nNot allowed to call rangeAdd() function with a single-modifiable segment tree!\n"); exit(1); } } void rangeSet(const ValueType& _val,int rangeL,int rangeR) { try { rangeSet_aux(_val,0,rangeL,rangeR,0,right,-1,tag); } catch(IllegalOperation) { printf("\nNot allowed to call rangeSet() function with a non-range-settable segment tree!\n"); exit(1); } } void pointSet(const ValueType& _val, int pos) { pointSet_aux(_val, 0, pos, 0, right, tag); } template <class Comparator> bool pointReplace(const ValueType& _val, int pos, Comparator cmp) { return pointReplace_aux(_val, 0, pos, 0, right, cmp, tag); } ValueType rangeAsk(int rangeL, int rangeR) { return rangeAsk_aux(0, rangeL, rangeR, 0, right, tag); } ValueType askAll() { return node[0].actualVal(); }};#include "SegmentTree/Cmp_Details/Init_aux.h"#include "SegmentTree/Cmp_Details/DropLazyTag.h"#include "SegmentTree/Cmp_Details/RangeAdd_aux.h"#include "SegmentTree/Cmp_Details/RangeAsk_aux.h"#include "SegmentTree/Cmp_Details/PointSet_aux.h"#include "SegmentTree/Cmp_Details/PointReplace_aux.h"#include "SegmentTree/Cmp_Details/RangeSet_aux.h"#endif /* SEGMENTTREE_H */
(请允许我吐槽一下NetBeans蛋疼的AStyle⊙﹏⊙b)
首先是文件开头的3个tag(空的struct)。其作用是作为线段树的模板参数,标明你使用的哪一级的节点。同样,我们使用了类继承的形式。
与之对应的是下面的3个traits。它们的作用就是利用typedef将你需要的节点类型“萃取”出来。两个特化版本分别针对secondary和tertiary节点。如果你选的是PrimaryTag,那就按第一个非特化版本来。
之后就是SegmentTree_Cmp类的成员声明或定义。
首先看成员变量:
第一个typedef配合上面的NodeTraits共同完成对节点的选择。
第二个……咳咳……
node数组:不言而喻。
right:表示线段树的区间总范围是[0,right]。
func:仿函数对象。前面说了用一次就设一个临时变量的话会浪费时间。
tag:对应前面的3个tag结构体。由于节点类型的不同,同一操作需要不同的策略,tag的作用就是视情况“分流”。设立一个对象的原因同func。
cpStack和rtStack的性质顾名思义,cur则是这两个栈共用的栈顶指针。
其作用是进行手动递归优化,具体原理稍后再讲。
然后看成员函数:
mid:求区间节点[nodeL,nodeR]的中点。我们将区间分成了[nodeL,mid]和[mid+1,nodeR]两个部分。
rightIdx:看起来比较诡异⊙﹏⊙b
这涉及到了我们如何存储和组织线段树节点:
引理:若某线段树节点的区间长度为L(即nodeR-nodeL+1),则以该节点为根的子树的节点数为f(L)=2L-1.
如果L为偶数,则f(L)=2f(L/2)+1;
如果L为奇数,记a=(L-1)/2,b=(L+1)/2,则f(L)=f(a)+f(b)+1
递推起点f(1)=1
之后由数学归纳法易证。
由该引理可知:我们需要的空间实际上只有总区间长度的两倍。
我们把根节点标号为0,然后进行先序遍历(根->左->右)。
记当前节点的标号为x,则:
左孩子的标号为x+1
右孩子的标号为x+2L(L为左孩子的区间长度)
x+2L实际上就是x+(2L-1)+1,表示遍历完左子树的2L+1个节点后,下一个节点就是右孩子。
节点本身并没有存储这些信息,它们都是即时计算出来的。
诡异的是:我原本想把每个节点的mid和rightIdx存储到node结构体中,从而避免大量的即时计算。然而预处理后的效率远远低于即时计算!
(哪位大牛知道原因可否指点一下(⊙o⊙) )
之后的一大堆aux(=auxiliary)函数对应相应接口(即public函数)的底层操作。而这里tag的作用就凸显出来了:
void dropLazyTag(int curIdx, int rightIdx, SecondaryTag); void dropLazyTag(int curIdx, int rightIdx, TertiaryTag);
以这两个为例,我们给dropLazyTag函数重载了两个版本,分别对应secondary节点和tertiary节点的lazyTag处理策略。tag的作用,就是在调用时确定该使用哪个版本。
最后一个函数参数并没有给出变量名,因为不需要:-D
接口函数及功能如下:
init:将线段树初始化。用某个数值或数组填充线段树。
rangeAsk:区间询问。
rangeAdd:给区间赋予一个增量_val。
rangeSet:将区间所有值设为_val。
pointSet:将某个点的值设为_val。
pointReplace:传递一个仿函数对象(或函数指针)cmp,当cmp(_val,该位置的值)为true时,等效于pointSet,否则,不做修改。
#ifndef INIT_AUX_H#define INIT_AUX_HSEGTREE_CMP_TEMPLATEvoid SEGTREE_CMP_TYPE::init_aux(const ValueType& _val) { int s = (right + 1) << 1; for (int i = 0; i < s; ++i) node[i].assign(_val);}SEGTREE_CMP_TEMPLATEtemplate <class RAIterator>void SEGTREE_CMP_TYPE::init_aux(RAIterator first, int curIdx, int len) { if (len == 1) node[curIdx].assign(*first); else { int mid = (len + 1) >> 1; int rightIdx = curIdx + (mid << 1); init_aux(first, curIdx + 1, mid); init_aux(first + (mid), rightIdx, len >> 1); node[curIdx].assign( func(node[curIdx + 1].val, node[rightIdx].val)); }}#endif /* INIT_AUX_H */
第一个版本很简单,直接赋值。
第二个版本利用了一个Random Access Iterator。
所谓Random Access Iterator(随机存取迭代器),就是支持+,-,*(取值)运算的迭代器。C语言的指针(它们被称作原生指针,顺带一提,数组名是常量指针)和std::vector的迭代器都属于这一类。
#ifndef DROPLAZYTAG_H#define DROPLAZYTAG_HSEGTREE_CMP_TEMPLATEvoid SEGTREE_CMP_TYPE::dropLazyTag (int curIdx,int rightIdx, SecondaryTag){ node[curIdx+1].add(node[curIdx].lazyTag); node[rightIdx].add(node[curIdx].lazyTag); node[curIdx].reset();}SEGTREE_CMP_TEMPLATEvoid SEGTREE_CMP_TYPE::dropLazyTag (int curIdx,int rightIdx, TertiaryTag){ switch (node[curIdx].status) { case 'n': return; case 'a': node[curIdx + 1].add (node[curIdx].lazyTag); node[rightIdx].add (node[curIdx].lazyTag); node[curIdx].reset (); break; case 's': node[curIdx + 1].set (node[curIdx].lazyTag); node[rightIdx].set (node[curIdx].lazyTag); node[curIdx].reset (); break; }}#endif /* DROPLAZYTAG_H */
向下传递lazyTag的操作。
对于secondary节点,直接给孩子节点加上当前节点的lazyTag值即可,然后将当前节点的lazyTag重设。
对于tertiary节点,
则需要根据当前节点的lazyTag类型,选择不同的下传方式
SEGTREE_CMP_TEMPLATEvoid SEGTREE_CMP_TYPE::rangeAdd_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, int stackPos, SecondaryTag) { while (1) { if (rangeL == nodeL && rangeR == nodeR) { node[curIdx].add(_val); break; } else { int MID = middle(nodeL, nodeR); int RIGHT = rightIndex(curIdx, nodeL, MID); dropLazyTag(curIdx, RIGHT, tag); if (rangeR <= MID) { cpStack[++cur]=curIdx; rtStack[cur]=RIGHT; ++curIdx; nodeR=MID; } else if (rangeL > MID) { cpStack[++cur]=curIdx; rtStack[cur]=RIGHT; curIdx=RIGHT; nodeL=MID+1; } else { rangeAdd_aux(_val, curIdx + 1, rangeL, MID, nodeL, MID,cur, tag); cpStack[++cur]=curIdx; rtStack[cur]=RIGHT; curIdx=RIGHT; nodeL=rangeL=MID+1; } } } for(;cur>stackPos;--cur) { int& _curIdx=cpStack[cur]; int& _rightIdx=rtStack[cur]; node[_curIdx].val=func(node[_curIdx+1].actualVal(),node[_rightIdx].actualVal()); }}SEGTREE_CMP_TEMPLATEvoid SEGTREE_CMP_TYPE::rangeAdd_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR, int stackPos, PrimaryTag) { throw IllegalOperation();}
区间add的常规写法是:向左递归,向右递归,或者是先后进行两者。在递归退栈的过程中更新节点的值。实际上很多递归的过程都是不必要的,尤其是对于单向下传的情况。
我们采取了递归优化的策略。利用两个栈:cpStack和rtStack,分别存放“递归”(因为不是真正的递归所以加了引号)过程中每一步的curIdx和rightIdx的值。
如果单向下传,则只需要使当前的curIdx和rightIdx入栈。
如果双向下传,递归其中的一支(代码中为递归左孩子)即可,另一支仍然利用单向下传的策略。
最后退栈到最初的位置(由函数的倒数第二个参数决定,递归的第一层这个参数为-1)。退栈过程中更新沿途的节点。我们直接给node的val成员赋值,是因为进栈的过程中lazyTag已经被更新掉了。
如果节点是primary的(不允许区间修改),那么我们什么都不要做,只抛出一个IllegalOperation()异常信号。
void rangeAdd(const ValueType& _val, int rangeL, int rangeR) { try { rangeAdd_aux(_val, 0, rangeL, rangeR, 0, right, -1, tag); } catch (IllegalOperation) { printf("\nNot allowed to call rangeAdd() function with a single-modifiable segment tree!\n"); exit(1); } }
这是对应的接口函数。如果接受到了异常信号,就输出错误提示并且强行退出程序(好狠O__O”…)
其他区间操作的递归优化策略类似。而对于单点操作,我们完全可以不用递归。
其他部分限于篇幅就不再贴出了,详见代码。事实上稍加观察就会发现很多操作都如出一辙,只是视实际情况有些差异。
最后附上“借教室”一题的测试结果:
测试数据 #0: Accepted, time = 0 ms, mem = 548 KiB, score = 5
测试数据 #1: Accepted, time = 0 ms, mem = 548 KiB, score = 5
测试数据 #2: Accepted, time = 0 ms, mem = 568 KiB, score = 5
测试数据 #3: Accepted, time = 0 ms, mem = 572 KiB, score = 5
测试数据 #4: Accepted, time = 0 ms, mem = 572 KiB, score = 5
测试数据 #5: Accepted, time = 0 ms, mem = 568 KiB, score = 5
测试数据 #6: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #7: Accepted, time = 15 ms, mem = 2508 KiB, score = 5
测试数据 #8: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #9: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #10: Accepted, time = 46 ms, mem = 2504 KiB, score = 5
测试数据 #11: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #12: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #13: Accepted, time = 78 ms, mem = 2508 KiB, score = 5
测试数据 #14: Accepted, time = 328 ms, mem = 20128 KiB, score = 5
测试数据 #15: Accepted, time = 500 ms, mem = 20124 KiB, score = 5
测试数据 #16: Accepted, time = 656 ms, mem = 20128 KiB, score = 5
测试数据 #17: Accepted, time = 781 ms, mem = 20124 KiB, score = 5
测试数据 #18: Accepted, time = 1093 ms, mem = 20128 KiB, score = 5
测试数据 #19: Accepted, time = 1109 ms, mem = 20124 KiB, score = 5
Accepted, time = 4761 ms, mem = 20128 KiB, score = 100
ps附一个完全不使用递归的rangeAdd_aux版本:
(以下的无递归版本和以上的半递归版本目前很难说孰优孰劣。本机上随机化测试时前者比后者慢出将近1s(debug模式,100万次rangeAdd操作,前者7.5s,后者6.5s),而在OJ上提交时后者总是比前者快o(╯□╰)o,不知为何)
//New things in SegmentTree_Cmp classint buf[32]; int nodeLBuf[32]; int nodeRBuf[32]; int rangeRBuf[32]; int bufPos;
//New constructorSegmentTree_Cmp(int _len) : right(_len - 1) { node = new SegNode[_len << 1]; cur = -1; bufPos=0; buf[0]=-1; }
//New rangeAdd_aux() functionSEGTREE_CMP_TEMPLATEvoid SEGTREE_CMP_TYPE::rangeAdd_aux(const ValueType& _val, int curIdx, int rangeL, int rangeR, int nodeL, int nodeR,SecondaryTag) { do { while (1) { if (rangeL == nodeL && rangeR == nodeR) { node[curIdx].add(_val); break; } else { int MID = middle(nodeL, nodeR); int RIGHT = rightIndex(curIdx, nodeL, MID); dropLazyTag(curIdx, RIGHT, tag); if (rangeR <= MID) { cpStack[++cur] = curIdx; rtStack[cur] = RIGHT; ++curIdx; nodeR = MID; } else if (rangeL > MID) { cpStack[++cur] = curIdx; rtStack[cur] = RIGHT; curIdx = RIGHT; nodeL = MID + 1; } else { buf[++bufPos] = cur; nodeLBuf[bufPos] = MID + 1; nodeRBuf[bufPos] = nodeR; rangeRBuf[bufPos] = rangeR; cpStack[++cur] = curIdx; rtStack[cur] = RIGHT; ++curIdx; rangeR = nodeR = MID; } } } if (bufPos) { for (; cur > buf[bufPos]+1; --cur) { int& _curIdx = cpStack[cur]; int& _rightIdx = rtStack[cur]; node[_curIdx].val = func(node[_curIdx + 1].actualVal(), node[_rightIdx].actualVal()); } nodeL = rangeL = nodeLBuf[bufPos]; nodeR = nodeRBuf[bufPos]; rangeR = rangeRBuf[bufPos--]; curIdx = rtStack[cur]; } else { for (; cur > buf[bufPos]; --cur) { int& _curIdx = cpStack[cur]; int& _rightIdx = rtStack[cur]; node[_curIdx].val = func(node[_curIdx + 1].actualVal(), node[_rightIdx].actualVal()); } } } while (cur>-1);}
最好的评测结果(还是借教室)如下:(不过比较糟的时候也会达到4700ms左右)
测试数据 #0: Accepted, time = 0 ms, mem = 552 KiB, score = 5
测试数据 #1: Accepted, time = 0 ms, mem = 552 KiB, score = 5
测试数据 #2: Accepted, time = 0 ms, mem = 572 KiB, score = 5
测试数据 #3: Accepted, time = 0 ms, mem = 576 KiB, score = 5
测试数据 #4: Accepted, time = 0 ms, mem = 568 KiB, score = 5
测试数据 #5: Accepted, time = 0 ms, mem = 568 KiB, score = 5
测试数据 #6: Accepted, time = 15 ms, mem = 2508 KiB, score = 5
测试数据 #7: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #8: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #9: Accepted, time = 31 ms, mem = 2508 KiB, score = 5
测试数据 #10: Accepted, time = 15 ms, mem = 2508 KiB, score = 5
测试数据 #11: Accepted, time = 62 ms, mem = 2508 KiB, score = 5
测试数据 #12: Accepted, time = 78 ms, mem = 2508 KiB, score = 5
测试数据 #13: Accepted, time = 46 ms, mem = 2508 KiB, score = 5
测试数据 #14: Accepted, time = 265 ms, mem = 20128 KiB, score = 5
测试数据 #15: Accepted, time = 406 ms, mem = 20128 KiB, score = 5
测试数据 #16: Accepted, time = 593 ms, mem = 20128 KiB, score = 5
测试数据 #17: Accepted, time = 781 ms, mem = 20128 KiB, score = 5
测试数据 #18: Accepted, time = 906 ms, mem = 20128 KiB, score = 5
测试数据 #19: Accepted, time = 1015 ms, mem = 20124 KiB, score = 5
Accepted, time = 4275 ms, mem = 20128 KiB, score = 100
- 线段树模板及技巧分析
- 线段树 模板 及 解释
- 线段树小解及模板
- 线段树模板及习题
- Discuz模板语句分析及知识技巧
- Discuz模板语句分析及知识技巧
- Discuz模板语句分析及知识技巧
- 线段树---分析 && 模板总结
- zkw线段树模板及理解
- 线段树学习笔记及模板
- 线段树模板及专题合集-----不断更新中
- 模板:线段树求区间最大/最小值及下标
- ACM 线段树模板(模板)
- 线段树模板
- hdu_1166_线段树模板
- 线段树模板
- 线段树模板 poj2777
- 线段树模板
- Oracle关于位图索引的创建与应用
- *记票统计
- 【Linux】在双系统中Linux挂载Win下的盘符
- sqlite触发器的说明和使用
- 【iOS架构】iOS ReactiveCocoa函数响应式编程
- 线段树模板及技巧分析
- kafka+zookeeper+storm+hdfs实现日志处理
- [JAVA · 初级]:10.如何更好的理解多态
- spring 中属性scope 的prototype(有状态)和singleton(无状态)
- HDFS-1
- mysql-oracle查询所有表的记录数
- 走进语音识别中的WFST(一)
- JavaSE知识集锦(2)序列化与反序列化
- (记录向)reactjs学习记录