25-暑期-来追梦noip-卷7 总结

好像目前为止只有我补了 D

开题顺序:A-C-D(B 没看)

分配时间:A 1h C 45min D 15min

A

预估 100,实际 60。

将序列转化为 \(1,-1\),其中大于等于 \(x\)\(1\),小于的为 \(-1\)

然后做一遍前缀和,容易发现区间和 \(\ge 0\) 的即为合法区间。

然后用树状数组统计一下即可,注意前缀和可能为负,所以需要离散化 / 偏移。

时间复杂度单 \(\log\)

赛时写的离散化挂了 40 分 /ll

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5,V=2e5+5,B=1e5;
int n,x;
int a[N],sum[N],tmp[N],tree[V];

int lowbit(int x){
	return x&(-x);
}
void upd(int x,int y){
	for(;x<=V-5;x+=lowbit(x))
		tree[x]+=y;
}
int qry(int x){
	int res=0;
	for(;x;x-=lowbit(x))
		res+=tree[x];
	return res;
}

signed main(){
	//freopen("T1.in","r",stdin);
	//freopen("T1.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>x;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i]=(a[i]>=x?1:-1);
		sum[i]=sum[i-1]+a[i];
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		ans+=qry(sum[i]+B);
		if(sum[i]>=0)
			ans++;
		upd(sum[i]+B,1);
	}
	cout<<ans;
	return 0;
}

B

预估 0,实际 0。

这种位运算题目,套路的,我们按位考虑贡献。

对于每一位,我们发现只有 \(1\) 才能产生贡献。

令第 \(i\)\(1\) 的个数为 \(cnt_i\),则有 \(2^{cnt_i}-1\) 中选择的方案,每一种的贡献显然是 \(2^i\)

直接这样统计是 \(\mathcal{O}(m \times 30n)\)(最多 \(30\) 位),无法接受,所以用树状数组优化一下统计的部分即可。具体而言,就是开 \(30\) 棵树状数组统计 \(n\) 个元素。时间复杂度可降至 \(\mathcal{O}(m \times 30 \log n)\)

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e6+5,M=31;
const int MOD=1e9+7;
int n,m;
int a[N],p[N];

struct BIT{
	int C[N];
	int lowbit(int x){
		return x&(-x);
	}
	void upd(int x,int y){
		for(;x<=n;x+=lowbit(x))
			C[x]+=y;
	}
	int qry(int x){
		int res=0;
		for(;x;x-=lowbit(x))
			res+=C[x];
		return res;
	}
}tree[M];

signed main(){
	//freopen("T2.in","r",stdin);
	//freopen("T2.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	p[0]=1;
	for(int i=1;i<=1000000;i++)
		p[i]=(p[i-1]*2)%MOD;
	for(int i=1;i<=n;i++)
		for(int j=0;j<30;j++)
			if((a[i]>>j)&1)
				tree[j].upd(i,1);
	cin>>m;
	while(m--){
		int op,x,y;
		cin>>op>>x>>y;
		if(op==1){
			for(int i=0;i<30;i++)
				if((a[x]>>i)&1)
					tree[i].upd(x,-1);
			a[x]=y;
			for(int i=0;i<30;i++)
				if((a[x]>>i)&1)
					tree[i].upd(x,1);
		}
		else{
			int ans=0;
			for(int i=0;i<30;i++)
				ans=(ans+((p[tree[i].qry(y)-tree[i].qry(x-1)]-1)%MOD)*(p[i]%MOD))%MOD;
			cout<<ans<<'\n';
		}
	}
	return 0;
}

总结:

  • 位运算题目按位考虑贡献。

C

预估 0,实际 0。

第一个操作是区间所有元素与 \(k\)\(\max\) 并赋值,可以使用线段树轻松维护。

第二个的话,需要我们找最小值、次小值......总共 \(x\) 个。从线段树的角度出发,假设区间 \([l,r]\) 找到了最小值,则它的次小值一定在以最小值所在位置分割,左右两半区间中的任一一个里边。因此,我们进行分治,直接用小根堆维护最小、次小等等这些值即可。时间复杂度双 \(\log\)

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=2e6+5;
const int INF=1e18; 
int n,m;
int a[N],tree[N<<2],pos[N<<2],tag[N<<2];
vector<int> vec;
struct NODE{
	int l,r,w,p;
	bool operator < (const NODE &b) const{
		return w>b.w;
	}
};
priority_queue<NODE> pq;

