【洛谷P6747/贪心】Teleport 转自自己的洛谷博客

洛谷P6747 Teleport

这道题有一些思维,同时又不是那么繁琐,还比较好想

(毕竟我这种蒟蒻都想出来了)

记得我想这道题的时候是两个星期前了差不多,当时一会儿想出来了一个假的做法 到今天尝试实现的时候,发现除了预处理的部分都错了。 所以先介绍预处理

思考算法的时候我比较习惯先看数据范围,看到n为1e5,q为1e5,显然不能用q * n的做法,也就是说不能够每次都扫描一遍序列,所以直接就没想扫描序列怎么做。 显然要对序列a先做些什么

考虑异或的性质

当a的j位异或1的时候,必然改变原来的数字 原来是1就变成0,原来是0就变为1

当a的j位异或0的时候,什么影响都没有。

再考虑a的和,可以变化为所有a在二进制下每一位的和,也就是第j位的和,是可以线性处理出来的,所有j位的和的和,就是原序列的和(虽然我们未必用到原序列的和)。

举个例子

3+5用刚刚的方法来想 就是 (11)2+(101)2 

两个数第零位的和sum[0]是1+1=2

两个数第1位的和sum[1]是1*21+0*21=2+0=2

两个数第三位的和就是   022+122=4

所有的sum的和依然是8,但是我们相当于直接把最后的和给二进制拆分了并存进了sum里,方便我们考虑这一位的k如果是1是个什么情况。

结合异或的性质 如果k的第j位是1,显然sum[j]会进行一定的改变,比如如果序列是3+5的话,即二进制下的11 和 101,


3的原二进制表示0113的每一位都异或1100
5的原二进制表示 1 0 1 5的每一位都异或1 0 1 0
每一位和的十进制表示 4 2 2 如上得到的十进制和 4 2 0

我们发现,每一位的二进制异或1之后,序列的0和1是翻过来的,如果把原序列的j位之和记做sum[j],异或1后记做sum1[j],再记j位上全是1的和为sum2[j] (即2^j n),存在sum2[j]=sum[j]+sum1[j],知2求1,sum2和sum都可以在输入a序列的时候直接处理出来,之后再多一个for,求出所有sum1,预处理结束*


n=qr();
	for(int i=1;i<=n;i++)
	{
		a[i]=qr();
		for(int j=0;j<=50;j++)
		{
		sum[j]+=(a[i]&((ll)1<<j)); //累加所有a的j位。k的j位为0时的结果。
		sum2[j]+=((ll)1<<j);//累加每一位都为1的结果。 
		}
	}
	for(int j=0;j<=50;j++)
	{
		sum1[j]=sum2[j]-sum[j];//k的j位为1时a的j位的累加和。 
	}

然后考虑如何找到最大的k,首先把k拆分成二进制的话,一定那么k中的1所在位越高越好,我原先想的是每次二分查找找到第一个比m小的sum1,找不到再找sum之类的,然后发现找到sum1是有后效性的,也就是说可能找到了一个比较大的sum1,然而后面的数不管怎么凑都不能让其余位上sum或sum1的和小于m,结果就找不到了。

接着想到了类似估价函数的方法,即设计一个minj,表示j位及以下的a[j]最小总和是多少。这个预处理只需要在原来的预处理加一点(最后两行)


	for(int i=1;i<=n;i++)
	{
		a[i]=qr();
		for(int j=0;j<=50;j++)
		{
		sum[j]+=(a[i]&((ll)1<<j)); //累加所有a的j位。k的j位为0时的结果。
		sum2[j]+=((ll)1<<j);//累加每一位都为1的结果。 
		}
	}
	for(int j=0;j<=50;j++)
	{
		sum1[j]=sum2[j]-sum[j];//k的j位为1时a的j位的累加和。 
		if(!j) minj[j]=min(sum1[j],sum[j]); 
		else minj[j]=minj[j-1]+min(sum1[j],sum[j]);//选择二者更小的那个 
	}

