[题解][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
不补了(逃