void pushup(int p){
	tree[p]=INF;
	if(tree[p]>tree[p<<1])
		tree[p]=tree[p<<1],pos[p]=pos[p<<1];
	if(tree[p]>tree[p<<1|1])
		tree[p]=tree[p<<1|1],pos[p]=pos[p<<1|1];
}
void addtag(int p,int val){
	tree[p]=max(tree[p],val);
	tag[p]=max(tag[p],val);
}
void pushdown(int p){
	if(!tag[p])
		return;
	addtag(p<<1,tag[p]);
	addtag(p<<1|1,tag[p]);
	tag[p]=0;
}
void build(int p,int lt,int rt){
	if(lt==rt){
		tree[p]=a[lt];
		pos[p]=lt;
		return;
	}
	int mid=(lt+rt)>>1;
	build(p<<1,lt,mid);
	build(p<<1|1,mid+1,rt);
	pushup(p);
}
void upd(int p,int lt,int rt,int ql,int qr,int k){
	if(lt>qr||rt<ql)
		return;
	if(ql<=lt&&rt<=qr){
		addtag(p,k);
		return;
	}
	pushdown(p);
	int mid=(lt+rt)>>1;
	upd(p<<1,lt,mid,ql,qr,k);
	upd(p<<1|1,mid+1,rt,ql,qr,k);
	pushup(p); 
}
pair<int,int> qry(int p,int lt,int rt,int ql,int qr){
	if(lt>qr||rt<ql)
		return {INF,0};
	if(ql<=lt&&rt<=qr)
		return {tree[p],pos[p]};
	pushdown(p);
	int mid=(lt+rt)>>1;
	pair<int,int> ret={INF,0};
	if(ql<=mid){
		auto cur=qry(p<<1,lt,mid,ql,qr);
		if(ret.first>cur.first)
			ret=cur;
	}
	if(qr>mid){
		auto cur=qry(p<<1|1,mid+1,rt,ql,qr);
		if(ret.first>cur.first)
			ret=cur;
	}
	return ret;
}

signed main(){
	//freopen("T3.in","r",stdin);
	//freopen("T3.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	build(1,1,n);
	cin>>m;
	while(m--){
		int op,a,b,k,x;
		cin>>op>>a>>b>>k;
		if(op==1)
			upd(1,1,n,a,b,k);
		else{
			cin>>x;
			vec.clear();
			while(!pq.empty())
				pq.pop();
			auto st=qry(1,1,n,a,b);
			pq.push({a,b,st.first,st.second});
			while(!pq.empty()){
				auto cur=pq.top();
				pq.pop();
				if(cur.w>=k)
					continue;
				vec.push_back(cur.w);
				auto u=qry(1,1,n,cur.l,cur.p-1);
				auto v=qry(1,1,n,cur.p+1,cur.r);
				pq.push({cur.l,cur.p-1,u.first,u.second});
				pq.push({cur.p+1,cur.r,v.first,v.second});
				if(vec.size()==x)
					break;
			}
			if(vec.size()!=x)
				cout<<"-1\n";
			else{
				sort(vec.begin(),vec.end());
				for(int i:vec)
					cout<<i<<' ';
				cout<<'\n';
			}
		}
	}
	return 0;
}

总结:

  • 找前 \(x\) 小:分治+小根堆。

D

首先,这个题里边有个长得很丑的式子,我们把它化简一下。

\[f_i \\ = \max(d_i, \min(\max^{i-1}_{j=1} d_j, \max^{n}_{k=i+1} d_k))\\ =\min(\max^{i}_{j=1} d_j, \max^{n}_{k=i+1} d_k) \]

然后我们惊喜的发现,这玩意不就是前后缀最大值取 \(\min\) 吗。

首先前后缀最大值显然可以线段树维护。然后观察前后缀最大值的图像(这里就直接搬了)。

image

容易发现,前后缀最大值总是单调不减的,并且总会在最后达到一个峰值。

那么,对于第一个操作,峰值处求一个两边区间和就是答案。