这样我们就可以在每次查询中对所有的j位进行处理。

**如果处理到某一步m-sum1[j]-minj[j-1]>=0, 就代表如果k的第j位选择了1之后,下面不管取了多少个1,总能有合法的结果(即保证a序列的处理后的和小于等于m),这个时候k+=2^j,m-=sum[j],并且直接continue,因为我们已经达到了这一步的最优结果,不需要继续考虑了。 **

**当然可能sum1[j]比较大,一减下去就小于0了,那再去看看m-sum[j]-minj[j-1]>=0与否,也就是不异或1,如果不异或1才能合法,那么只好m-=sum[j],k+0,也就是不操作。当然如果两步都不满足,标记一个flag=1,也就是无论怎么操作,a序列的和总是要大于m,让飞船爆炸的。 ** 完整代码


#include<bits/stdc++.h>
using namespace std;
typedef __int128 ll;
inline ll qr()
{
	ll x=0,f=0;char ch=0;
	while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return f?-x:x;
}
inline void print(ll x){
	if(x<0){putchar('-');x=-x;}
    if(x>9) print(x/10);
    putchar(x%10+'0');
}
const int maxn=1e5+10;
int n,q;
ll m;
ll k;
ll a[maxn];
ll sum1[51],sum[51],sum2[51];//序列a第j位异或1的和,原序列a第j位之和,第j位全为1的和。 
ll minj[51];//j位及以下的最小总和是多少。 
int flag;
int main()
{
	n=qr();
	for(int i=1;i<=n;i++)
	{
		a[i]=qr();
		for(int j=0;j<=50;j++)
		{
		sum[j]+=(a[i]&((ll)1<<j)); //累加所有a的j位。k的j位为0时的结果。
		sum2[j]+=((ll)1<<j);//累加每一位都为1的结果。 
		}
	}
	for(int j=0;j<=50;j++)
	{
		sum1[j]=sum2[j]-sum[j];//k的j位为1时a的j位的累加和。 
		if(!j) minj[j]=min(sum1[j],sum[j]); 
		else minj[j]=minj[j-1]+min(sum1[j],sum[j]);//选择二者更小的那个 
	}
	q=qr();
	for(int i=1;i<=q;i++)
	{
		m=qr();k=0;flag=0;
		for(int j=50;j>=0;j--)
		{
			if(j==0){//到达边界了,前面的判断都给过去了,这里只用处理sum就好了,一定至少有一个能让它合法。 
				if(m-sum1[j]>=0)
				{
					m-=sum1[j],k+=1;//每次m减去这一位的和
					continue;	
				}
				if(m-sum[j]>=0)
				{
					m-=sum[j];
					continue;
				}
			}
			if(m-minj[j-1]-sum1[j]>=0)//选择这一位的k能够为1的同时保证下面能够凑出来保证合法的数,这大概算是个估价,第一步就可以判断出是否合法 
			{
				m-=sum1[j],k+=((ll)1<<j);//这一位可以为1。k累加上 
				continue;
			}
			if(m-minj[j-1]-sum[j]>=0)//这一位k为1是没戏了,只能为0,加上原aj位的和 
			{
				m-=sum[j]; 
				continue;
			}
			flag=1;	break;//两个都不能用,肯定是坏了,直接break。 
		}
		if(flag==1) //(要用flag判断,而不是k==0,因为k可以等于0)
		{
		printf("%d\n",-1);
		}
		else print(k),printf("\n");
	}
	return 0;
} 

哦对了,虽然我这整个操作中似乎不会爆long long 但是不用int128还就是会爆炸,40pts。 如果有大佬能看出来哪里暴了欢迎指正。

(当然如果有人能看到这篇博文的话)

posted @ 2020-10-05 06:26  explorerxx  阅读(144)  评论(0编辑  收藏  举报