算法进阶

Posted zhuier-xquan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法进阶相关的知识,希望对你有一定的参考价值。

树状数组


求一个数组(A_1,A_2……A_n)的逆序对数(树状数组做

(n ≤ 100000, |A_i| ≤ 10^9)

我们将(A_1, ..., A_n)按照大小关系变成(1...n).这样数字的大小范围在([1, n])中.(离散化)
从左往右动态维护一个数组(B_i),表示扫描到当前位置有多少个数的大小正好是(i)
从左往右扫描每个数,对于(A_i),累加(B_{A_i+1}...B_n)的和,同时将(B_{Ai})(1).
时间复杂度为(O(N log N)).

链接

分治进阶

(CDQ)分治

(CDQ)分治的思想是用一个子问题来计算对另一个子问题的贡献。
我们注意到在分治的过程中,与(i)无关的([1,i-1])([i+1,n])会由于分割而慢慢地被分离开。对于j对i的贡献,我们在(j)(i)被分开的时候去计算.
我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间([L,R])表示。
分。递归处理左边区间([L,M])和右边区间([M+1,R])的问题。
治。合并两个子问题,同时考虑到([L,M])内的修改对([M+1,R])内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。   
这就是(CDQ)分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,([L,M])内的问题不会对([M+1,R])内的问题产生影响。


三维(LIS)

给定数组(A, B,)求最长的子序列,满足(?i < j,A_i<A_j,B_i<B_j)

(N ≤ 100000)

分治+树状数组+(dp)

(f[i])表示到(i)为止的最长上升子序列,则(f[i]=max_{j<i,A_j<A_i,B_j<B_i}f[j]+1),分治寻找最大的(j)

将一个大的区间拆解为两个小区间,先递归处理左区间,然后通过左区间修改右区间,接着再递归处理右区间:

(1.)前提是左区间的(f[i])都已经处理好了

(2.)我们将左右区间按照(A[i])升序排列

(3).那么对于右区间的一个(A[i]),我们一定可以找到一个从左区间左侧开始连续的一段区间,这段区间的每一个(A[j])都满足(A[j]<A[i]);

(4).然后将所有满足的(A[j])的相关信息放入一个树状数组中:

树状数组中以(B[])的值为下标(这里假装离散化过),以(f[])的值为树状数组维护的权值

(5.)如何求得(f[i]):在树状数组中寻找最大的下标在(1)~(B[i]-1)之间的(f[j]),更新(f[i])

(6.)不要着急:为了做到树状数组重复使用,记得每次清空树状数组

技术图片

(CODE from fz:)

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<ctime>
#include<set>
#include<vector>
#include<map>
#include<queue>

#define N 300005
#define M 8000005

#define ls (t<<1)
#define rs ((t<<1)|1)
#define mid ((l+r)>>1)

#define mk make_pair
#define pb push_back
#define fi first
#define se second

using namespace std;

int i,j,m,n,p,k,f[N],tree[N],id[N],a[N],b[N];

inline int cmp(int x,int y)
{
        return a[x]<a[y];
}

int lowbit(int x)
{
    return x&-x;
}

void ins(int x,int y)
{
        for(;x<=n;x+=lowbit(x)) tree[x]=max(tree[x],y); 
}

void Clear(int x)
{
        for(;x<=n;x+=lowbit(x)) tree[x]=0; 
}   

int ask(int x)
{
        int s=0;
        for (;x;x-=lowbit(x)) s=max(s,tree[x]);
        return s;
}

void Work(int l,int r)
{
        if (l==r) f[l]+=1;
        else
        {
                Work(l,mid);
                for (i=l;i<=r;++i) id[i]=i;
                sort(id+l,id+mid+1,cmp);
                sort(id+mid+1,id+r+1,cmp);
                int L=l;
                for (i=mid+1;i<=r;++i)
                {
                        int p=id[i];
                        while (L<=mid&&a[p]>a[id[L]]) ins(b[id[L]],f[id[L]]),++L;
                        f[p]=max(f[p],ask(b[p]-1));
                }
                for (i=l;i<=mid;++i) Clear(b[id[L]]);
                Work(mid+1,r);
        }
}

int main()
{
        scanf("%d",&n);
        for (i=1;i<=n;++i) scanf("%d",&a[i]);
        for (i=1;i<=n;++i) scanf("%d",&b[i]);
        Work(1,n); 
        for (i=1;i<=n;++i)f[0]=max(f[0],f[i]);
        printf("%d
",f[0]);
}

整体二分

区间第K大值

给定长度为(N)的序列(A,Q)组询问,每组询问给出(li,ri, ki),询问第(li)个数到第(ri)个数之间第(ki)大的值是多少。
(N, Q ≤ 100000, |Ai| ≤ 10^9)

二分第(K)大值为(mid),判断区间([l,r])(geq mid)的数的个数(ans),如果(ans< ki),则说明答案在([-∞,mid])之间,否则在([mid,+∞])之间。

现在考虑怎么快速计算(geq mid)的个数:

一个显然的想法是我们每次找出大于mid的所有数,然后排序之后通过两次二分找到([l,r])用新数组的下标就可以得到答案了:如下:

技术图片

然后:???????

分块

Counter

给出一个长度为(N)的整数序列(A),有(Q)组询问,第(i)组询问给出(li,ri),询问(li)(ri)之间有多少个不同的数.
(N ≤ 100000)

首先我们对整个序列进行离散化,使得所有的数都落在(1)(N)之内。然后对序列分块
我们先预处理第一个信息,令(F_{i,j})表示前(i)块里面,数字(j)出现的次数.
然后我们考虑一个(G_{i,j}),表示第(i)块到第(j)块里不同数字的个数。那么(G_{i,j} = G_{i,j?1} + Work(i, j ? 1, j).)
其中(Work(i, x, y))表示的是在第(i)块到第(x)块的末尾加入第(y)块增加的不同数个数。

(CODE from fz:)

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<ctime>
#include<set>
#include<vector>
#include<map>
#include<queue>

#define N 300005
#define M 8000005

#define ls (t<<1)
#define rs ((t<<1)|1)
#define mid ((l+r)>>1)

#define mk make_pair
#define pb push_back
#define fi first
#define se second

using namespace std;

int i,j,m,n,p,k,ans,a[N],S;

int main()
{
        scanf("%d",&n);
        for (i=1;i<=n;++i) scanf("%d",&a[i]);
        int S=(int)sqrt(n);
        for (i=1;i<=n;i=ed[tot]+1)
        {
                st[++tot]=i;
                ed[tot]=min(n,i+S-1);
        }
        for (i=1;i<=tot;++i) //sqrt(n) 
        {
                for (j=1;j<=n;++j) F[i][j]=F[i-1][j]; //O(n) 
                for (j=st[i];j<=ed[i];++j) F[i][a[j]]++; //sqrt(n)
        }
        //[i..j-1] a[k] F[j-1][a[k]]-F[i-1][a[k]]
        for (i=1;i<=tot;++i) //sqrt(n)
            for (j=i;j<=tot;++j) //sqrt(n)
            {
                    G[i][j]=G[i][j-1]; 
                    for (k=st[j];k<=ed[j];++k) //sqrt(n) 
                        if (!(F[j-1][a[k]]-F[i-1][a[k]])&&!vis[a[k]]) G[i][j]++,vis[a[k]]=1;
                    for (k=st[j];k<=ed[j];++k) vis[a[k]]=0;
            }
        for (Q=1;Q<=m;++Q)
        {
                scanf("%d%d",&l,&r);
                int left=1,right;
                //[left,right] 
                if (r-l<=2*S)
                {
                        baoli(l,r);
                        continue;
                }
                while (st[left]<l) ++left;
                while (ed[right]<=r) ++right; --right;
                ans=G[left][right];
                for (i=st[left]-1;i>=l;--i)
                {
                        if (!(F[right][a[i]]-F[left-1][a[i]])&&!vis[a[i]]) ans++,vis[a[i]]=1;
                }
                for (i=ed[right]+1;i<=r;++i)
                {
                        if (!(F[right][a[i]]-F[left-1][a[i]])&&!vis[a[i]]) ans++,vis[a[i]]=1;
                } 
                printf("%d
",ans);
                for (i=st[left]-1;i>=l;--i)
                {
                        vis[a[i]]=0;
                }
                for (i=ed[right]+1;i<=r;++i)
                {
                        vis[a[i]]=0;
                } 
        }
}

基础莫队

莫队常用于各种区间相关的离线问题,其本质非常的暴力.

我们现在有一个区间([L, R])的答案,并且维护了这个区间的一些信息,现在我们要改求([L′, R′])的答案。
那很简单,只要我们暴力的把(L)一步一步地移动到(L′),再把(R)一步一步移动到(R′)就可以辣.
但是如果我们直接暴力总复杂度肯定不对,所以要求离线,用一些分析去把复杂度降低.在之后的讨论里,我们认为序列长度N和询问次数(Q)是同阶的.

(HH)的项链

给出一个长度为(N)的整数序列(A),有(Q)组询问,第(i)组询问给出(l_i,r_i),询问(l_i)(r_i)之间有多少个不同的数.
(N ≤ 100000.)

1.离散化

2.序列维护每种数在([L, R])里面出现了多少次,当左右端点移动时,如果要加入一个数就对应加,否则对应减.当(cnt)(1)变成(1)时,就把(Ans)(1),当(cnt[x])(1)变成(0)时,就把(Ans)(1).

如何来调整区间访问顺序使得总变化次数较少?

我们将序列按(sqrt N)分块,然后所有的询问先按照左端点所在的块排序,如果左端点所在块相同就按照右端点从小往大排序,这样就可以了.

我们分析移动次数。左端点的移动次数,如果是在同一块内,则单次不超过(sqrt N),否则如果是跨块,由于这样的次数不超过(sqrt N)次,则总复杂度不超过(Nsqrt N).
对于右端点,在每一块都会至多花费(O(N))的时间从头扫到尾再从尾扫到头,这个时候总复杂度为(O(Nsqrt N)).

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<ctime>
#include<set>
#include<vector>
#include<map>
#include<queue>

#define N 300005
#define M 8000005

#define ls (t<<1)
#define rs ((t<<1)|1)
#define mid ((l+r)>>1)

#define mk make_pair
#define pb push_back
#define fi first
#define se second

using namespace std;

int i,j,m,n,p,k,ansl,ansr,S,Sum[N],Ans[N],Block,A[N],l,r;

struct Node{
        int l,r,id;
}Q[N];

void Add(int x)
{
        Sum[x]++;
        if (Sum[x]==1) S++;
}

void Del(int x)
{
        Sum[x]--;
        if (Sum[x]==0) S--;
}

inline int cmp(Node a,Node b)
{
        if ((a.l-1)/Block!=(b.l-1)/Block) return a.l/Block<b.l/Block;
        return a.r<b.r;
}

int main()
{
        scanf("%d%d",&n,&m);
        for (i=1;i<=n;++i) scanf("%d",&A[i]);
        Block=(int)sqrt(n);
        ansl=ansr=1; S=1;
        for (i=1;i<=m;++i) scanf("%d%d",&Q[i].l,&Q[i].r),Q[i].id=i;
        sort(Q+1,Q+m+1,cmp);
        for (i=1;i<=m;++i)
        {
                l=Q[i].l,r=Q[i].r;
                while (ansl>l) --ansl,Add(A[ansl]);
                while (ansr<r) ++ansr,Add(A[ansr]);
                while (ansl<l) Del(A[ansl]),++ansl;
                while (ansr>r) Del(A[ansr]),--ansr;
                Ans[Q[i].id]=S;
        }
}
#include<bits/stdc++.h>

using namespace std;

inline int read() {
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar(); 
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}const int mxn=1000010;

int n,m,B,Ans;
int a[mxn];
int cnt[mxn],ANS[mxn];

struct node {
    int l,r,id;
}b[mxn];

bool cmp(node a,node b) {
    if((a.l-1)/B!=(b.l-1)/B) 
        return a.l<b.l;
    return a.r<b.r;
}

void Add(int x) {
    cnt[x]++;
    if(cnt[x]==1) Ans++;
}

void Delete(int x) {
    cnt[x]--;
    if(cnt[x]==0) Ans--;
}

int main() {
    n=read();
    for(int i=1;i<=n;i++) 
        a[i]=read();
    B=sqrt(n);
    m=read();
    for(int i=1;i<=m;i++) {
        b[i].l=read();
        b[i].r=read();
        b[i].id=i;
    }
    sort(b+1,b+m+1,cmp);
    int L=1,R=1;Ans=1;cnt[a[1]]++;
    for(int i=1,l,r;i<=m;i++) {
        l=b[i].l;r=b[i].r;
        while(L<l) Delete(a[L]),L++;
        while(R>r) Delete(a[R]),R--;
        while(L>l) L--,Add(a[L]);
        while(R<r) R++,Add(a[R]);
        ANS[b[i].id]=Ans;
    }
    for(int i=1;i<=m;i++)
        printf("%d
",ANS[i]);
    return 0;
}

区间第(K)大????

给出一个序列(A,Q)次询问,每次询问在([Li, Ri])中第(K)大的数是多少.(N, Q ≤ 100000,)为了方便,之后的题目中序列数值大小总不超过(N).
(N, Q ≤ 100000.)

(gugugu)

区间(mex)

给出一个序列(A,Q)次询问,每次询问在([Li, Ri])(mex)是多少.
(N, Q ≤ 100000.)
一堆数的(mex)指的是不在这些数里出现的最小的自然数.

维护每个数的出现次数,询问的时候从小往大依次判断每个块是否是满的,如果不是满的则暴力扫描.然后从0开始暴力扫描第一个为0的出现次数,即为([L_i,R_i])(mex)

总复杂度为(O(Nsqrt N).mex)这个东西是非常有用的.

树上莫队

反 正 我 没 听 懂

以上是关于算法进阶的主要内容,如果未能解决你的问题,请参考以下文章

Atom编辑器入门到精通 Atom使用进阶

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段

Atom编辑器入门到精通 Atom使用进阶