第二周学习总结

第二周学习总结

分块

思想:把长度为 \(N\) 的序列分为若干个长度为 \(S\) 的快。对于每次询问/修改,整块打包处理,零散部分暴力处理。

一般情况况下,当 \(S=\sqrt{n}\) 时,有较好复杂度 \(m \sqrt{n}\)。

模板代码:

[线段树]区间极大值2

#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<cstring>
const int N=1e5+5;
int n,m,id[N],len,a[N],s[N];
void change(int x,int v){
	a[x]+=v;
	s[id[x]]=-2e8;
	for(int i=(id[x]-1)*len+1;i<=id[x]*len&&i<=n;i++) s[id[x]]=std::max(s[id[x]],a[i]);
}
int query(int l,int r){
    int sid=id[l],eid=id[r],ans=-2e8;
    if(sid==eid){
        for(int i=l;i<=r;i++) ans=std::max(ans,a[i]);
        return ans;
    }
    for(int i=l;i<=sid*len;i++) ans=std::max(ans,a[i]);
    for(int i=sid+1;i<eid;i++) ans=std::max(ans,s[i]);
    for(int i=(eid-1)*len+1;i<=r;i++) ans=std::max(ans,a[i]);
    return ans;
}
int main(){
    scanf("%d%d",&n,&m);
    len=(int)sqrt(n);
    for(int i=1;i<=n;i++) s[i]=-2e8;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),id[i]=(i-1)/len+1,s[id[i]]=std::max(s[id[i]],a[i]);
    int x,y,z;
    while(m--){
    	scanf("%d%d%d",&x,&y,&z);
    	if(x==1) change(y,z);
    	else printf("%d\n",query(y,z));
	}
    return 0;
}

例题

[根号]动态区间第K小数

将原序列分为长度为 \(\sqrt{n}\) 的块,块内元素从小到大排序。

修改操作:将 \(a_i\) 修改为 \(t\),然后将第 \(i\) 个元素所在的块进行重组排序。时间复杂度为 \(O(\sqrt{n} \log_2 \sqrt{n})\)。

查询操作:对答案进行二分。每次二分一个 \(mid\) 值,都到询问区间内寻找比 \(mid\) 小的元素个数。整块在块内直接 \(lower \_ bound\) ,零散部分暴力比较。如果小于 \(mid\) 的元素个数恰好为 \(k-1\),则 \(mid\) 即为答案。时间复杂度为 \(O(\sqrt{n} \log_2 \sqrt{n})\)。

综上,时间复杂度为 \(O(m \sqrt{n} \log_2 \sqrt{n})\),可以通过测试。

代码:

#include<stdio.h>
#include<algorithm>
#include<vector>
#include<math.h>
#include<cstring>
const int N=5e4+5;
int n,m,id[N],len,a[N];
std::vector<int> v[N];
void change(int x,int y){
//	printf("%d %d\n",x,y);
//	for(auto i:v[id[x]]) printf("%d ",i);
//	putchar('\n');
	v[id[x]].erase(std::lower_bound(v[id[x]].begin(),v[id[x]].end(),a[x]));
//	for(auto i:v[id[x]]) printf("%d ",i);
//	putchar('\n');
	a[x]=y;
	v[id[x]].emplace(std::lower_bound(v[id[x]].begin(),v[id[x]].end(),y),y);
//	for(auto i:v[id[x]]) printf("%d ",i);
//	putchar('\n');
}
int count(int l,int r,int val){
	int sid=id[l],eid=id[r],ans=0;
	if(sid==eid){
		for(int i=l;i<=r;i++) ans+=(a[i]<val);
		return ans;
	}
	for(int i=l;i<=sid*len;i++) ans+=(a[i]<val);
	for(int i=sid+1;i<eid;i++) ans+=std::lower_bound(v[i].begin(),v[i].end(),val)-v[i].begin();
	for(int i=(eid-1)*len+1;i<=r;i++) ans+=(a[i]<val);
	return ans;
}
int query(int l,int r,int k){
    int L=0,R=50005;
    while(L+1<R){
    	int mid=L+(R-L>>1);
    	if(count(l,r,mid)>=k) R=mid;
    	else L=mid;
	}
    return L;
}
int main(){
//	freopen("oo.in","r",stdin);
    scanf("%d%d",&n,&m);
    len=(int)sqrt(n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),id[i]=(i-1)/len+1,v[id[i]].emplace_back(a[i]);
    for(int i=1;i<=n;i++) if(v[i].size()>0) std::sort(v[i].begin(),v[i].end());
    int x,y,z;
    char ch[2];
    while(m--){
    	scanf("%s",ch);
    	if(*ch=='C') scanf("%d%d",&x,&y),change(x,y);
    	else scanf("%d%d%d",&x,&y,&z),printf("%d\n",query(x,y,z));
	}
    return 0;
}

