zoukankan      html  css  js  c++  java
  • 算法进阶

    树状数组


    求一个数组(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)这个东西是非常有用的.

    树上莫队

    反 正 我 没 听 懂

  • 相关阅读:
    js 导航栏多项点击显示下拉菜单代码
    当你工作与生活迷茫时可以来看看 shuke
    vs code使用指南
    两列
    三列浮动中间宽度自适应
    两列右列宽度自适应
    word文档巧替换(空行替换、空格替换、软回车替换成硬回车)
    统计单元格内指定的字符数方法 ,方法 一好用
    umi ui 构建时出现 spawn sh ENOENT 报错的解决方法
    新的博客,声明一下以前的域名作废了
  • 原文地址:https://www.cnblogs.com/zhuier-xquan/p/12254117.html
Copyright © 2011-2022 走看看