算法进阶

树状数组


求一个数组\(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

\(N ≤ 100000\)

分治+树状数组+\(dp\)

\(f[i]\)表示到\(i\)为止的最长上升子序列,则\(f[i]=max_{j,分治寻找最大的\(j\)

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

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

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

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

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

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

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

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

算法进阶_第1张图片

\(CODE \ from \ fz:\)

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#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[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\n",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]\)用新数组的下标就可以得到答案了:如下:

算法进阶_第2张图片

然后:???????

分块

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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#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;--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\n",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\)次,则总复杂度不超过\(N\sqrt N\).
对于右端点,在每一块都会至多花费\(O(N)\)的时间从头扫到尾再从尾扫到头,这个时候总复杂度为\(O(N\sqrt N)\).

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#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/Blockl) --ansl,Add(A[ansl]);
                while (ansrr) Del(A[ansr]),--ansr;
                Ans[Q[i].id]=S;
        }
}
#include

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.lr) Delete(a[R]),R--;
        while(L>l) L--,Add(a[L]);
        while(R

区间第\(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(N\sqrt N).mex\)这个东西是非常有用的.

树上莫队

反 正 我 没 听 懂

你可能感兴趣的:(算法进阶)