2023牛客OI赛前集训营-提高组(第三场) - 题解汇总
空位与数(game)
贪心即可,因为正正得正,负负也得正,所以将两个数组分别按照正负性分开,然后让正数里面大的配上大的,负数里面绝对值大的配上绝对值大的,这样可以让正积总和尽量大。剩下不足的(必须要一正一负相乘的)让绝对值大的配绝对值小的,这样可以让负积总和尽量小。
#include<cstdio> #include<algorithm> using namespace std; const int N=1e5+5; int n,m,a[N],b[N]; int a1n,a2n,b1n,b2n; long long a1[N],a2[N],b1[N],b2[N]; long long ans=0; bool cmp(int x,int y){return x>y;} int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); if(a[i]>=0) a1[++a1n]=a[i]; else a2[++a2n]=a[i]; } sort(a1+1,a1+a1n+1,cmp); sort(a2+1,a2+a2n+1); for(int i=1;i<=m;i++) { scanf("%d",&b[i]); if(b[i]>=0) b1[++b1n]=b[i]; else b2[++b2n]=b[i]; } sort(b1+1,b1+b1n+1,cmp); sort(b2+1,b2+b2n+1); for(int i=1;i<=a1n;i++) { if(i<=b1n) ans+=a1[i]*b1[i]; else ans+=a1[i]*b2[b2n-(i-b1n)+1]; } for(int i=1;i<=a2n;i++) { if(i<=b2n) ans+=a2[i]*b2[i]; else ans+=a2[i]*b1[b1n-(i-b2n)+1]; } printf("%lld\n",ans); return 0; }
机场滞留!(airport)
我采用的是树状数组(不是权值树状数组!)+二分的做法。
设原数列为 \(a\)。
贪心思想:体重越小的人越优先上车,即找上车的人的时候应当从小到大找人。
离线处理:对于每组测试数据,读入所有数后排序来并找到每个数在排序后新的位置,然后在处理询问的时候一个一个插入到对应的位置上,设有序的数列为 \(b\)。
每次询问的时候,找到第一个 \(\sum_{k=1}^{i-1}b_k \le m-a_i\) 的 \(k\)(此时 \(a\) 中只有 \(a_{1}\) 到 \(a_{i-1}\) 已经被放到 \(b\) 当中了),在 \(k\) 之前已经被放入的数的数量就是除了 \(a_i\) 外可以上车的人数,此时的答案就是 \(i-1-k\)。
因为 \(b_i \ge 0\),所以 \(b\) 具有单调性,可以二分查找。
求取和更新 \(\sum_{k=1}^{i-1}b_k\) 可以使用树状数组。
找“在 \(k\) 之前已经被放入的数的数量”可以再开一个树状数组,与 \(b\) 同步同位置更新,只是每次加的数是 \(1\),找到 \(k\) 以后直接求这个树状数组里 \(k\) 位置的前缀和就是“在 \(k\) 之前已经被放入的数的数量”。
时间复杂度 \(O(N \log M \log N) = O(N \log MN)\)
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; const int N=1e5+5; int T,n,m,a[N],ans[N]; pair<int,int> p[N]; int pos[N]; struct BIT{ int c[N]; inline int lowbit(int x){return x&-x;} void add(int x,int y) { for(;x<=n;x+=lowbit(x)) c[x]+=y; return; } int query(int x) { int res=0; for(;x;x-=lowbit(x)) res+=c[x]; return res; } void clear() { for(int i=1;i<=n;i++) c[i]=0; return; } }weight,num; int BinSch(int x) { int l=0,r=n+1; while(l+1<r) { int mid=l+r>>1; if(weight.query(mid)<=x) l=mid; else r=mid; } return num.query(l); } int main() { ios::sync_with_stdio(false); cin>>T; while(T--) { cin>>n>>m; for(int i=1;i<=n;i++) { cin>>a[i]; p[i]={a[i],i}; } sort(p+1,p+n+1); for(int i=1;i<=n;i++) pos[p[i].second]=i; for(int i=1;i<=n;i++) { ans[i]=i-1-BinSch(m-a[i]); weight.add(pos[i],a[i]); num.add(pos[i],1); } for(int i=1;i<=n;i++) cout<<ans[i]<<' '; cout<<'\n'; weight.clear(),num.clear(); } return 0; }
糖果与蛀牙(candy)
特殊性质:K=1
只有一个小朋友分糖果,找 \(\max_{i=1}^{n}\{\sum_{j=1}^{i}a_j\} = \max_{i=1}^{n}\{sum_j\}\) 即可
另 30 分:1≤N≤100
题目要求“分到糖果的蛀牙值最大值最小是多少”,“最大值最小”是二分的经典标志,问题转化为 check()
怎么写。
设 \(f_i\) 表示前 \(i\) 个数最多可以分成多少段,使得每段的总和都 \(\le mid\),。
注意:原数列中的元素 \(a_i\) 有可能是负数,所以不能够使用传统的“如果该段总和大于 \(mid\) 就开下一段”的做法,因为可能虽然此时总和大于 \(mid\),是非法段,也有可能后面加上一个负数以后又成为合法段。
因此这里我们使用动态规划的方法,\(f_i = \max_{j=0}^{i-1}\{f_j+1\}\),其中 \(sum_i - sum_j \le mid\),即 \(a_{j+1}\) 到 \(a_i\) 这一段的和需要小于等于 \(mid\),才可以作为一段。
时间复杂度 \(O(N^2 \log N)\)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=1e5+5; int T,n,k; long long a[N],sum[N]; int f[N]; bool check(long long x) { for(int i=1;i<=n;i++) f[i]=-0x3f3f3f3f; for(int i=1;i<=n;i++) { for(int j=0;j<i;j++) if(sum[i]-sum[j]<=x) f[i]=max(f[i],f[j]+1); if(f[i]>=k) return true; } return false; } long long BinSch() { long long l=-1e14-1,r=1e14+1; while(l+1<r) { long long mid=l+r>>1; if(check(mid)) r=mid; else l=mid; } return r; } int main() { scanf("%d",&T); while(T--) { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) { scanf("%lld",&a[i]); sum[i]=sum[i-1]+a[i]; } if(k==1) { long long ans=1e18; for(int i=1;i<=n;i++) ans=min(ans,sum[i]); printf("%lld\n",ans); } else { printf("%lld\n",BinSch()); } } return 0; }
100 分:1≤N≤100000
可以看到上述算法的时间瓶颈在于 \(O(N^2)\) 的 check()
,所以我们可以优化一下这个函数。
转化式子 \(sum_i - sum_j \le mid\) 可以得到 \(sum_j \ge sum_i - mid\),我们要求的就是满足这个条件的最大的 \(f_j\)。
开一个权值树状数组,下标表示 \(sum_i\),它存储的值表示 \(sum_i\) 的后缀的(即所有 \(sum_j > sum_i\) 的)最大 \(f_i\) 值,这样,查询可以 query(sum[i]-mid);
,而每次找到一个 \(f_i\) 以后执行 update(sum[i],f[i]);
即可。
还有一些细节:
- 因为值域过大且含负数(\([-10^{14},10^{14}]\)),所以需要先离散化。注意要离散化的除了所有的 \(sum_i\),还有每次的 \(sum_i - mid\),因为
check()
中会查询这两组数。 - 因为 \(f_0=0\),所以 \(0\) 也要一并离散化
- 代码中树状数组求的是后缀和而非前缀和,具体来说,将
add
和query
中的for(;x<=n;x+=lowbit(x))
和for(;x;x-=lowbit(x))
调换了位置。 - 注意有些地方要开
long long
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=1e5+5; int T,n,k,a[N]; long long sum[N]; struct BIT{ int c[N<<1]; BIT(){clear();} inline int lowbit(int x){return x&-x;} void add(int x,int y) { for(;x;x-=lowbit(x)) //后缀和 c[x]=max(c[x],y); return; } int query(int x) { int res=-0x3f3f3f3f; for(;x<=(n<<1)+1;x+=lowbit(x)) //后缀和 res=max(res,c[x]); return res; } void clear() { for(int i=0;i<=(n<<1)+1;i++) c[i]=-0x3f3f3f3f; return; } }bit; long long disc[N<<1]; int len; void Discrite(long long arr[],int size) //离散化 { for(int i=1;i<=size;i++) disc[i]=arr[i]; sort(disc+1,disc+size+1); len=unique(disc+1,disc+size+1)-(disc+1); return; } int get_disc(long long x) { return lower_bound(disc+1,disc+len+1,x)-disc; } int f[N]; long long tmp[N<<1]; bool check(long long x) //check()函数,注意文中的mid对应此处的x { for(int i=n+1;i<=n<<1;i++) tmp[i]=sum[i-n]-x; tmp[(n<<1)+1]=0; Discrite(tmp,(n<<1)+1); bit.clear(); f[0]=0; bit.add(get_disc(0),f[0]); for(int i=1;i<=n;i++) { f[i]=bit.query(get_disc(sum[i]-x))+1; if(f[i]>=k) return true; bit.add(get_disc(sum[i]),f[i]); } return false; } long long BinSch(long long L,long long R) //二分查找 { long long l=L-1,r=R+1; while(l+1<r) { long long mid=l+r>>1; if(check(mid)) r=mid; else l=mid; } return r; } signed main() { scanf("%d",&T); while(T--) { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); tmp[i]=sum[i]=sum[i-1]+a[i]; } if(k==1) //特殊性质,卡20分用的,这里删去也行 { long long ans=1e18; for(int i=1;i<=n;i++) ans=min(ans,sum[i]); printf("%lld\n",ans); } else printf("%lld\n",BinSch(-1e14,1e14)); } return 0; }
宝石宝石!(diamond)
暴力
DP + 卡常
设 \(f_{i,j}\) 表示第一条传送带上 \([l_1,i]\) 和第二条传送带上 \([l_2,j]\) 的最大利润。
分别表示卖第一条传送带上的宝石,卖第二条传送带上的宝石和加工两宝石再卖。
注意 \(f\) 的初始化,详见代码。
时间复杂度 \(O(QNM)\)
暴力优化
注意到每次只有当 \(l_1\) 或 \(l_2\) 不相等时,才需要重新计算动态规划的 \(f\) 数组,所以我们离线处理所有询问,将所有 \(l_1,l_2\) 均相等的询问归为一组,这一组可以只计算一次 \(f\) 数组,对应的 \(r_1\) 和 \(r_2\) 则是这一组中所有 \(r_1\) 的最大值和所有 \(r_2\) 的最大值。
特殊性质:N=1
(标题行不加 \(\KaTeX\) 是因为目录识别会出错)
当第一条传送带上只有一个宝石的时候,答案只可能是加工这颗宝石和不加工这颗宝石两种。
当不加工这颗宝石的时候,答案就是所有的宝石变卖价格之和,即 \(\sum_{i=l_2}^{r_2}e_i + d_1\),可以前缀和快速求出。
当加工这颗宝石的时候,第二条传送带中加工的这颗宝石 \(i\) 损失了 \(e_i\) 的价值而获得了 \((a_1+b_i-c_{1,i})\) 的价值,所以答案就是 \(\sum_{i=l_2}^{r_2}e_i + \max_{i=l_2}^{r_2}\{a_1+b_i-c_{1,i}\}\),可以用 ST 表处理 \(\max_{i=l_2}^{r_2}\{a_1+b_i-c_{1,i}\}\)。
时间复杂度 \(O(Q \log M)\)
总结
暴力优化 + 特判特殊性质即可卡过这道题。
才不是因为我打不来正解。
#include<cstdio> #include<vector> #include<algorithm> using namespace std; const int N=5e4+10,Q=1e5+5; int n,m,q; long long a[N],b[N],d[N],e[N]; vector<long long> c[N],f[N]; long long esum[N]; namespace ST_Table{ int lg2[N]; long long st[N][25]; void Init() { for(int i=1;i<=m;i++) st[i][0]=a[1]+b[i]-c[1][i]-e[i]; for(int i=2;i<=m;i++) lg2[i]=lg2[i>>1]+1; for(int k=1;k<=lg2[m];k++) for(int i=1;i+(1<<k)-1<=m;i++) st[i][k]=max(st[i][k-1],st[i+(1<<k-1)][k-1]); return; } long long query(int l,int r) { int p=lg2[r-l+1]; return max(st[l][p],st[r-(1<<p)+1][p]); } } inline long long max(long long x,long long y,long long z){return max(x,max(y,z));} void Solve(int l1,int r1,int l2,int r2) { for(int i=l1-1;i<=r1;i++) for(int j=l2-1;j<=r2;j++) f[i][j]=0; for(int i=l1;i<=r1;i++) f[i][l2-1]=f[i-1][l2-1]+d[i]; for(int i=l2;i<=r2;i++) f[l1-1][i]=f[l1-1][i-1]+e[i]; for(int i=l1;i<=r1;i++) for(int j=l2;j<=r2;j++) f[i][j]=max(f[i-1][j]+d[i],f[i][j-1]+e[j],f[i-1][j-1]+a[i]+b[j]-c[i][j]); return; } struct QS{ int l1,r1,l2,r2; int id; }qs[Q]; long long ans[Q]; bool cmp(QS x,QS y) { if(x.l1!=y.l1) return x.l1<y.l1; if(x.l2!=y.l2) return x.l2<y.l2; return x.id<y.id; } int main() { scanf("%d%d%d",&n,&m,&q); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); for(int i=1;i<=m;i++) scanf("%lld",&b[i]); for(int i=1;i<=n;i++) scanf("%lld",&d[i]); for(int i=1;i<=m;i++) scanf("%lld",&e[i]); c[0].resize(m+5,0),f[0].resize(m+5,0); for(int i=1;i<=n;i++) { c[i].resize(m+5,0),f[i].resize(m+5,0); for(int j=1;j<=m;j++) scanf("%lld",&c[i][j]); } if(n==1) { ST_Table::Init(); for(int i=1;i<=m;i++) esum[i]=esum[i-1]+e[i]; while(q--) { int l1,r1,l2,r2; scanf("%d%d%d%d",&l1,&r1,&l2,&r2); printf("%lld\n",max(esum[r2]-esum[l2-1]+d[1], esum[r2]-esum[l2-1]+ST_Table::query(l2,r2))); } } else { for(int i=1;i<=q;i++) { scanf("%d%d%d%d",&qs[i].l1,&qs[i].r1,&qs[i].l2,&qs[i].r2); qs[i].id=i; } sort(qs+1,qs+q+1,cmp); for(int i=1,j=1;i<=q;i=j) { int maxr1=0,maxr2=0; for(j=i;j<=q && qs[j].l1==qs[i].l1&&qs[j].l2==qs[i].l2;j++) maxr1=max(maxr1,qs[j].r1),maxr2=max(maxr2,qs[j].r2); Solve(qs[i].l1,maxr1,qs[i].l2,maxr2); for(int k=i;k<j;k++) ans[qs[k].id]=f[qs[k].r1][qs[k].r2]; } for(int i=1;i<=q;i++) printf("%lld\n",ans[i]); } return 0; }
本文采用 「CC-BY-NC 4.0」 创作共享协议,转载请注明作者及出处,禁止商业使用。
作者:Jerrycyx,原文链接:https://www.cnblogs.com/jerrycyx/p/18454533
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步