2022.12.18 ~ 2022.12.24 一周学习记录
2022.12.18 模拟退火学习
模拟退火是一种常用的随机化算法 ,当答案是一个连续的函数时,我们就可以考虑用模拟退火进行求解。
注意调参数(看rp)
伪代码:
void SA()//模拟退火
{
for(double T=10000;T>1e-5;T=T*0.975)
{
int p=更改前的答案;
随机更改;
int q=更改后的答案;
int change=q-p;
if(exp(change/T)<(double)rand()/RAND_MAX)
以一定概率接受更改后的结果
else 恢复
}
}
练习题目:
[AHOI2014/JSOI2014]保龄球
将次序转化成序列,每次随机交换两个位置,判断是否合法后开始不断更新最优解即可。
[HAOI2006]均分数据
考虑贪心,将每个数加到当前和最小的组里,每次随机交换两个元素,不断更新最优解即可。
2022.12.19 ~ 2022.12.21 文艺平衡树(\(splay\))学习
\(splay\) 是一种平衡树,维护了中序遍历的不变性,核心操作有两个:rotate 和 splay
\(rotate\) :通过旋转不断将节点移动,但移动的同时也要保持中序遍历的不变性,可以手画一下推结论,代码如下:
struct node{
int left,right,siz,fa,v;
void init(int father,int vv)
{
fa=father,v=vv;
siz=1;
}
} tr[N];//平衡树
void pushup(int k)
{
tr[k].siz=tr[tr[k].left].siz+tr[tr[k].right].siz+1;
}
void rotate(int x)
{
int y=tr[x].fa,z=tr[y].fa;
if(tr[z].right==y) tr[z].right=x;
else tr[z].left=x;
tr[x].fa=z;
if(tr[y].left==x) tr[y].left=tr[x].right,tr[tr[x].right].fa=y,tr[x].right=y,tr[y].fa=x;
else tr[y].right=tr[x].left,tr[tr[x].left].fa=y,tr[x].left=y,tr[y].fa=x;
pushup(y),pushup(x);
}
splay:将一个节点转到另一个节点的下面(其中 \(k=0\) 表示转到根节点)需要分三个点是否在一条直线上讨论,在一条直线上要 \(rotate(y) \;rotate(x)\) ,否则需要 \(rotate(x)\) 两次,代码如下:
void splay(int &root,int x,int k)
{
while(tr[x].fa!=k)
{
int y=tr[x].fa,z=tr[y].fa;
if(z!=k)
{
if((tr[z].right==y) ^ (tr[y].right==x)) rotate(x);
else rotate(y);
}
rotate(x);
}
if(k==0) root=x;
}
insert:插入操作,插入后需要记得将其转到根节点
void insert(int x)
{
int u=root,p=0;
while(u!=0)
{
p=u;
if(x>tr[u].v) u=tr[u].right;
else u=tr[u].left;
}
u=++idx;
if(p!=0)
{
if(x>tr[p].v) tr[p].right=u;
else tr[p].left=u;
}
tr[u].init(p,x);
splay(u,0);
}
练习题目:
【模板】文艺平衡树
区间翻转操作,维护一个是否翻转的懒标记即可
[NOI2004] 郁闷的出纳员
需要记录一个全局的懒标记,并需要区间删除和查询排名为\(k\)的数
区间删除:找到这个区间的前一个数\(L\)和后一个数\(R\),将\(L\)转到根节点,再将\(R\)转到\(L\)的下面,考虑其中序遍历的不变性,这样\(R\)的左子树就是要删除的那段区间,直接删除即可
查询排名为\(k\)的数:从根节点开始遍历,将排名与左子树的大小进行比较。
如果排名更大,先特判是不是当前节点,也就是判断左子树的大小加\(1\)是否等于\(k\),否则去遍历右子树,将查询的排名减去左子树的大小加\(1\)。
否则直接遍历左子树即可。
int get_k(int x)
{
int u=root;
while(u!=0)
{
if(tr[tr[u].left].siz>=x) u=tr[u].left;
else if(tr[tr[u].left].siz+1==x) return tr[u].num;
else x=x-tr[tr[u].left].siz-1,u=tr[u].right;
}
}
[HNOI2012]永无乡
需要利用并查集和 \(splay\) 的启发式合并。
对于两棵 \(splay\) 的合并,遍历一棵树中的全部节点挨个插入即可。
void merge(int x,int y)//将以x为根的splay里的所有节点 遍历并插入到 以y为根的splay中
{
if(tr[x].left!=0) merge(tr[x].left,y);
if(tr[x].right!=0) merge(tr[x].right,y);
insert(tr[x].v,tr[x].id,y);
}
[NOI2005] 维护数列
操作大杂烩 + \(5K\) 代码
对于区间求和操作,我们在每个节点上维护一个 \(sum\) 后,可以用一个类似的操作:先找到区间的前一个数 \(L\) 和后一个数 \(R\),将 \(L\) 转到根节点,再将 \(R\) 转到 \(L\) 的下面,此时求和的区间就是 \(R\) 的左子树,直接输出 \(sum\) 即可。
对于求最大子段和操作,它只可能是三种情况:左子树的最大子段和、右子树的最大子段和、左子树的最大后缀和加上右子树的最大前缀和加上这个节点的值,取一个 \(max\) 即可。但此时我们每个节点上还要再维护最大前缀和和最大后缀和,最大前缀和的维护就是它的左子树的总和加上左子树的值加上右子树的最大前缀和,最大后缀和的维护类似。
struct node{
int left,right,fa,v,siz;
int rev,same,sum,ms,ls,rs;//是否翻转 是否相同 和 最大子段和 最大前缀和 最大后缀和
void init(int vv,int pp)//初始化
{
fa=pp,v=vv;
left=right=0;
rev=same=0;
siz=1;
sum=ms=v;
ls=rs=max(v,0);
}
} tr[N];
void pushup(int u)
{
tr[u].siz=tr[tr[u].left].siz+tr[tr[u].right].siz+1;
tr[u].sum=tr[tr[u].left].sum+tr[tr[u].right].sum+tr[u].v;
tr[u].ls=max(tr[tr[u].left].ls,tr[tr[u].left].sum+tr[u].v+tr[tr[u].right].ls);
tr[u].rs=max(tr[tr[u].right].rs,tr[tr[u].right].sum+tr[u].v+tr[tr[u].left].rs);
tr[u].ms=max(tr[tr[u].left].rs+tr[u].v+tr[tr[u].right].ls,max(tr[tr[u].left].ms,tr[tr[u].right].ms));
}
此题为了防止空间爆炸的问题,也使用了内存回收的技巧,将删掉的点的编号放入一个回收站中,当插入另一个点的时候从回收站里取编号即可。
2022.12.22 ~ 2022.12.24 树套树学习
树套树,顾名思义就是两种数据结构的融合,一般码量较长。
练习题目:
【模板】二逼平衡树(树套树)
操作有:查询区间内一个数的排名、查询区间内排名为 \(k\) 的数、单点修改、求区间内一个数的前驱和后继。
就是线段树套平衡树,线段树维护区间,每个线段树区间内的 \(splay\) 维护区间里的数。
此处不想过多解释了,直接贴代码吧,比较容易理解。(就是太难调了)
#include <iostream>
using namespace std;
const int N=2e6+9,INF=2147483647;
struct node{
int left,right,siz,fa,v;
void init(int father,int vv)
{
fa=father,v=vv;
siz=1;
}
} tr[N];//平衡树
int n,T,L[N],R[N],root[N],idx,a[N];//线段树数组,root表示这个线段树区间内平衡树的根节点
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void pushup(int k)
{
tr[k].siz=tr[tr[k].left].siz+tr[tr[k].right].siz+1;
}
void rotate(int x)
{
int y=tr[x].fa,z=tr[y].fa;
if(tr[z].right==y) tr[z].right=x;
else tr[z].left=x;
tr[x].fa=z;
if(tr[y].left==x) tr[y].left=tr[x].right,tr[tr[x].right].fa=y,tr[x].right=y,tr[y].fa=x;
else tr[y].right=tr[x].left,tr[tr[x].left].fa=y,tr[x].left=y,tr[y].fa=x;
pushup(y),pushup(x);
}
void splay(int &root,int x,int k)
{
while(tr[x].fa!=k)
{
int y=tr[x].fa,z=tr[y].fa;
if(z!=k)
{
if((tr[z].right==y) ^ (tr[y].right==x)) rotate(x);
else rotate(y);
}
rotate(x);
}
if(k==0) root=x;
}
void insert(int &root,int x)
{
int u=root,p=0;
while(u!=0)
{
p=u;
if(x>tr[u].v) u=tr[u].right;
else u=tr[u].left;
}
u=++idx;
if(p!=0)
{
if(x>tr[p].v) tr[p].right=u;
else tr[p].left=u;
}
tr[u].init(p,x);
splay(root,u,0);
}
int get_k(int root,int x)//返回小于x的数的个数
{
int u=root,ans=0;
while(u!=0)
{
if(tr[u].v<x) ans=ans+tr[tr[u].left].siz+1,u=tr[u].right;
else u=tr[u].left;
}
return ans;
}
void update(int &root,int x,int y)//删去x,插入y
{
int u=root;
while(u!=0)
{
if(tr[u].v==x) break;
if(x>tr[u].v) u=tr[u].right;
else u=tr[u].left;
}
splay(root,u,0);
int l=tr[u].left,r=tr[u].right;
while(tr[l].right!=0) l=tr[l].right;
while(tr[r].left!=0) r=tr[r].left;
splay(root,l,0),splay(root,r,l);
tr[r].left=0;
pushup(r),pushup(l);
insert(root,y);
}
int get_pre(int root,int x)//求小于x的最大数
{
int u=root,ans=-INF;
while(u!=0)
{
if(x>tr[u].v) ans=max(ans,tr[u].v),u=tr[u].right;
else u=tr[u].left;
}
return ans;
}
int get_suc(int root,int x)//求大于x的最小数
{
int u=root,ans=INF;
while(u!=0)
{
if(x>=tr[u].v) u=tr[u].right;
else ans=min(ans,tr[u].v),u=tr[u].left;
}
return ans;
}
void build(int k,int l,int r)
{
L[k]=l,R[k]=r;
insert(root[k],-INF);
insert(root[k],INF);
for(int i=l;i<=r;i++) insert(root[k],a[i]);
if(l==r) return;
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
}
int query(int k,int l,int r,int x)//求x的排名
{
if(l<=L[k] && R[k]<=r) return get_k(root[k],x)-1;
int mid=(L[k]+R[k])>>1,ans=0;
if(l<=mid) ans=ans+query(k<<1,l,r,x);
if(mid<r) ans=ans+query(k<<1|1,l,r,x);
return ans;
}
void upd(int k,int pos,int x)
{
update(root[k],a[pos],x);//在平衡树中修改
if(L[k]==R[k]) return;
int mid=(L[k]+R[k])>>1;
if(pos<=mid) upd(k<<1,pos,x);
else upd(k<<1|1,pos,x);
}
int query_pre(int k,int l,int r,int x)
{
if(l<=L[k] && R[k]<=r) return get_pre(root[k],x);
int mid=(L[k]+R[k])>>1,ans=-INF;
if(l<=mid) ans=max(ans,query_pre(k<<1,l,r,x));
if(mid<r) ans=max(ans,query_pre(k<<1|1,l,r,x));
return ans;
}
int query_suc(int k,int l,int r,int x)
{
if(l<=L[k] && R[k]<=r) return get_suc(root[k],x);
int mid=(L[k]+R[k])>>1,ans=INF;
if(l<=mid) ans=min(ans,query_suc(k<<1,l,r,x));
if(mid<r) ans=min(ans,query_suc(k<<1|1,l,r,x));
return ans;
}
int main()
{
n=read(),T=read();
for(int i=1;i<=n;i++) a[i]=read();
build(1,1,n);
while(T--)
{
int opt=read();
if(opt==1)//给值查询排名
{
int a=read(),b=read(),x=read();
printf("%d\n",query(1,a,b,x)+1);
}
else if(opt==2)//给排名查询值
{
int a=read(),b=read(),k=read();
int l=0,r=1e8;
while(l<r)//二分
{
int mid=(l+r+1)>>1;
if(query(1,a,b,mid)+1<=k) l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
else if(opt==3)//插入操作
{
int pos=read(),x=read();
upd(1,pos,x);
a[pos]=x;
}
else if(opt==4)//求小于x的最大数
{
int a=read(),b=read(),x=read();
printf("%d\n",query_pre(1,a,b,x));
}
else//求大于x的最小数
{
int a=read(),b=read(),x=read();
printf("%d\n",query_suc(1,a,b,x));
}
}
return 0;
}