这道题更高效的解法是可持久化线段树,时间复杂度为 \(O(m \log_2 n)\)。

[根号]子集求和

考虑将集合分为两种。

这里,我们称元素个数少于或等于 \(\sqrt{n}\) 的集合为小集合,元素个数多于 \(\sqrt{n}\) 的集合为大集合。

由于 \(m\) 个集合的元素个数总和不超过 \(10^5\),所以大集合的个数不会超过 \(\sqrt{n}\) 的级别。

设 \(cnt_{i,j}\) 表示第 \(i\) 个集合和第 \(j\) 个集合之间相关联的元素个数(即 \(size(i \thinspace \cap \thinspace j)\)),\(sum_i\) 表示第 \(i\) 个大集合内的元素之和,\(lazy_i\) 表示第 \(i\) 个大集合的下传标记。预处理的时间复杂度为 \(n \sqrt{n}\) 的级别。

将第 \(i\) 个小集合的元素加上 \(v\):将集合 \(i\) 中的元素直接暴力加上 \(v\),再把于 \(i\) 相关联的大集合 \(j\) 的 \(sum_j\) 加上 \(v \times cnt_{i,j}\)。时间复杂度为 \(O(\sqrt{n})\)。

将第 \(i\) 个大集合的元素加上 \(v\):直接将 \(lazy_i\) 的值加上 \(v\)。时间复杂度为 \(O(1)\)。

询问第 \(i\) 个小集合内的元素之和:暴力加上集合内的元素,再将所得结果加上与 \(i\) 相关联的大集合 \(j\) 的 \(lazy_j \times cnt_{i,j}\)。时间复杂度为 \(O(\sqrt {n})\)。

询问第 \(i\) 个大集合内的元素之和:将 \(sum_i\) 加上 \(i\) 相关联的大集合 \(j\) 的 \(lazy_j \times cnt_{i,j}\)。时间复杂度为 \(O(\sqrt {n})\)。

综上,时间复杂度为 \(q \sqrt{n}\),可以通过测试。

代码:

#include<stdio.h>
#include<vector>
#include<math.h>
typedef long long ll;
const int N=1e5+5,M=2e3;
std::vector<ll> v[N],num[N],guan[N];
ll n,m,q,len,a[N],cnt[N][M],sum[N],lazy[N];
int main(){
	scanf("%lld%lld%lld",&n,&m,&q);
	len=(ll)sqrt(n);
	for(ll i=1;i<=n;i++) scanf("%lld",&a[i]);
	for(ll i=1,size;i<=m;i++){
		scanf("%lld",&size);
		if(size<=len) for(ll j=1,x;j<=size;j++) scanf("%lld",&x),v[i].emplace_back(x),sum[i]+=a[x];
		else for(ll j=1,x;j<=size;j++) scanf("%lld",&x),v[i].emplace_back(x),num[x].emplace_back(i),sum[i]+=a[x];
	}
	for(ll i=1;i<=m;i++){
		for(auto j:v[i]){
			for(auto k:num[j]){
				cnt[i][k]++;
				if(cnt[i][k]==1) guan[i].emplace_back(k);
			}
		}
	}
	char ch[2];
	ll x,y;
	while(q--){
		scanf("%s",ch);
		if(*ch=='+'){
			scanf("%lld%lld",&x,&y);
			if(v[x].size()<=len){
				for(auto i:v[x]) a[i]+=y;
				for(auto i:guan[x]) sum[i]+=y*cnt[x][i];
			}
			else lazy[x]+=y;
		}
		else{
			scanf("%lld",&x);
			ll ans=0;
			if(v[x].size()<=len){
				for(auto i:v[x]) ans+=a[i];
				for(auto i:guan[x]) ans+=lazy[i]*cnt[x][i];
			}
			else{
				ans+=sum[x];
				for(auto i:guan[x]) ans+=lazy[i]*cnt[x][i];
			}
			printf("%lld\n",ans);	
		}
	}
	return 0;
}

