2019 CCF夏令营 day 2
堆,并查集,加权并查集,树链剖分(重链,长链),lca。
堆
单次严格o(lgn)插入、删除最小(最大)的数字,o(1)询问最小(最大)的数字,实践中一个点常用x*2,x*2+1,x/2代表其左右儿子和父亲。
删除任意数字
除了插入/删除堆顶,还要实现删除堆中任意数字(要保证其一定在堆中)
用一个堆来维护需要删除的数字,每次取堆顶时看看这个元素是否被删除了即可。
堆排序 o(nlgn)
带插入的中位数
每次插入一个数字,然后询问所有数字的中位数
维护一个大根堆和一个小根堆,维护两堆之间数量之差不超过1,o(nlgn)
行有序数表第k大
有一个n*m的数表,每一行数字从小到大。询问这个数表中第k小的数字,要求(忽略读入复杂度)复杂度O((n+k)lgn)。
每次从某行中取一个目前的最小值,用一个堆维护每行的开头,每次取出当前最小值删掉,若这一行还有数字就再加进去。
第k小点对和
给你两个序列a,b,求a(x)+b(y)的第k小。要求O((n+k)lgn)
与上一个相似,a(x)作为一行数表,b(y)作为另一行数表。
t1 合并石子
洛谷1090
#include <bits/stdc++.h> using namespace std; int n,u,ans; priority_queue< int ,vector<int>,greater<int> >q; int main(){ cin>>n; for(int i=1;i<=n;i++) { cin>>u; q.push(u); } while(q.size()>=2) { int x=q.top(); q.pop(); int y=q.top(); q.pop(); ans+=x+y; q.push(x+y); } cout<<ans; return 0; }
t2
一条街上有n个白点,坐标依次是x_1~x_n; 有个人一开始在0。选择第i个点涂黑要付出a_i的代价(必须走到这个点)。最后必须回到0。 选出某k个点涂黑的代价是这k个点的a的和,加上两倍的坐标最大值。 对每个k=1…n求,涂黑恰好k个不同的点的最大代价。 n<=100000
(此题洛谷2672推销员,先默默骂一下,题目给改的乱七八糟)
先搞出1的情况,可以见得2的情况为,(1.)2在1左面,则为s1+a1+a2,(2.)在1右面,则为s2+a1+a2
说
s2>s1;
所以最好选右面的;用两个堆,一个堆放s*2+a,一个放a,对应两种情况,维护一个堆序号在当前最远右面,一个在左面。
#include <iostream> #include <cstdio> #include <queue> #include <algorithm> using namespace std; priority_queue < pair<int,int> > q1; priority_queue <int> q2; struct llo{ int s,a; } e[100002]; int now,n,q11,q22,ans; bool cmp(llo x,llo y){return x.s<y.s;} int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&e[i].s); for(int i=1;i<=n;i++) scanf("%d",&e[i].a); sort(e+1,e+n+1,cmp); for(int i=1;i<=n;i++) q1.push(make_pair(e[i].s*2+e[i].a,i)); now=0; for(int i=1;i<=n;i++){ q11=q22=0; if(!q2.empty()) q22=q2.top(); while(!q1.empty()&&q1.top().second<=now) q1.pop(); if(!q1.empty()) q11=q1.top().first; if(q22<=q11-e[now].s*2){ ans+=q11-e[now].s*2; for(int j=now+1;j<q1.top().second;j++) q2.push(e[j].a); now=q1.top().second; q1.pop(); } else{ ans+=q22; q2.pop(); } printf("%d\n",ans); } return 0; }
并查集
启发式合并
对每个集合维护一个大小,每次把小的集合的代表元素的父亲设为大的集合元素的代表元素,这样每跳一步,其子树大小就会至少翻倍,每个点任意时刻到根的路径的长度都是o(lgn)的。
把小的弄到大的上去的操作就叫做启发式合并。
按秩合并
每个集合维护深度最大,每次把深度小的挂到深度大的上去。
显然新的集合深度变大1当且仅当原先两个集合深度相等,因此可以归纳证明每次最大深度+1,集合大小也会翻倍,复杂度和上一个一样
路径压缩
太简单了不说了
复杂度是基于均摊的(就是尽管某些单步代价很高但是总代价不大),我不会证明,据说是O(n+log_{1+m/n} (n或者m))的,总之也是O(nlgn)级别的。
Tarjan很牛的证明了当路径压缩和按秩合并一块用的时候复杂度是O(n\alpha(n))的,其中\alpha(n)是阿克曼的某个反函数,增长慢到大概你取n是全宇宙的原子总数,这玩意也不超过6大概。
加权并查集
本质上就是并查集中每个点到父亲的边有一个权值,表示原图中这个点到父节点的某些信息。
或者这个点到父节点的某些信息。
kruskal算法
o(mlgn)它有个十分重要的性质叫环切性质,对于一条没有被选中的边(称非树边),会和选中的边(树边)形成恰好一个简单环,那么这条非树边的权值一定比所有树边的权值大
借由这个性质,假设kruskal求出的树不是最优的最小生成树,那么考虑第一条加错的边,其在最优的最小生成树中对应的环,一定存在一条边权值比它大,这样就不满足环切性质了,也就是一定可以做一个替换使得这个最小生成树更小,矛盾。 因此kruskal求出的是最小生成树。 顺带一提,若要求两个点的一条路径使得边权最大值最小,那么使用最小生成树上的路径一定最优。 因为本质上就是从小到大加边到第一次二者连通,就是kruskal求出的东西。也可以用环切性质说明。
星球大战(早忘了几百年了)
#include <iostream> #include <cstdio> #include <vector> #define MA 400002 using namespace std; int n,m,a[MA],b[MA],f[MA],t,star[MA],broken[MA],iis[MA],tot; vector <int> son[400002]; int find(int x){ if(f[x]!=x) f[x]=find(f[x]); return f[x]; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ scanf("%d%d",&a[i],&b[i]); son[a[i]].push_back(b[i]); son[b[i]].push_back(a[i]); } for(int i=0;i<n;i++) f[i]=i; scanf("%d",&t); for(int i=1;i<=t;i++){ scanf("%d",&star[i]); broken[star[i]]=-1; } tot=n-t; for(int i=1;i<=m;i++){ int x=a[i],y=b[i]; if(broken[x]==-1||broken[y]==-1) continue; else{ int fx=find(x); int fy=find(y); if(fx!=fy){ f[fx]=fy; tot--; } } } iis[t+1]=tot; for(int i=t;i>=1;i--){ //printf("%d ",broken[2]); tot++; //cout<<endl; broken[star[i]]=0; for(int j=0;j<son[star[i]].size();j++){ int x=star[i],y=son[star[i]][j]; if(broken[y]==-1) continue; //printf("%d %d\n",x,y); int fx=find(x); int fy=find(y); if(fx!=fy){ f[fy]=fx; tot--; } } iis[i]=tot; } for(int i=1;i<=t+1;i++) printf("%d\n",iis[i]); return 0; }
关押罪犯
#include <bits/stdc++.h> using namespace std; struct ll{ int mm,enemy,w; } en[2000008]; int n,m,f[200009]; bool cmp(ll x,ll y){ return x.w>y.w; } int find(int x){ if(f[x]!=x) f[x]=find(f[x]); return f[x]; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=2*n;i++) f[i]=i; for(int i=1;i<=m;i++){ int a,b,c; scanf("%d%d%d",&a,&b,&c); en[i].enemy=b; en[i].mm=a; en[i].w=c; } int ff=0; sort(en+1,en+m+1,cmp); for(int i=1;i<=m;i++){ int x=en[i].mm,y=en[i].enemy; if(find(x)==find(y)||find(x+n)==find(y+n)){ ff=en[i].w; break; } f[f[x]]=f[y+n]; f[f[x+n]]=f[y]; } printf("%d",ff); return 0; }
树
存储:vector,链式前向星
倍增求LCA
一个跟链有关的结论:
有一个在某些题中很重要的性质是,两条链的交集还是一条链,并且新链的端点的LCA是原先某条链的端点的LCA。
重链剖分和长链剖分
哈哈哈哈哈哈不会。