10.4 2025多校冲刺CSP模拟赛2 改题记录

HZOJ
accoders

写在前面

October 4th 好耶!明天就放假啦!感觉这几天集训状态挺不错,可能因为临近考试了?感觉差距还很大,不会的东西还很多,但是就尽力而为吧。虽然老早就迫不及待地想退役了,但马上真要退役了莫名还有些感伤怎么回事?但是总之还是把最后不到两个月撑过去吧。话说这场模拟赛怎么三个双log。

A. 查询

题意是给定三个序列\(a,b,c\) ,求问所有\(a_i+b_i\times c_j\) 的值中第\(k\) 小的是多大。
本来想搞三维前缀和,虽然我也不知道有啥用。还好数据范围及时制止了我。然后跳出个\(O(nlognlogV)\) 的二分答案的想法,差点因为主观臆断会超时没写。还好写了。
具体做法就是因为\(c\)\(a,b\) 无必然联系,所以可以先将\(c\) 排序,那么对于每对\(a,b\),按顺序所求出的值都是递增的。然后二分一个上界,统计不超过该上界的数的个数,可以利用\(c\) 的单调性二分搜索。不断缩小范围,直到求出答案。

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
typedef long long ll;
int a[maxn],b[maxn],c[maxn],n;
inline ll check(ll mid){
	ll sum=0,t;
	for(int i=1;i<=n;++i){
		t=(mid-a[i])/b[i];
		if(t>c[n]) sum+=n;
		else if(t>=c[1]) sum+=upper_bound(c+1,c+n+1,t)-c-1;
	}
	return sum;
}
int main(){
	freopen("query.in","r",stdin);
	freopen("query.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int mxa=0,mxb=0,mxc=0;
	ll k;
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],mxa=(mxa<a[i]?a[i]:mxa);
	for(int i=1;i<=n;++i) cin>>b[i],mxb=(mxb<b[i]?b[i]:mxb);
	for(int i=1;i<=n;++i) cin>>c[i],mxc=(mxc<c[i]?c[i]:mxc);
	cin>>k;
	sort(c+1,c+n+1);
	ll l=0,r=mxa+1ll*mxb*mxc,mid;
	while(l<=r){
		mid=(l+r)>>1;
		if(check(mid)>=k) r=mid-1;
		else l=mid+1;
	}
	cout<<l;
	return 0;
}

B. 参加

神仙题。题意是给出一个序列,可以操作使其某个区间+1,求问至少多少次操作能将序列变成单峰的。

我说它是神仙题是因为这道题1e5的数据\(O(n)\) 复杂度开2s时限,搞得我从开这道题到结束这道题大概15min,写完心里完全没底。

首先转化题意。单峰的另一种体现就是以某个点为分界,该点及其以前的点都比它前面的点值大,该点后的点都比它前面的点值小,再转化一下就是差分数组以某一点为界,该点及其以前的点都为正值,该点后的所有点对应的值都是负值。再转化一下操作,每次操作就相当于选差分数组的一个点加一个数,选该点之后的一个点减去那个数。所以做法就很显然了。先预处理出每个点差分数组小于1部分的与1的差值前缀和(包括该点),每个点在差分数组上大于-1部分的与-1的差值后缀和(不包括该点),答案就为所有位置上前缀和与后缀和较大者的最小值。

既然我过了就放你一马。

