[题解][test06]2024/11/23 模拟赛 / 2023牛客OI赛前集训营-提高组(第三场) A~C

原题页面:https://ac.nowcoder.com/acm/contest/65194
Statements & Solution : https://www.luogu.com.cn/problem/U507978

\(80+80+50+24=234\)

A

贪心+双指针。

根据贪心思想,大值选大、小值选小。我们按绝对值从大到小给\(a\)排序,再按从小到大给\(b\)排序,取双指针\(l=1,r=m\)

从左往右遍历\(a[i]\),如果\(a[i]>0\)则选\(b[r]\)配对,否则选\(b[l]\)配对,配对后指针相应移动即可。

时间复杂度\(O(n)\)

不放赛时代码了。赛时想复杂了,代码实现不是很简洁,而且暂时无从得知为什么只拿了\(80\text{ pts}\),已经对拍了上千组样例了(^^;

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define N 100010
#define M 100010
using namespace std;
int n,m,a[N],b[M],ans;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=m;i++) cin>>b[i];
	int l=1,r=m;
	sort(a+1,a+1+n,[](int a,int b){return abs(a)>abs(b);});
	sort(b+1,b+1+m);
	for(int i=1;i<=n;i++) ans+=a[i]*(a[i]>0?b[r--]:b[l++]);
	cout<<ans<<"\n";
	return 0;
}

B

最后\(20\)分钟才开窍打出来了\(80\text{ pts}\),当时要是时间再多点正解也出来了,主要是前面无效思考太多了,果然还得多练。

赛时\(80\text{ pts}\)思路

建立大小为\(m\)的线段树,每个节点维护\(sum,cnt\),即数的总和、以及出现的总次数。

对于询问\(i\),将\(a[1\sim (i-1)]\)压进线段树中。我们想找的是“压入的所有元素中,最多能选出多少个,使得它们的和\(\le V\)”,其中\(V=m-a[i]\)

为了让个数尽可能多,我们肯定优先选尽可能小的元素,而我们是按值域建树的,所以从根节点开始。先考虑左子树,如果左子树的\(sum(lson)\le V\),那就跳到左子树里找\(V\);否则跳到右子树中找\(V-sum(lson)\),再把答案加上\(cnt(lson)\)。代码是这样的:

int query(int v,int x,int l,int r){//[l,r]值域范围内,和<=v最多能选多少个
	if(l==r) return min(cnt[x],v/l);
	int mid=(l+r)>>1;
	if(sum[lc(x)]<=v) return cnt[lc(x)]+query(v-sum[lc(x)],rc(x),mid+1,r);
	else return query(v,lc(x),l,mid);
}