[根号][APIO2015]雅加达的摩天楼

考虑普通建图与分层图结合使用。

对于跳跃能力 \(p\) 大于 \(\sqrt{n}\) 的 \(\operatorname{doge}\),他能跳到的摩天楼数量 \(\frac{n}{p}\) 一定小于等于 \(\sqrt{n}\),所以直接暴力建边即可。

对于跳跃能力 \(p\) 小于等于 \(\sqrt{n}\) 的 \(\operatorname{doge}\),如果暴力建边,则边数就为 \(O(n^2)\) 的量级,无法接受。所以,我们考虑分层图建边。

对于每栋楼 \(i\),我们都新建 \(\sqrt{n}\) 个相较于 \(i\) 的虚点,\(i_k\) 表示以 \(i\) 为起点,且跳跃长度为 \(k\)。每个虚点都要连接其对应的 \(i\),边权为 \(0\)。

对于 \(i_k\),我们再将其向 \(i_k-k\) 和 \(i_k+k\) 连边,边权为 \(1\),表示从第 \(i\) 个点出发跳 \(1\) 步可以跳到 \(i_k-k\) 和 \(i_k+k\) 的对应实点。

对于在第 \(i\) 栋楼上,跳跃能力为 \(p\) 的 \(\operatorname{doge}\),我们将 \(i\) 向 \(i_p\) 连边,表示从 \(i\) 点出发,\(1\) 步可以跳到 \(i_p\) 所连虚点对应的实点。

最后跑一遍最短路即可。

时间复杂度分析:

对于每只 跳跃能力 \(p\) 大于 \(\sqrt{n}\) 的 \(\operatorname{doge}\),他最多会向外连接 \(\sqrt{n}\) 条边,所以总边数不超过 \(n\sqrt{n}\)。

对于分层图每层点之间连的边,共有 \((n-1)+(n-2)+\cdots+(n-\sqrt{n})=\frac{\sqrt{n}(2n-\sqrt{n}-1)}{2}\) 条边,量级为 \(n \sqrt{n}\) 条。

对于每个虚点,每个虚点都会向其对应实点连边,而虚点有 \(n \sqrt{n}\) 个,所以会连 \(n \sqrt{n}\) 条边。

对于每个实点,每个存在 \(\operatorname{doge}\) 的实点都会向其对应的一个虚点连一条边,所以会连 \(n\) 条边。

综上,边数的量级为 \(n \sqrt{n}\) 级别。使用 \(\operatorname{Dijkstra}\) 算法,时间复杂度为 \(n \sqrt{n} \log_2 n \sqrt{n}\),勉强通过测试。

代码:

#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<queue>
#include<cstring>
const int N=3e4+5,M=174,Q=1.8375e7+5,L=6e6+5;
//(n+1)+n+(n-1)+...+(n-sqrt(n)+2)
struct jie{int to,val,nxt;}a[Q];
int n,m,building[N],len,pp,head[L],cnt,dis[L];
bool vis[Q];
void add(int x,int y,int z){
	cnt++;
	a[cnt].to=y;
	a[cnt].val=z;
	a[cnt].nxt=head[x];
	head[x]=cnt;
}
inline int num(int i,int j){return i*n+j;}
struct node{
	int id,val;
	bool operator < (const node &x) const{return val>x.val;}
};
void Dijkstra(int s){
	for(int i=1;i<=len*n+n;i++) dis[i]=0x3f3f3f3f;
	std::priority_queue<node> q;
	dis[s]=0,q.push({s,dis[s]});
	while(!q.empty()){
		node u=q.top();q.pop();
		if(vis[u.id]) continue;
		vis[u.id]=true;
		for(int i=head[u.id];i;i=a[i].nxt) if(u.val+a[i].val<dis[a[i].to]) dis[a[i].to]=u.val+a[i].val,q.push({a[i].to,dis[a[i].to]});
	}
}
int main(){
	scanf("%d%d",&n,&m);
	len=std::max((int)sqrt(n/3),1);
	/*int */pp=n;
	for(int i=1;i<=len;i++) for(int j=1;j<=n;j++) add(num(i,j),j,0);
	for(int i=1;i<=len;i++) for(int j=1;j<=n;j++) if(i+j<=n) add(num(i,j),num(i,i+j),1),add(num(i,i+j),num(i,j),1);
	for(int i=1,p;i<=m;i++){
		scanf("%d%d",&building[i],&p);
		building[i]++;
		if(p<=len) add(building[i],num(p,building[i]),0)/*,printf("%d %d\n",building[i],num[p][building[i]])*/;
		else{
			for(int j=1;building[i]+p*j<=n;j++) add(building[i],building[i]+p*j,j);
			for(int j=1;building[i]-p*j>=1;j++) add(building[i],building[i]-p*j,j);
		}
	}
	Dijkstra(building[1]);
	if(dis[building[2]]==0x3f3f3f3f) puts("-1");
	else printf("%d",dis[building[2]]);
	return 0;
}

莫队

对于某些区间问题,可以考虑使用莫队求解。

考虑暴力离线求解区间询问。

对于暴力法,我们应该以左端点为第一关键字,右端点为第二关键字。但是,由于询问之间的波动可能很大,所以,暴力法的时间复杂度为 \(O(mn)\),无法通过测试。

但是,我们可以考虑以下优化。

把数组分成长度为 \(\sqrt{n}\) 的若干块,然后把查询的区间按左端点所在块的序号排序,如果左端点的块相同,再按右端点排序。其他步骤与暴力法完全相同。

可证得,不带修莫队的时间复杂度为 \(O(m \sqrt{n} + n \sqrt{n})\)。

模板代码:

[HH的项链](M - HH的项链 | CQNK (nks.edu.cn))

