原題頁面: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;
}