11.14-11.29小结(一)

最近好忙,总算抽出时间写一写总结了。

上上周学了分块,然后复习了下主席树,上周学了莫队+带修莫队,然后这部分题目难度确实有点大,消化起来比较费劲,做的也比较慢,这学期明天再考一门课的期中考试,后面基本上就是摆烂+消化为主了。

1.数列分块入门

1.1问题引入

给定一个序列,长度为n,序列有n个元素,有m种操作,操作1:给定区间x,y,对区间的每个数加上k。操作2:给定区间x,y,求区间[x,y]的和。(咦,这道题不是线段树的模板题吗,你糊弄谁呢)

我们今天要讲的是另外一种做法,分块做法。我们首先考虑暴力修改,则每次查询是O(n)的,每次修改也是O(n)的,m次询问,O(nm)的复杂度显然是难以接受的。考虑将数组分成B块,则每个块的大小是\frac{n}{B}。首先考虑单次操作,若l,r在同一个块中,直接暴力修改,复杂度是O(\frac{n}{B})的,若l,r不在同一个块中,首先从l到该块的块尾,直接暴力修改,从r这个块的块头到r也直接暴力修改,这部分复杂度是O(\frac{n}{B})的。然后考虑中间的整块如何处理,我们可以沿用线段树加懒标记的思想,不要动中间块的具体元素,对块打上lazy标记即可,复杂度是O(B)的。接下来我们考虑询问操作,首先如果在同一个块,暴力记录答案,注意记录的答案应该是a[i]+lazy[id[i]]。其中id[i]表示点i所在的块的索引。如果不在同一个块内,对于左右两边的散块暴力记录答案,对于中间的块,我们在分块时先记录该块的值,后面再加上lazy[i]*块长即可。总复杂度是O(m(B+\frac{n}{B}))的,根据高中数学知识我们不难发现,B取\sqrt{n}时,能达到理论最优复杂度,这时总复杂度是O(m\sqrt{n})。复杂度看似比较高,远高于线段树的O(mlogn),那是不是说明分块就没啥用呢?分块又被叫做优雅的暴力,处理问题实际上是比较暴力的,泛用性也更强。

我写了这么多应该非常枯燥,下面直接放代码吧

1.2代码实现

这题是LOJ的#6277。这题是单点查询,其实差不多,细节什么的看注释吧.

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long 
using namespace std;
const int N=5e5+10;
const int S=1010;
int n,sq,st[S],ed[S],id[N];//原数组
ll lazy[S],a[N];//与块相关
void build(){
	sq=sqrt(n);//块的数量
	for(int i=1;i<=sq;i++){
		st[i]=n/sq*(i-1)+1;
		ed[i]=n/sq*i;
	}
	ed[sq]=n;//强行规定最后一块的结束位置 
	for(int i=1;i<=sq;i++){
		for(int j=st[i];j<=ed[i];j++){
			id[j]=i;//记录每个位置在哪个块 
		}
	}
}
void add(int l,int r,int k){
	//l~r+k
	if(id[l]==id[r]){
		//在一个块中 就暴力扫一遍
		for(int i=l;i<=r;i++)a[i]+=k; 
	}
	else{
		for(int i=l;i<=ed[id[l]];i++)a[i]+=k;
		for(int i=st[id[r]];i<=r;i++)a[i]+=k;
		for(int i=id[l]+1;i<id[r];i++)lazy[i]+=k;
	}
}
void query(int x){
	printf("%lld\n",a[x]+lazy[id[x]]);
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	build();//建块
	for(int i=1;i<=n;i++){
		int opt;
		cin>>opt;
		if(opt==0){
			int l,r,k;
			cin>>l>>r>>k;
			add(l,r,k);
		}else{
			int x;
			int o;
			cin>>o>>x>>o;
			query(x);
		}
	}
	return 0;
}

1.3洛谷P2801

题意:一个序列,维护两种操作。第一种,给定x,y,对区间[x,y]的每个数加上x。第二种,查询区间[x,y]≥c的数的个数。