#include<stdio.h>
#include<algorithm>
#include<math.h>
const int N=1e6+5;
int n,m,a[N],id[N],cnt[N],ans[N],ANS,len;
struct node{
	int l,r,k;
	bool operator < (const node &x) const{return id[l]==id[x.l]? (id[l]&1? r>x.r:r<x.r):id[l]<id[x.l];}
}q[N];
void add(int x){ANS+=++cnt[a[x]]==1;}
void del(int x){ANS-=!--cnt[a[x]];}
int main(){
	scanf("%d",&n);
	len=(int)sqrt(n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),id[i]=(i-1)/len+1;
	scanf("%d",&m);
	for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].k=i;
	std::sort(q+1,q+m+1);
	int L=1,R=0;
	for(int i=1;i<=m;i++){
		while(L<q[i].l) del(L++);
		while(R>q[i].r) del(R--);
		while(L>q[i].l) add(--L);
		while(R<q[i].r) add(++R);
		ans[q[i].k]=ANS;
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

带修莫队

与不带修莫队类似,只是多了一维时间维。

注意,由分析可得带修莫队的时间复杂度为 \(O(mS + mS + \frac{m n^2}{S^2})\),其中 \(S\) 表示块长。

当 \(S=n^{\frac{2}{3}}\) 时,有较好时间复杂度为 \(O(m n^{\frac{2}{3}})\)。

若 \(S=\sqrt{n}\),则时间复杂度为 \(O(mn)\),退化为暴力法。

模板代码:

[[莫队]数颜色](N - 【莫队】数颜色 | CQNK (nks.edu.cn))

#include<stdio.h>
#include<algorithm>
#include<math.h>
const int N=1e6+5;
int n,m,a[N],id[N],cnt[N],ans[N],ANS,len,L,R;
struct node{
	int l,r,time,k;
	bool operator < (const node &x) const{return id[l]==id[x.l]? (id[r]==id[x.r]? time<x.time:id[r]<id[x.r]):id[l]<id[x.l];}
}q[N];
struct chan{int wei,old,xin;}change[N];
void add(int x){ANS+=((++cnt[a[x]])==1);}
void del(int x){ANS-=(!(--cnt[a[x]]));}
void update(int x,int y){
	if(L<=x&&x<=R) del(x);
	a[x]=y;
	if(L<=x&&x<=R) add(x);
}
int p1,p2;
int main(){
	scanf("%d%d",&n,&m);
	int len=(int)pow(n,2.0/3.0);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),id[i]=(i-1)/len+1;
	char ch[2];
	int x,y;
	while(m--){
		scanf("%s%d%d",ch,&x,&y);
		if(*ch=='Q') p1++,q[p1]=/**/{x,y,p2,p1};
		else change[++p2]=/**/{x,a[x],y},a[x]=y;
	}
	std::sort(q+1,q+p1+1);
	int T=p2;
	L=1,R=0;
	for(int i=1;i<=p1;i++){
//		printf("%d %d %d\n",q[i].l,q[i].r,q[i].time);
		while(L<q[i].l) del(L++);
		while(R>q[i].r) del(R--);
		while(L>q[i].l) add(--L);
		while(R<q[i].r) add(++R);
		while(T<q[i].time) T++,update(change[T].wei,change[T].xin);
		while(T>q[i].time) update(change[T].wei,change[T].old),T--;
		ans[q[i].k]=ANS;
	}
	for(int i=1;i<=p1;i++) printf("%d\n",ans[i]);
	return 0;
}

矩阵

一个 \(m\) 行 \(n\) 列的矩阵(记作 \(m \times n\))的矩阵用二维数组 \(ma[][]\) 存储,其中 \(ma[i][j]\) 表示第 \(i\) 行第 \(j\) 列的元素的值。

矩阵加减法

把两个矩阵对应位置的元素相加减即可。要求两个矩阵的行、列数相同。

矩阵乘法

\(1.\) 一个数 \(k\) 程矩阵 \(A\),把 \(k\) 乘以矩阵的每个元素,记作 \(kA\)。

\(2.\) 两个矩阵 \(A\) 和 \(B\) 相乘,要求 \(A\) 的列数等于 \(b\) 的行数。设 \(A\) 的尺寸为 \(m \times n\),\(B\) 的尺寸为 \(n \times k\),那么乘积 \(C=AB\) 的尺寸为 \(m \times k\)。矩阵乘法 \(C=AB\) 的计算公式为:\(c[i][j]=\sum \limits _{k=1}^{n}A[i][k] \times B[k][j]\)。计算时间复杂度为 \(O(mnk)\)。

矩阵模板

struct matrix{
	ll n,m;
	ll ma[N][N];
	matrix(ll p=0,ll q=0){
		n=p,m=q;
		for(ll i=1;i<=n;i++) for(ll j=1;j<=m;j++) ma[i][j]=0;
	}
	matrix operator + (const matrix &x) const{
		matrix c=matrix(n,m);
		for(ll i=1;i<=n;i++) for(ll j=1;j<=m;j++) c.ma[i][j]=(ma[i][j]+x.ma[i][j])%mod;
		return c;
	}
	matrix operator * (const matrix &x) const{
		matrix c=matrix(n,x.m);
		for(ll i=1;i<=n;i++)
			for(ll k=1;k<=m;k++)
				if(ma[i][k])
				    for(ll j=1;j<=x.m;j++)
						c.ma[i][j]=(c.ma[i][j]+ma[i][k]*x.ma[k][j]%mod)%mod;
		return c;
	}
}A,f;//mod为模数
posted @ 2024-01-11 17:10  gevenfeng  阅读(39)  评论(0)    收藏  举报