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\) 也要一并离散化
  • 代码中树状数组求的是后缀和而非前缀和,具体来说,将 addquery 中的 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_{i,j} = \max \left\{ \begin{aligned} &f_{i-1,j}+d_i\\ &f_{i,j-1}+e_j\\ &f_{i-1,j-1}+a_i+b_j-c_{i,j} \end{aligned} \right. \]

分别表示卖第一条传送带上的宝石,卖第二条传送带上的宝石和加工两宝石再卖。

注意 \(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;
}
posted @ 2024-10-09 16:25  Jerrycyx  阅读(85)  评论(0编辑  收藏  举报