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;
}
View Code

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;
}
View Code

 

并查集

启发式合并

对每个集合维护一个大小,每次把小的集合的代表元素的父亲设为大的集合元素的代表元素,这样每跳一步,其子树大小就会至少翻倍,每个点任意时刻到根的路径的长度都是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;
}
View Code

关押罪犯

#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;
}
View Code

存储:vector,链式前向星

倍增求LCA

一个跟链有关的结论:

有一个在某些题中很重要的性质是,两条链的交集还是一条链,并且新链的端点的LCA是原先某条链的端点的LCA。

重链剖分和长链剖分

哈哈哈哈哈哈不会。

 

posted @ 2019-08-11 15:14  sdzmq  阅读(182)  评论(0编辑  收藏  举报