对于第二个操作,我们发现这个 \(f_i\) 就是图像中较低的那条线,它显然具有单调性,所以我们可以二分出一个 \(\ge v\) 的左边界和右边界,然后和 \([l,r]\) 比较一下得到真实区间即可统计。

对于第三个操作,修改某一个位置总是只会影响后面的前缀,前面的后缀,所以直接区间修改即可,别忘了更新峰值。

时间复杂度当然是单 \(\log\)

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;

const int N=3e5+5;
int n,m;
int a[N],pre[N];

struct Sgt_Pepper_s_Lonely_Hearts_Club_Band{
	int sum[N<<2],mx[N<<2],tag[N<<2];
	void pushup(int p){
		sum[p]=sum[p<<1]+sum[p<<1|1];
		mx[p]=mx[p<<1|1];
	}
	void addtag(int p,int lt,int rt,int val){
		mx[p]=tag[p]=val;
		sum[p]=val*(rt-lt+1);
	}
	void pushdown(int p,int lt,int rt){
		if(!tag[p])
			return;
		int mid=(lt+rt)>>1;
		addtag(p<<1,lt,mid,tag[p]);
		addtag(p<<1|1,mid+1,rt,tag[p]);
		tag[p]=0;
	}
	void build(int p,int lt,int rt){
		if(lt==rt){
			sum[p]=mx[p]=pre[lt];
			return;
		}
		int mid=(lt+rt)>>1;
		build(p<<1,lt,mid);
		build(p<<1|1,mid+1,rt);
		pushup(p);
	}
	void upd(int p,int lt,int rt,int x,int val){
		if(lt>=x&&mx[p]<=val){
			addtag(p,lt,rt,val);
			return;
		}
		if(lt==rt)
			return;
		pushdown(p,lt,rt);
		int mid=(lt+rt)>>1;
		if(mx[p<<1]<=val)
			upd(p<<1|1,mid+1,rt,x,val);
		if(mid>=x)
			upd(p<<1,lt,mid,x,val);
		pushup(p);
	}
	int getsum(int p,int lt,int rt,int x){
		if(rt<=x)
			return sum[p];
		pushdown(p,lt,rt);
		int mid=(lt+rt)>>1,res=getsum(p<<1,lt,mid,x);
		if(mid<x)
			res+=getsum(p<<1|1,mid+1,rt,x);
		return res;
	}
	int getfir(int p,int lt,int rt,int val){
		if(lt==rt)
			return lt;
		pushdown(p,lt,rt);
		int mid=(lt+rt)>>1;
		if(mx[p<<1]>=val)
			return getfir(p<<1,lt,mid,val);
		return getfir(p<<1|1,mid+1,rt,val);
	}
}f,g;

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m;
	int maxi=-1e9,pos;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		if(maxi<a[i])
			maxi=a[i],pos=i;
	}
	for(int i=1;i<=n;i++)
		pre[i]=max(pre[i-1],a[i]);
	f.build(1,1,n);
	for(int i=1;i<=n;i++)
		pre[i]=max(pre[i-1],a[n-i+1]);
	g.build(1,1,n);
	while(m--){
		int op,l,r,x,v;
		cin>>op;
		if(op==1)
			cout<<f.getsum(1,1,n,pos)+g.getsum(1,1,n,n-pos)<<'\n';
		else if(op==2){
			cin>>l>>r>>v;
			if(v>maxi){
				cout<<"0\n";
				continue;
			}
			l=max(l,f.getfir(1,1,n,v));
			r=min(r,n-g.getfir(1,1,n,v)+1);
			cout<<(l<=r?r-l+1:0)<<'\n';
		}
		else{
			cin>>x>>v;
			a[x]+=v;
			if(maxi<a[x])
				maxi=a[x],pos=x;
			f.upd(1,1,n,x,a[x]);
			g.upd(1,1,n,n-x+1,a[x]);
		}
	}
	return 0;
}

总结:

  • 画图、看见式子推式子。

结语

成绩:60+0+0+32=92。

问题:离散化没有注意把所有的元素放进去;对于一些套路不熟悉;草稿纸没有利用好。

方案:注意代码细节,其他同上总结。

posted @ 2025-08-22 17:50  _KidA  阅读(8)  评论(0)    收藏  举报