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