时间复杂度\(O(T(n+m)\log m)\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lc(x) ((x)<<1)
#define rc(x) ((x)<<1|1)
#define N 100010
using namespace std;
int t,n,m,a[N],cnt[400010],sum[400010];
void update(int x){
	cnt[x]=cnt[lc(x)]+cnt[rc(x)];
	sum[x]=sum[lc(x)]+sum[rc(x)];
}
void build(int x,int l,int r){
	if(l==r) return (void)(cnt[x]=sum[x]=0);
	int mid=(l+r)>>1;
	build(lc(x),l,mid),build(rc(x),mid+1,r);
	update(x);
}
void chp(int a,int v,int v2,int x,int l,int r){
	if(l==r) return (void)(cnt[x]+=v,sum[x]+=v2);
	int mid=(l+r)>>1;
	if(a<=mid) chp(a,v,v2,lc(x),l,mid);
	else chp(a,v,v2,rc(x),mid+1,r);
	update(x);
}
int query(int v,int x,int l,int r){//[l,r]值域范围内,和<=v最多能选多少个
	if(l==r) return min(cnt[x],v/l);
	int mid=(l+r)>>1;
	if(sum[lc(x)]<=v) return cnt[lc(x)]+query(v-sum[lc(x)],rc(x),mid+1,r);
	else return query(v,lc(x),l,mid);
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>t;
	while(t--){
		cin>>n>>m;
		build(1,1,m);
		for(int i=1;i<=n;i++){
			cin>>a[i];
			cout<<i-query(m-a[i],1,1,m)-1<<" ";
			chp(a[i],1,a[i],1,1,m);
		}
		cout<<"\n";
	}
	return 0;
}

正解

这种写法显然有很多冗余空间,我们将值域离散一下不就好了嘛,只要里面的内容不改变就行了。

时间复杂度\(O(T n\log m)\),可以通过。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lc(x) ((x)<<1)
#define rc(x) ((x)<<1|1)
#define N 100010
using namespace std;
int t,n,m,a[N],tmp[N],cnt[400010],sum[400010];
unordered_map<int,int> to;
void update(int x){
	cnt[x]=cnt[lc(x)]+cnt[rc(x)];
	sum[x]=sum[lc(x)]+sum[rc(x)];
}
void build(int x,int l,int r){
	if(l==r) return (void)(cnt[x]=sum[x]=0);
	int mid=(l+r)>>1;
	build(lc(x),l,mid),build(rc(x),mid+1,r);
	update(x);
}
void chp(int a,int v,int v2,int x,int l,int r){
	if(l==r) return (void)(cnt[x]+=v,sum[x]+=v2);
	int mid=(l+r)>>1;
	if(a<=mid) chp(a,v,v2,lc(x),l,mid);
	else chp(a,v,v2,rc(x),mid+1,r);
	update(x);
}
int query(int v,int x,int l,int r){//[l,r]值域范围内,和<=v最多能选多少个
	if(l==r) return min(cnt[x],v/tmp[l]);
	int mid=(l+r)>>1;
	if(sum[lc(x)]<=v) return cnt[lc(x)]+query(v-sum[lc(x)],rc(x),mid+1,r);
	else return query(v,lc(x),l,mid);
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>t;
	while(t--){
		cin>>n>>m;
		for(int i=1;i<=n;i++) cin>>a[i],tmp[i]=a[i];
		sort(tmp+1,tmp+1+n);
		int tn=unique(tmp+1,tmp+1+n)-tmp-1;
		for(int i=1;i<=tn;i++) to[tmp[i]]=i;
		build(1,1,tn);
		for(int i=1;i<=n;i++){
			cout<<i-query(m-a[i],1,1,tn)-1<<" ";
			chp(to[a[i]],1,a[i],1,1,tn);
		}
		cout<<"\n";
	}
	return 0;
}

C

赛时\(50\text{ pts}\)思路

\(50\text{ pts}\)比较的送:

  • \(20\text{ pts}\)\(k=1\)的特殊性质求前缀和最小值即可。
  • \(30\text{ pts}\)\(n\le 100\)可以打\(O(n^3)\) DP,令\(f[i][j]\)\(a[1\sim i]\)\(j\)段的答案,\(S\)\(a\)的前缀和数组,那么有转移:

    \[f[i][j]=\min\limits_{k\in[i-1,j-1]}\Big(\max(f[k][j-1],S[i]-S[k])\Big) \]

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define N 100010
using namespace std;
int T,n,k,a[N],len[N],f[102][102];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>T;
	while(T--){
		cin>>n>>k;
		for(int i=1;i<=n;i++) cin>>a[i];
		if(k==1){
			int ans=LLONG_MAX;
			for(int i=1;i<=n;i++){
				a[i]+=a[i-1];
				ans=min(ans,a[i]);
			}
			cout<<ans<<"\n";
			continue;
		}
		for(int i=1;i<=n;i++) a[i]+=a[i-1];
		memset(f,0x3f,sizeof f);
		for(int i=1;i<=n;i++) f[i][1]=a[i];
		for(int i=2;i<=n;i++)
			for(int j=2;j<=i;j++)
				for(int k=j-1;k<i;k++)
					f[i][j]=min(f[i][j],max(f[k][j-1],a[i]-a[k]));
		int ans=LLONG_MAX;
		for(int i=k;i<=n;i++) ans=min(ans,f[i][k]);
		cout<<ans<<"\n";
	}
	return 0;
}

正解

这道题很像是P1182 数列分段 Section II的加强版。

  • 首先存在负权,不能贪心计数。
  • 其次这道题可以对\(a\)的任意一个前缀进行分段。

但我们看到最大值最小化,还是首先想到二分蛀牙值的最大值\(mid\)

我们用\(f[i]\)表示\(a[1\sim i]\)最多分多少段,使得每段和都\(\le mid\),有转移\(f[i]=\max(f[j]+1)\),其中\(j\)满足\(j<i,S[i]-S[j]\le mid\)。如果存在\(f[i]\ge K\)则合法(这是因为只要存在\(f[i]>K\),就一定存在\(j<i\)使得\(f[j]=K\))。

时间复杂度\(O(n^2\log V)\),时间开销主要在于\(O(n)\)的状态转移。

\(S[i]-S[j]\le mid\)移项得\(S[j]\ge S[i]-mid\)。我们考虑用\(S[u]\)作为索引,\(f[u]\)作为值,扔到线段树里。这样我们更新的时候求线段树\((S[i]-mid)\sim V\)的最大值\(maxx\),用\(maxx+1\)更新\(f[i]\)就可以了。另外,为了保证\(j<i\),我们必须顺序遍历,这样才能保证计算\(f[i]\)时只有\(f[1\sim (i-1)]\)被扔进了线段树(况且不顺序遍历就无法递推了)。

\(V\)实在是太大,因此我们需要将所有用到的下标(即\(S[i]\)与所有\(S[i]-mid\))离散化。

就酱。

时间复杂度降为\(O(n\log n\log V)\)


