[題解](更新中)[test06]2024/11/23 模擬賽 / 2023牛客OI賽前集訓營-提高組(第三場) A~C

Sinktank發表於2024-11-23

原題頁面: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\)則合法。

時間複雜度\(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\)更新\(S[i]\)就可以了。另外,為了保證\(j<i\),我們必須順序遍歷,這樣才能保證計算\(f[i]\)時只有\(f[1\sim (i-1)]\)被扔進了線段樹(況且不順序遍歷就無法遞推了)。

\(V\)實在是太大,因此我們需要將所有用到的下標(即\(S[i]\)與所有\(S[i]-mid\))離散化。

就醬。


不過線段樹常數太大會被卡成\(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;
}

相關文章