操作一和例题是一样的,关于如何处理区间[x,y]中≥c的个数,这用到了分块中一种很常见的思想(应该吧),那就是预处理,我们在建块的时候,首先对每个块的元素分别从小到大进行排序,这样每个块内的元素就是有序的,在查询的时候可以二分。对于修改操作,我们需要在暴力修改的部分进行整块重构,即排个序,而中间的整块直接打上lazy标记就不要动了。注意,二分的时候一定是判断a[i]+lazy[id[i]],进行二分。下面直接贴个代码。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long 
using namespace std;
const int N=5e5+10;
const int S=1010;
//原数组
int n,sq,st[S],ed[S],id[N];
ll lazy[S],a[N];//与块相关
void build(){
	sq=sqrt(n);//块的大小
	for(int i=1;i<=sq;i++){
		st[i]=n/sq*(i-1)+1;
		ed[i]=n/sq*i;
	}
	ed[sq]=n;//强行规定最后一块的结束位置 
	for(int i=1;i<=sq;i++){
		for(int j=st[i];j<=ed[i];j++){
			id[j]=i;//记录每个位置在哪个块 
		}
	}
}
void add(int l,int r,int k){
	//l~r+k
	if(id[l]==id[r]){
		//在一个块中 就暴力扫一遍
		for(int i=l;i<=r;i++)a[i]+=k; 
	}
	else{
		for(int i=l;i<=ed[id[l]];i++)a[i]+=k;
		for(int i=st[id[r]];i<=r;i++)a[i]+=k;
		for(int i=id[l]+1;i<id[r];i++)lazy[i]+=k;
	}
}
void query(int x){
	printf("%lld\n",a[x]+lazy[id[x]]);
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	build();//建块
	for(int i=1;i<=n;i++){
		int opt;
		cin>>opt;
		if(opt==0){
			int l,r,k;
			cin>>l>>r>>k;
			add(l,r,k);
		}else{
			int x;
			int o;
			cin>>o>>x>>o;
			query(x);
		}
	}
	return 0;
}

1.4洛谷P4168

这道题好像是LOJ的分块入门9,应该算是简单分块中的一个大BOSS了,这题是强制在线的,不用担心,分块就是一种强制在线的算法。

题意:给定序列n和询问次数m,每次询问给定l,r,强制在线后求[l,r]的最小的众数。这题方法很多,既然学了分块那就用分块来解吧。