不过线段树常数太大会被卡成\(80\text{ pts}\),可以用树状数组来代替,虽然它无法直接维护区间最大值,但我们发现每次询问求的都是后缀和,所以可以直接把树状数组反着建,这样每次查询后缀就可以了~

线段树
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
#define int long long
#define lc(x) ((x)<<1)
#define rc(x) ((x)<<1|1)
#define N 100010
using namespace std;
using namespace __gnu_pbds;
int t,n,k,a[N],maxx[N<<3],v[N<<1],f[N],idx;
gp_hash_table<int,int> ma;
void update(int x){maxx[x]=max(maxx[lc(x)],maxx[rc(x)]);}
void build(int x,int l,int r){
	maxx[x]=LLONG_MIN;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(lc(x),l,mid),build(rc(x),mid+1,r);
}
void chp(int a,int v,int x,int l,int r){
	if(l==r) return (void)(maxx[x]=max(maxx[x],v));
	int mid=(l+r)>>1;
	if(a<=mid) chp(a,v,lc(x),l,mid);
	else chp(a,v,rc(x),mid+1,r);
	update(x);
}
int query(int a,int b,int x,int l,int r){
	if(a<=l&&r<=b) return maxx[x];
	int mid=(l+r)>>1,ans=LLONG_MIN;
	if(a<=mid) ans=max(ans,query(a,b,lc(x),l,mid));
	if(b>mid) ans=max(ans,query(a,b,rc(x),mid+1,r));
	return ans;
}
bool check(int mid){
	idx=0,ma.clear();
	for(int i=1;i<=n;i++) v[++idx]=a[i],v[++idx]=a[i]-mid;
	v[++idx]=0;
	sort(v+1,v+1+idx); 
	int tn=unique(v+1,v+1+idx)-v-1;
	for(int i=1;i<=tn;i++) ma[v[i]]=i;
	build(1,1,tn),chp(ma[0],(f[0]=0),1,1,tn);
	for(int i=1;i<=n;i++){
		f[i]=query(ma[a[i]-mid],tn,1,1,tn)+1;
		if(f[i]>=k) return 1;
		chp(ma[a[i]],f[i],1,1,tn);
	}
	return 0;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>t;
	while(t--){
		cin>>n>>k;
		for(int i=1;i<=n;i++) cin>>a[i],a[i]+=a[i-1];
		int l=-1e14,r=1e14;
		while(l<r){
			int mid=(l+r)>>1;
			if(check(mid)) r=mid;
			else l=mid+1;
		}
		cout<<l<<"\n";
	}
	return 0;
}
树状数组
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
#define int long long
#define N 100010
using namespace std;
using namespace __gnu_pbds;
int t,n,k,a[N],v[N<<1],f[N],idx,maxx[N<<1],tn;
gp_hash_table<int,int> ma;
inline int lowbit(int x){return x&-x;}
void init(){for(int i=1;i<=tn;i++) maxx[i]=LLONG_MIN;}
void opt(int x,int k){while(x) maxx[x]=max(maxx[x],k),x-=lowbit(x);}
int query(int x){int ans=LLONG_MIN;while(x<=tn) ans=max(ans,maxx[x]),x+=lowbit(x);return ans;}
bool check(int mid){
	idx=0,ma.clear();
	for(int i=1;i<=n;i++) v[++idx]=a[i],v[++idx]=a[i]-mid;
	v[++idx]=0;
	sort(v+1,v+1+idx); 
	tn=unique(v+1,v+1+idx)-v-1;
	for(int i=1;i<=tn;i++) ma[v[i]]=i;
	init(),opt(ma[0],(f[0]=0));
	for(int i=1;i<=n;i++){
		f[i]=query(ma[a[i]-mid])+1;
		if(f[i]>=k) return 1;
		opt(ma[a[i]],f[i]);
	}
	return 0;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>t;
	while(t--){
		cin>>n>>k;
		for(int i=1;i<=n;i++) cin>>a[i],a[i]+=a[i-1];
		int l=-1e14,r=1e14;
		while(l<r){
			int mid=(l+r)>>1;
			if(check(mid)) r=mid;
			else l=mid+1;
		}
		cout<<l<<"\n";
	}
	return 0;
}

这样想来,这道题如果没有了第二条“加强”还真不好做,因为不满足单调性,所以\(f_i\)成立 \(\nRightarrow f_i +1\)也成立。如果只有第一条“加强”,要求\(a\)的答案,就不能设\(f\)为最大值,而是要从前面所有可能的值进行转移,这样还得额外乘一个\(O(n)\)的时间复杂度。目前还想不到足够优秀的复杂度来解决这个新的问题。如果大家有想法,欢迎在评论区讨论!

D

不补了(逃

posted @ 2024-11-23 20:19  Sinktank  阅读(19)  评论(0编辑  收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2024 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.