算法进阶
树状数组
略
求一个数组\(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\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]\)用新数组的下标就可以得到答案了:如下:
然后:???????
分块
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\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<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\n",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(N\sqrt N).mex\)这个东西是非常有用的.
树上莫队
反 正 我 没 听 懂