考虑在分块的时候预处理,cnt[i]记录i出现的次数,f[x][y]是区间[x,y]出现的最小的众数。考虑将每个数出现的位置扔进一个vector数组里存起来,方便查询[l,r]出现的最小众数。在预处理的过程中,若cnt[a[i]]>mode(mode记录当前众数)或cnt[a[i]]==mode且a[i]<mode时则替换,这样预处理完成了,查询时,最终的答案一定是从两边的答案或者中间的众数,用vector二分找一下出现的次数,复杂度O(m\sqrt{n}logn),要注意的是,这道题数据范围是1~1e9的,所以我们要离散化处理一下,用map会T。注意一定不要把离散化权值和绝对权值搞错了,我在这里WA了好几次。接下来直接看代码。这道题还有个变种是洛谷P4135,两个题的做法思路非常像,所以我不放在这次总结里了。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
const int N= 4e5+10;
const int S= 700;
vector<int>v[N];//用于离散化 
inline int read()
{
	int x=0,y=1;char c=getchar();//y代表正负(1.-1),最后乘上x就可以了。
	while (c<'0'||c>'9') {if (c=='-') y=-1;c=getchar();}//如果c是负号就把y赋为-1
	while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
	return x*y;//乘起来输出
}
int n,m,a[N],p[N],val[N],res;//p是相对大小,val是用来返回真实大小的 
int st[S],ed[S],id[N],sq,f[S][S],idx,cnt[N];//f是预处理两个块中间的众数
map<int,int>mp;
void build(){
	sq=sqrt(n);
	for(int i=1;i<=sq;i++){
		st[i]=n/sq*(i-1)+1;
		ed[i]=n/sq*i;
	}
	ed[sq]=n;
	for(int i=1;i<=sq;i++)
		for(int j=st[i];j<=ed[i];j++)
			id[j]=i;
	for(int i=1;i<=sq;i++){//从每一块的左端点记录到右端点 记录从i块到j块的众数
		memset(cnt,0,sizeof cnt);
		int mode=0,num=0;//众数和众数的数量 
		for(int j=st[i];j<=n;j++){
			int x=i;
			int y=id[j];
			cnt[p[j]]++;
			if(cnt[p[j]]>num||((cnt[p[j]]==num)&&val[p[j]]<val[mode])){
				mode=p[j];
				num=cnt[p[j]];
			}
			f[x][y]=mode;//记录区间众数 
		}
	}
}
int Num(int x,int l,int r){
	//查询一个数的下标
	int k=upper_bound(v[x].begin(),v[x].end(),r)-lower_bound(v[x].begin(),v[x].end(),l);
	return k;
}
int query(int l,int r){
	int mode=1e6+10;
	int ans=0;//mode是众数,ans是数量
	if(id[l]==id[r]){
		for(int i=l;i<=r;i++){
			int k=Num(p[i],l,r);
			if(k>ans||(k==ans&&val[p[i]]<val[mode])){
				ans=k;
				mode=p[i];
			}
		}
		return val[mode];
	}
	mode=f[id[l]+1][id[r]-1];//如果在同一区间或者相邻区间就为0
	ans=Num(mode,l,r);//初始化
	for(int i=l;i<=ed[id[l]];i++){
		int k=Num(p[i],l,r);
			if(k>ans||(k==ans&&val[p[i]]<val[mode])){
				ans=k;
				mode=p[i];
			}
	}
	for(int i=st[id[r]];i<=r;i++){
		int k=Num(p[i],l,r);
		if(k>ans||(k==ans&&val[p[i]]<val[mode])){
			ans=k;
			mode=p[i];
		}
	}
	return val[mode];
	//枚举其他位置 看看有没有数能够成为众数
	
}
int main(){
	n=read();m=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		if(!mp[a[i]]){
			mp[a[i]]=++idx;
			val[idx]=a[i];
		}//不在乎绝对大小甚至不在乎相对大小 
		//val[mp[a[i]]]=a[i];//这里出错了
		p[i]=mp[a[i]];//记录是第几个出现的
		v[p[i]].push_back(i);//记录下某个数出现的位置 
	}
	build();
	while(m--){
		int l,r;
		l=read();
		l=(l+res-1)%n+1;
		r=read();
		r=(r+res-1)%n+1;
		if(l>r)swap(l,r);
		res=query(l,r);
		printf("%d\n",res);
	}
	return 0;
}

1.5洛谷P1637

