玄学算法/精彩DS总结 I

teafrogsf

2018-03-26 22:04:45

Personal

## $0.Preface$ ### 第一次认真写一大篇博文,主要是因为~~模拟退火这玩意儿~~很多东西都太玄学,有必要自己总结一下提升印象。 **大部分代码都在luogu/loj的提交记录里面,可以随时找到。** 时光流逝太多了,转眼间总结都写了九篇,甚至有很多内容因为写不下我自己单独开了$slide$。自己比起以前虽然进步了很多,但智商越来越低,体力也越来越差。这时就能够体会到,$OI$确实是一门竞赛。 ## $1.Simulated\ Annealing$ 简单来说,模拟退火是一种模拟现实世界退火过程进行随机贪心的算法。 **它主要进行如下的步骤:** 1. **选取初始温度、温度下降率和温度下界**。这通常决定你算法的效率、同时也决定了算法的正确概率。 2. **随机选取一个当前解的扰动范围中的新解,比较两者的答案。选择过程中可以根据步长调整随机**。当新解答案更优时,直接选取新解答案;当原解答案更优时,根据$Metropolis$准则选取答案。 3. **重复以上过程,直至温度降低到下界为止**。 其中,$Metropolis$准则是指当概率$p=\exp(\frac{-(Ans_i-Ans_j)}{T})>rd$时,选取新解,否则选取原解。 $i$表示原解,$j$表示新解。公式中$Ans_i-Ans_j$当且仅当$Ans_i<Ans_j$时$Ans_j$为更优解时。否则为$Ans_j-Ans_i$。 $rd$为$[0,1)$的随机浮点数。 ### 然而实际在应用中,一般用$\rm rand()\%10000<=T$代替$Metropolis$准则。这样开始接受新解的概率很高,而后期会趋向于接受原解。 4. **多次重复退火,选取最优答案**。 ### 此外一定要注意的是: ### 1.在实际应用中,有时候你在退火前会对数组进行处理,退火后继续处理(如$\rm HNOI2011$任务调度),此时请务必重新复制一个数组放入退火过程; ### 2.务必注意你的随机过程中有没有$\%0$或$\div0$。 ------------ ### [HAOI2006]均分数据 此题退火第二步为随机选取一个元素将其放到另一组内。初始化时已经随机分组。 顺带一提,一大波大佬的博客都是把$Metropolis$改成一个神奇的随机,但正确率貌似比$Metropolis$还高。把$\rm19260817$换成$\rm19491001$才过。 ------------ ### [HNOI2011]任务调度 这题其实可以用各种随机算法艹过去。写退火练练手。 贪心地进行检测就好。 调参比较费劲,考场上记得对拍。 ```cpp #include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #define neko 1010 #define chkmin(a,b) ((a)<(b)?(a):(b)) #define chkmax(a,b) ((a)>(b)?(a):(b)) #define f(i,a,b) for(register int i=(a);i<=(b);i=-(~i)) #define rf(i,a,b) for(register int i=(a);i>=(b);i=~(-i)) using std::sort; static int n,pa,pb,stk,suma,sumb,ans=2e9; typedef int arr[neko]; static arr salp,sbet,alp,bet,s; struct node { int opt,a,b; }q[neko]; bool cmpa(int x,int y) { if(q[x].b==q[y].b)return q[x].a<q[y].a; return q[x].b>q[y].b; } bool cmpb(int x,int y) { if(q[x].a==q[y].a)return q[x].b<q[y].b; return q[x].a>q[y].a; } int greedy() { int ansa=suma,ansb=0,sum; //assume q[alp].b time < q[bet].a time f(i,1,pb) { ansb+=q[bet[i]].b; if(ansb<ansa)ansa+=q[bet[i]].a; else ansa=ansb+q[bet[i]].a; }sum=chkmax(ansa,ansb); //opposite ansb=sumb,ansa=0; f(i,1,pa) { ansa+=q[alp[i]].a; if(ansa<ansb)ansb+=q[alp[i]].b; else ansb=ansa+q[alp[i]].b; }sum=chkmax(sum,chkmax(ansa,ansb));//pick the maximum return sum; } void Swap(int &a,int &b) {int t=a;a=b;b=t;} int solve() { int nowa=0,swapa=0,nowb=0,swapb=0,preans,nowans,minus; double T=100000; preans=greedy(); while(T>0.1) { T*=0.99; if(pa)nowa=rand()%pa+1,swapa=rand()%pa+1; if(pb)nowb=rand()%pb+1,swapb=rand()%pb+1; //printf("%d %d %d %d\n",nowa,nowb,swapa,swapb); if((nowa==swapa)&&(nowb==swapb))continue; Swap(alp[nowa],alp[swapa]),Swap(bet[nowb],bet[swapb]); minus=preans-(nowans=greedy()); if(minus<0&&(rand()%10000>T))Swap(alp[nowa],alp[swapa]),Swap(bet[nowb],bet[swapb]);//do not accept the new solution else preans=nowans;//accept the solution,so update the answer //printf("%d %d\n",preans,nowans); }return preans; } int SA() { int SAans=2e9;suma=sumb=0; memcpy(alp,salp,sizeof(salp)); memcpy(bet,sbet,sizeof(sbet)); f(i,1,pa)suma+=q[alp[i]].a; f(i,1,pb)sumb+=q[bet[i]].b; sort(alp+1,alp+pa+1,cmpa),sort(bet+1,bet+pb+1,cmpb); f(i,1,100)SAans=chkmin(SAans,solve()); return SAans; } void dfs(int step) { if(step>stk){ans=chkmin(ans,SA());return;} salp[++pa]=s[step]; if(rand()%40000<30000)dfs(step+1); --pa; sbet[++pb]=s[step]; if(rand()%40000<30000)dfs(step+1); --pb; } int main() { srand(19491001+19260817); scanf("%d",&n); f(i,1,n) { scanf("%d%d%d",&q[i].opt,&q[i].a,&q[i].b); if(q[i].opt==1)salp[++pa]=i; else if(q[i].opt==2)sbet[++pb]=i; else s[++stk]=i; }dfs(1),printf("%d\n",ans); return 0; } ``` ## $2.Linear\ Basis$ ~~这坑目前是填不掉了。虽然写了几个板子,HAOI2017的那道题没用线段树分治也拿到了60分~~其实是70分但是WA了一个点~~,但是感觉还是没有理解线性基。之后问一下几位大佬。~~ 注意:以下内容为未完全理解线性基时写的内容,完整版本请看$slide$。 还是写一点东西吧。 线性基是一个大小为$\log$的子集合,集合内的所有元素可以异或出原集合的所有数。 ### 构造、求异或最大值 都是从大到小贪心,复杂度为$\Theta(\log)$。 $UPD:$实际上是$O($消元复杂度$\times$基元素个数(也就是向量维数)$)$。 ### 合并 复杂度是$\Theta(\log^2)$。 ### 求异或最小值 直接看最低的哪一位有值。 ### 此外可以$\Theta(\log^2)$的重构线性基来使每一位相互独立: 如果$j<i$且$bas_i\&(1<<j)$,那么$bas_i\wedge=bas_j$。 **注意:这被称为真线性基,即满足线性无关的线性基其实是长这样的。** 这可以用来解决求异或第k小的问题:res=0,如果k第i位为1,res异或线性基中第i个元素,最终res就是第k小。 ### [SCOI2016]幸运数字 树链剖分维护线性基异或最大值。对于每条链都暴力合并,复杂度是十分正确的$\Theta(\log^2)$。但是我常数太大了qwq ```cpp // luogu-judger-enable-o2 #include<cstdio> #include<bitset> #define neko 30010 #define chkmin(a,b) ((a)<(b)?(a):(b)) #define chkmax(a,b) ((a)>(b)?(a):(b)) #define f(i,a,b) for(register int i=(a);i<=(b);++i) #define rf(i,a,b) for(register int i=(a);i>=(b);--i) #define travel(i,u,v) for(register int i=head[u],v=e[i].v;i;i=e[i].next,v=e[i].v) using namespace std; static int maxbit; typedef int arr[neko]; static arr dep,siz,fa,son,w,ord,top,head; typedef long long ll; static ll a[neko],A[neko]; struct Basis { ll x[62]={0}; }bas[neko<<2]; namespace Linear_Basis { void insert(int root,ll alp) { rf(i,maxbit,0) { if(!bas[root].x[i]){bas[root].x[i]=alp;break;} else alp^=bas[root].x[i]; } } Basis merge(Basis l,Basis r) { f(i,0,maxbit) { if(r.x[i]) { rf(j,maxbit,0) { if(r.x[i]&(1ll<<j)) { if(!l.x[j]){l.x[j]=r.x[i];break;} else r.x[i]^=l.x[j]; } } } }return l; } ll greedy(Basis alp) { ll ans=0; rf(i,maxbit,0)if((ans^alp.x[i])>ans)ans^=alp.x[i]; return ans; } } namespace Seg_Tree { #define mid ((l+r)>>1) #define lson root<<1,l,mid #define rson root<<1|1,mid+1,r #define ori tagl,tagr using namespace Linear_Basis; void build(int root,int l,int r) { if(l==r){insert(root,A[ord[l]]);return;} build(lson); build(rson); bas[root]=merge(bas[root<<1],bas[root<<1|1]); } Basis query(int root,int l,int r,int tagl,int tagr) { if(l>=tagl&&r<=tagr)return bas[root]; Basis tmp;bool flag=0; if(tagl<=mid)tmp=query(lson,ori),flag=1; if(tagr>mid)tmp=merge(tmp,query(rson,ori)); return tmp; } } int root,n,q,t,cnt; struct node { int v,next; }e[neko<<1]; namespace Siz_Subdivision { using namespace Seg_Tree; void swap(int &x,int &y) {int t=x;x=y,y=t;} void add(int x,int y) { e[++t].v=y; e[t].next=head[x]; head[x]=t; } void dfs(int u) { dep[u]=dep[fa[u]]+1; siz[u]=1; travel(i,u,v) { if(v!=fa[u]) { fa[v]=u; dfs(v); siz[u]+=siz[v]; if(!son[u]||siz[son[u]]<siz[v])son[u]=v; } } } void dfs2(int u) { w[u]=++cnt; ord[cnt]=u; if(son[fa[u]]==u)top[u]=top[fa[u]]; else top[u]=u; if(son[u])dfs2(son[u]); travel(i,u,v)if(v!=fa[u]&&v!=son[u])dfs2(v); } ll pathq(int l,int r) { Basis now; while(top[l]!=top[r]) { if(dep[top[l]]<dep[top[r]])swap(l,r); now=merge(now,query(1,1,n,w[top[l]],w[l])); l=fa[top[l]]; }if(dep[l]>dep[r])swap(l,r); now=merge(now,query(1,1,n,w[l],w[r])); return Linear_Basis::greedy(now); } } void print(ll x) { char num[110];int p=0; while(x) { num[++p]=x%10; x/=10; }rf(i,p,1)putchar(num[i]+'0'); putchar('\n'); } int main() { using namespace Siz_Subdivision; int x,y; scanf("%d%d",&n,&q); f(i,1,n)scanf("%lld",&a[i]),A[i]=a[i]; f(i,1,n) { x=0; while(a[i])a[i]>>=1,++x; maxbit=chkmax(x,maxbit); } f(i,1,n-1) { scanf("%d%d",&x,&y); add(x,y),add(y,x); }root=1; dfs(root),dfs2(root); Seg_Tree::build(1,1,n); f(i,1,q)scanf("%d%d",&x,&y),print(pathq(x,y)); } ``` ## $3.Persistent\ Weight\ Segment\ Tree$ 可持久化线段树由其只需变动修改$\log n$个区间而节省空间。 可持久化权值线段树通常利用前缀和的思想和PST的基本操作来完成。 **需要注意的一点是:在查询时,你导入的参数为两个根,而这两个根需要随遍历往下。还要注意向右遍历时,需要用rank-tmp。可改成非递归。** ### 注意:luogu上的板子是错的,查rank的时候root为空、rank为0要return 0,当前和小于rank也要return 0。某些题目区间内数没有区间大小那么多的话就凉了。 ### 注意:Others 里有不少补充。 ## $4.Persistent\ Balanced\ Tree$ **洛谷上的模板是个卡常~~SB~~题,根只能开500000个,不然直接MLE并显示RE。** 可持久化平衡树是一个类似于PST的东西,写法也有许多相似之处。~~只是调试仍然和普通平衡树一样恶心~~ 当然,$\rm\ fhq\ treap$是一个相当好用且暴力的数据结构,但$\rm split$和$\rm merge$操作有许多值得注意的细节。 1. 两个操作都需要新建节点,并利用新的节点进行递归操作(包括$\rm pushup$)。 2. 除了求第k大,其他操作都需要$\&root$,以便于随时的$\rm split$和$\rm merge$。 3. 与原来的平衡树一样,注意前驱后继的边界。 4. **考场上谨慎把握码力。** ## $5.Static\ Node\ Dividing\&Conquering\ Tree$ 此坑终于可以开始填了。 静态点分树是指一般点分树的题型,这类题型通常不会要求你更改树的形态,因此点分树结构只需要预处理就可得出。 点分树是一个巧妙的暴力数据结构, 为了不用ST表求两点距离减少常数,**一般地,我们会将一个点的所有点分树上的祖先信息存在vector里,这样只消耗了$O(n\log n)$的时空复杂度且减少了常数。** 此外,若用vector自带的lower_bound和upper_bound,一般可以方便地二分出一个左闭右开的区间,所以我们可以使用后缀和方便统计答案。 此内容的详细部分可以见slide。 ## $6.Segment\ Tree\ Merging$ 线段树合并在树上询问子树答案时具有优异的性质和较小的复杂度(时空均$O(n\log n)$)。当然其他地方也可以用,可以参考fjzzq的博客和任轩笛的国家队论文。 ```cpp int merge(int x,int y,int l,int r) { if((!x)||(!y))return x|y; if(l==r){/*把xy两点信息合并*/ return x;} /*时间线段树合并有时需要直接在这里统计答案,其他的目前没看到;一般只有左边的修改对右边的答案有影响*/ /*(Fix[L[x]]&Qry[R[y]])+(Fix[L[y]]&Qry[R[x]])*/ /*此处 & + 均为信息合并,上式成立仅当不同子树贡献算在当前点上*/ L[x]=merge(L[x],L[y],l,mid); R[x]=merge(R[x],R[y],mid+1,r); pushup(x); return x; } void dfs(int u,int fa) { for(register int i=head[u],v=e[i].v;i;i=e[i].nex,v=e[i].v)if(v^fa)dfs(v,u),now=0,rt[u]=merge(rt[u],rt[v],0,m),ans[u]+=now; }//这是树上线段树合并的dfs形式 ``` $ $ ```cpp int merge(int x,int y) { if((!x)||(!y))return x|y; //如果有强制在线或者不能在merge时回答询问 可以在这里newnode 可持久化一下 L[x]=merge(L[x],L[y]); R[x]=merge(R[x],R[y]); //此处把y的信息合并到x上 这种写法当且仅当任意信息可直接加 注意根据题目使用 //如Sum[x]+=Sum[y]; return x; } ``` $newnode$的空间复杂度也是$O(n\log n)$的,因为原来构建了$O(n\log n)$(题目不同有差异)个点,而$merge$遇到的总点数就是那么多,所以没有问题。~~但应该要开$\sout{8\log}$空间的啊,做题开$\sout{4\log}$似乎就没有问题了,不知道为啥qwq~~ 需要注意因为非可持久化的线段树合并后儿子信息会改变(但普通动态开点线段树的根是不会改变的),因为如果你运用了一个儿子的结点上来,那么接下来你对这个点进行修改的时候对应的儿子结点也会修改,所以**当前答案只能在当前统计**。 还有一个$Interesting\ Fact$,就是“可持久化”线段树合并和“可持久化线段树”合并还是有点区别的。