代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
int n,a[maxn],cf[maxn];
ll bigger[maxn],smaller[maxn];
int main(){
	freopen("attend.in","r",stdin);
	freopen("attend.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],cf[i]=a[i]-a[i-1];
	for(int i=1;i<=n;i++){
		bigger[i]=bigger[i-1]; 
		if(cf[i]<1) bigger[i]+=1-cf[i];
	}
	for(int i=n-1;i>=1;i--){
		smaller[i]=smaller[i+1];
		if(cf[i+1]>-1) smaller[i]+=1+cf[i+1];
	}
	ll ans=1e18;
	for(int i=1;i<=n;i++) ans=min(ans,max(bigger[i],smaller[i]));
	cout<<ans;
	return 0;
}

C. 决斗

神仙题。题意是给出两个序列\(a,b\) ,求使得满足\(a_i<b_i\) 的位置最多的字典序最大的\(b\) 的排列。

这题也是神仙题是因为赛时写了>2.5h 然后只有暴力的部分拿了20pts。我无语了。

赛时思路乱成一锅了就不写了。总之就是瞎贪心。

我们可以很容易求出最大的满足条件的位置数,然后我们要做的就是在不改变满足条件的位置数的前提下最大化字典序。虽然赛时不会写,但是思路正确的想法是,如果一个大的值和在其当前位置以前的小的值交换位置后满足条件的位置数不变是,交换必然更优。现在我们要考虑的就是在规定时间内将所有能交换的位置交换。

我们要求每个位置对应的值。我们假定当前位置以前的值都已经求出。则我们应该贪心地选取不会使答案改变的最大的值。当一个较大的值可选时,那么一个较小的值也能选。所以我们可以二分出最大的能选的值。但是我们哪些值可选是个问题。所以我们可以建一棵权值线段树,维护\(a,b\) 两个序列的剩余和排名情况,还有在树上剩余的点中最多能在树上匹配的点数。

\(t\) 为树上节点代表的区间内能匹配的最大位置数,\(ta\) 为该区间内可参与匹配的\(a\) 的数量,\(tb\) 为该区间内可参与匹配的\(b\) 的数量,\(ts\) 为当前区间内能参与匹配的(包括树上已匹配的)\(b\) 的数量。pushup的时候就可以将该节点右子树的\(b\) 和左子树的\(a\) 合并,剩余没有被合并的部分就继续上传。最后\(t_1\) 就是树上所有点能匹配的最大位置数。

那么对于从前往后的每个位置,我们要记录到当前有多少点已经匹配上了。我们先找出其对应的\(a\) 在树上的排名,删去树上的\(a\),然后分类讨论:
1.如果该点被匹配:二分能匹配到的最大值。下界为\(a\) 的排名加一,上界为剩余的\(b\) 的总数。每分一次就先删去排名为mid的点(用来和该点匹配),如果树上剩余匹配数加上树外匹配数等于了最大值则符合条件,否则不符合,然后将删去的点重新插回去。当我们二分出了最大可匹配点,则该点的答案就是二分出的点。否则该点不能被匹配,只能对字典序产生贡献,不能对答案产生贡献。
2.如果该点不被匹配:同样是二分。但是这次我们在1到该点排名的范围内查找,因为能进行到该步的肯定不能对答案有贡献。流程跟1类似。但是这里就只是单纯找最大的不影响答案的数了,不用讨论贡献了。

然后就没了。好像梳理了一下没那么复杂了。但是话说这种题是什么神人才能想出来做法的。

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10,rm=1e5;
int n,ans[maxn],b[maxn],aa[maxn],bb[maxn],a[maxn];
#define lc(u) (u<<1)
#define rc(u) (u<<1|1)
int ta[maxn<<2],tb[maxn<<2],t[maxn<<2],ts[maxn<<2];
inline void pushup(int u){
	int d=min(ta[lc(u)],tb[rc(u)]);
	t[u]=t[lc(u)]+t[rc(u)]+d;
	ta[u]=ta[lc(u)]+ta[rc(u)]-d;
	tb[u]=tb[lc(u)]+tb[rc(u)]-d;
	ts[u]=ts[lc(u)]+ts[rc(u)];
}
inline void build(int u,int l,int r){
	if(l==r){
		ta[u]=aa[l],ts[u]=tb[u]=bb[l];
		return;
	}
	int mid=(l+r)>>1;
	build(lc(u),l,mid);
	build(rc(u),mid+1,r);
	pushup(u);
}
inline void update(int u,int l,int r,int p,int ka,int kb){
	if(l==r){
		ta[u]+=ka,tb[u]+=kb,ts[u]+=kb;
		return;
	}
	int mid=(l+r)>>1;
	if(p<=mid) update(lc(u),l,mid,p,ka,kb);
	else update(rc(u),mid+1,r,p,ka,kb);
	pushup(u);
}
inline int rnk(int u,int l,int r,int p){
	if(l==r) return ts[u];
	int mid=(l+r)>>1;
	if(p<=mid) return rnk(lc(u),l,mid,p);
	return ts[lc(u)]+rnk(rc(u),mid+1,r,p);
}
inline int kth(int u,int l,int r,int x){
	if(l==r) return l;
	int mid=(l+r)>>1;
	if(x<=ts[lc(u)]) return kth(lc(u),l,mid,x);
	return kth(rc(u),mid+1,r,x-ts[lc(u)]);
}
int main(){
	freopen("duel.in","r",stdin);
	freopen("duel.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n;
	int sum=0,cur=0;
	for(int i=1;i<=n;++i) cin>>a[i],++aa[a[i]];
	for(int i=1;i<=n;++i) cin>>b[i],++bb[b[i]];
	build(1,1,rm);
	sum=t[1];
	for(int i=1;i<=n;i++){
		int cnt=rnk(1,1,rm,a[i]);
		update(1,1,rm,a[i],-1,0);
		int l=cnt+1,r=n-i+1,mid;
		if(l<=r){
			while(l<=r){
				mid=(l+r)>>1;
				int val=kth(1,1,rm,mid);
				update(1,1,rm,val,0,-1);
				++cur;
				if(sum==t[1]+cur) l=mid+1;
				else r=mid-1;
				--cur,update(1,1,rm,val,0,1);
			}
			int val=kth(1,1,rm,r);
			++cur,update(1,1,rm,val,0,-1);
			if(cur+t[1]==sum){
				ans[i]=val;
				continue;
			}
			--cur,update(1,1,rm,val,0,1);
		}
		l=1,r=cnt;
		while(l<=r){
			mid=(l+r)>>1;
			int val=kth(1,1,rm,mid);
			update(1,1,rm,val,0,-1);
			if(sum==t[1]+cur) l=mid+1;
			else r=mid-1;
			update(1,1,rm,val,0,1);
		}
		int val=kth(1,1,rm,r);
		ans[i]=val;
		update(1,1,rm,val,0,-1);
	}
	for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
	return 0;
}

D. 回文串问题

还是神仙题。题意是有一个不知名字符串,有以下两种操作:
1.给出\(l,r\),表示该字符串的\(l,r\) 的子串是个回文串;
2.给出\(a,b,x,y\),求问\(substr(a,b)\)\(substr(x,y)\) 的关系。

这题是神仙题因为我把\(q\) 个循环只跑了\(n\) 次,痛挂20pts。自以为用bitset存相等关系能优化点,但是实际上之间跑暴力修改能多拿25pts。我无语。

赛时想到相等关系就是0/1,然后可合并性就像是或运算,然后就用了bitset,将要添加关系的两个值对应的关系或起来,然后再对于或出的关系每个是1的位进行一个或。

然后正解大概就是把回文做了一定的转化。一般串匹配问题都用哈希做,但是回文串不好处理反向的哈希,我们可以就在原串后加一个反串(也可以和原串并列)。求出哈希值。对于添加关系操作,如果某一对位置间已有建立好的关系,我们不用继续添加。需要新建立的关系就需要将这两个位置所有的关系位置都并起来。所以我们可以二分哈希,找到需要添加的位置添加。然后如何合并也是个问题。暴力合并肯定不行,但是暴力的思想和优秀的复杂度可以兼得——启发式合并。每次我们将较小的集合合并到较大的集合内,可证(我不会证)均摊复杂度是log的。

然后对于询问,当且仅当两个子串长度不等的时候我们才能断定这两个串一定不等。如果两个子串每个位置能一一对应(两个串哈希值相同)才可以断定两个串一定相等,否则两串关系未知。

谨防哈希冲突qwq。还有,本次采用了非同寻常的哈希方法,也算是进行了一次想法的实践。

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
typedef unsigned long long ull;
const ull P=1e9+7;
int n,q;
mt19937 rnd(time(NULL));
ull t[maxn<<1],p[maxn<<1];
inline void update(int u,ull x){
	for(int i=u;i<=2*n;i+=(i&-i)) t[i]+=x*p[u];
}
inline ull qry(int u){
	ull sum=0;
	for(int i=u;i;i-=(i&-i)) sum+=t[i];
	return sum;
}
inline ull gethash(int l,int r){
	return (qry(r)-qry(l-1));
}
int bl[maxn];
ull hs[maxn<<1];
vector<int> djl[maxn];
inline void merge(int a,int b){
	if(a==b) return;
	if(djl[a].size()<djl[b].size()) swap(a,b);
		ull val=hs[djl[a].front()];
		while(!djl[b].empty()){
			int tmp=djl[b].back();
			update(tmp,val-hs[tmp]);
			update(2*n-tmp+1,val-hs[tmp]);
			hs[2*n-tmp+1]=hs[tmp]=val;
			bl[tmp]=a;
			djl[a].push_back(tmp);
			djl[b].pop_back();
		} 
}
inline void up(int l1,int r1,int x,int y){
	if(l1==r1){
		if(gethash(l1,r1)*p[x-l1]!=gethash(x,y)) merge(bl[l1],bl[2*n+1-x]);
		return;
	}
	int mid1=(l1+r1)>>1,mid2=(x+y)>>1;
	if(gethash(l1,mid1)*p[x-l1]!=gethash(x,mid2)) up(l1,mid1,x,mid2);
	if(gethash(mid1+1,r1)*p[mid2-mid1]!=gethash(mid2+1,y)) up(mid1+1,r1,mid2+1,y);
}
int main(){
	freopen("palin.in","r",stdin);
	freopen("palin.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>q;
	p[0]=1;
	for(int i=1;i<=2*n;i++) p[i]=p[i-1]*P;
	for(int i=1;i<=n;i++) hs[i]=hs[(n<<1)-i+1]=rnd(),djl[i].emplace_back(i),bl[i]=i,update(i,hs[i]),update((n<<1)-i+1,hs[i]);
	for(int i=1,op;i<=q;i++){
		cin>>op;
		if(op==1){
			int l,r,len;
			cin>>l>>r;
			if(l==r) continue;
			len=r-l+1;
			up(l,l+(len>>1)-1,2*n-r+1,2*n-(r-(len>>1)+1)+1);
		}
		else{
			int a,b,x,y;
			cin>>a>>b>>x>>y;
			if(b-a!=y-x){
				cout<<"Not equal\n";
				continue;
			} 
			if(a>x) swap(a,x),swap(b,y);
			bool flg=(gethash(a,b)*p[x-a]==gethash(x,y));
			cout<<(flg?"Equal\n":"Unknown\n");
		}
	}
	
	return 0;
}
posted @ 2025-10-04 21:43  _dlwlrma  阅读(25)  评论(2)    收藏  举报