一句话的题意:找这样的三元组(ai,aj,ak),使得ai<aj<ak。不妨枚举j,找j左边有多少<aj的,j右边有多少>aj的,直接分块就行了,解法和P2801一样。 

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#define int long long 
using namespace std;
const int N=1e5+10;
const int SQ=340;
int n,m,sq,a[N],b[N],res;
int st[SQ],ed[SQ],id[N];
void build(){//建块  本题没有修改 不需要懒标记 
	sq=sqrt(n);
	for(int i=1;i<=sq;i++){
		st[i]=n/sq*(i-1)+1;
		ed[i]=n/sq*i;
	}
	ed[sq]=n;
	for(int i=1;i<=sq;i++){
		for(int j=st[i];j<=ed[i];j++){
			id[j]=i;
			b[j]=a[j];
		}
	}
	for(int i=1;i<=sq;i++){
		sort(b+st[i],b+ed[i]+1);//整块排序 
	}
}
int find(int l,int r,int c){
	while(l<r){
		int mid=l+r>>1;
		if(b[mid]>c)r=mid;
		else l=mid+1;
	}
	return l;
}
int cal(int l,int r,int c){
	while(l<r){
		int mid=l+r>>1;
		if(b[mid]>c)r=mid;
		l=mid+1;
	}
	return l;
}
int ask1(int l,int r,int c){//找左边区间小于c的 
	int ans=0;
	if(id[l]==id[r]){
		for(int i=l;i<=r;i++){
			if(a[i]<c)ans++;;
		}
		return ans;
	}
	else{
		int ans=0;
		for(int i=l;i<=ed[id[l]];i++){
			if(a[i]<c)ans++;
		}
		for(int i=st[id[r]];i<=r;i++){
			if(a[i]<c)ans++;
		}
		for(int i=id[l]+1;i<id[r];i++){//找区间内小于c的最大位置
			int pos=find(st[i],ed[i],c-1);
			if(a[pos]<c)ans+=pos-st[i]+1; 
		}
		return ans;
	}
}
int ask2(int l,int r,int c){//找右边区间大于c的 
	int ans=0;
	if(id[l]==id[r]){
		for(int i=l;i<=r;i++){
			if(a[i]>c)ans++;;
		}
		return ans;
	}
	else{
		int ans=0;
		for(int i=l;i<=ed[id[l]];i++){
			if(a[i]>c)ans++;
		}
		for(int i=st[id[r]];i<=r;i++){
			if(a[i]>c)ans++;
		}
		for(int i=id[l]+1;i<id[r];i++){//找区间内小于c的最大位置
			int pos=find(st[i],ed[i],c+1);
			if(a[pos]>c)ans+=ed[i]-pos+1; 
		}
		return ans;
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=2;i<n;i++){
		int l1=1,r1=i-1;
		int l2=i+1,r2=n;
		res+=ask1(l1,r1,a[i])*ask2(l2,r2,a[i]);
	}
	cout<<res<<endl;
	return 0;
}

2.杂鱼~❤杂题

2.1洛谷P4513

这道题非常有意思了,是一道线段树的经典好题。以前做的题都太板了。

题意:操作1:修改x位置的数为k。操作2:查询区间[l,r]的最大字段和。

我们考虑维护区间最大子段和max,包含左端点的最大子段和maxl,包含右端点的最大子段和maxr,区间和sum。

sum:tr[id].sum=tr[id<<1].sum+tr[id<<1|1].sum.

其余三个我们可以考虑画图理解。

如图,maxl可以由红色部分,即左孩子的maxl推得,也可以由左孩子的sum与右孩子的maxl推得

即:tr[id].maxl=max(tr[id<<1].maxl,tr[id<<1].sum+tr[id<<1|1].maxl)

同理可知tr[id].maxr=max(tr[id<<1|1].maxr+tr[id<<1].sum+tr[id<<1|1].maxr)

这里写仔细点,不然你就会和我一样一调半小时 。

而最大子段和可能是左孩子的最大字段和,也可可能是右孩子的最大字段和,也可能是左右孩子加起来的左孩子包含右端点,右孩子包含左端点的和,三者取最大值。可以直观感受一下。

2.2洛谷P2419

题意:给你若干组奶牛实力的相对大小,确定最后能确定排名的奶牛的数量。

n≤100,可以考虑用一种类似与floyd的思想。在建图时若实力u>v将f[u][v]标为1,否则标为0。注意的是将f数组初始化成+∞,最后若f[i][k]和f[k][j]都为1,f[i][j]也为1,若都为0则f[i][j]也为0。

最后扫一遍,f不是+∞就记录答案,时间复杂度是O(^{_{n}3})。

#include<iostream>
#include<cstring>
using namespace std;
const int N=110;
int n,m,f[N][N],ans;
int main(){
	cin>>n>>m;
	memset(f,0x3f,sizeof f);
	while(m--){
		int u,v;
		cin>>u>>v;
		f[u][v]=1;
		f[v][u]=0;
	}
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(f[i][k]==f[k][j]&&f[i][k]==1)f[i][j]=1;
				if(f[i][k]==f[k][j]&&f[i][k]==0)f[i][j]=0;
			}
		}
	}
	for(int i=1;i<=n;i++){
		int flag=1;
		for(int j=1;j<=n;j++){
			if(i==j)continue;
			if(f[i][j]!=1&&f[i][j]!=0){
				flag=0;
				break;
			}
		}
		ans+=flag;
	}
	cout<<ans<<endl;
	return 0;
}

今天就写那么多吧。

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值