T1 Maximum
题意:给出一个序列a,求其中任意两项进行与/或/异或运算后的最大值
30分解法:暴力枚举两个数,复杂度 <nobr> O(n2) </nobr>
60分解法:注意到有 <nobr> %30 </nobr>的数据数字小于1024,可以用一个cnt数组记录出现过的值,然后从1到1024枚举,复杂度 <nobr> O(10242) </nobr>
100分解法:
先将60分特判掉,然后考虑到大数被选的几率大,假设出题人很懒,不想构造坑,将数据排序一下,我们从最大的数中选200个,从最小的数中选200个,暴力枚举答案, <nobr> O(nlongn+q∗2002) </nobr>,然后就莫名其妙的A了。这就是正解你信吗
正解:
每一种运算需要分开考虑
- &:按位贪心,从高到低每一位枚举,如果所有数字第i位上1的个数>=2 ,
则答案的第 i 位就可以是1,同时把第i位上不为1的数字删去,复杂度 <nobr> O(20∗n) </nobr>。 - | :从高位到低,如果某一位是0,需要判断能否为这个数配上一个1,类似于前缀和地预处理一下即可复杂度 <nobr> O(20∗n) </nobr>。
- ^ : 建一棵字典树,每一个节点只有0/1两个分支,所以是个二叉树,将每一个数字依次插入字典树中,每插入一个数之前统计其他数和这个数异或值最大是多少,同样贪心地从高到低位查询每一位是否有不同的值即可,复杂度 <nobr> O(20∗n) </nobr>。
PS:可以用FWT加速(WTF)
//by sdfzchy
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int M=1048576;
const int N=100010;
int n,c;
int num[N],ans,a[M],base[21],u[N],maxn;
bool used[N],tr[(M<<1)+5];
void solveand(int pos)
{
if(pos==0) return;
int tot=0,cnt=0;
for(int i=1;i<=n;i++)
if(!used[i])
{
if(num[i]&base[pos-1]) ++cnt;
else u[++tot]=i,used[i]=1;
}
if(cnt>=2){ solveand(pos-1) ; ans+=(1ll<<(pos-1)) ; }
else { for(int i=1;i<=tot;i++) used[u[i]]=0 ; solveand(pos-1); }
}
void add(int rt,int len,int x)
{
tr[rt]=1;
if(len==-1) return ;
if(x&base[len]) add(rt<<1|1,len-1,x);
else add(rt<<1,len-1,x);
}
int Query(int rt,int len,int x)
{
if(len==-1) return 0;
if(x&base[len])
{
if(tr[rt<<1]) return Query(rt<<1,len-1,x)+base[len];
return Query(rt<<1|1,len-1,x);
}
if(tr[rt<<1|1]) return Query(rt<<1|1,len-1,x)+base[len];
return Query(rt<<1,len-1,x);
}
void solvexor(int maxn)
{
memset(tr,0,sizeof(tr));
add(1,19,num[1]);
for(int i=2;i<=n;i++)
{
ans=max(ans,Query(1,19,num[i]));
add(1,19,num[i]);
}
}
void solveor(int maxn)
{
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++) a[num[i]]=1;
for(int i=0;i<20;i++)
for(int j=1;j<M;j++)
if(a[j]&&(j&base[i])) a[j^base[i]]=1;
int ret,tmp;
for(int i=1;i<=n;i++)
{
ret=tmp=0;
for(int j=19;j>=0;j--)
if(num[i]&base[j]) ret|=base[j];
else if(a[tmp|base[j]]) {ret|=base[j];tmp|=base[j];}
ans=max(ans,ret);
}
}
int main()
{
freopen("maximum.in","r",stdin);
freopen("maximum.out","w",stdout);
int T;
scanf("%d",&T);
base[0]=1;
for(int i=1;i<=20;i++) base[i]=base[i-1]*2;
while(T--)
{
maxn=ans=0;
scanf("%d%d",&n,&c);
for(int i=1;i<=n;i++)
{
scanf("%d",&num[i]);
maxn=max(maxn,num[i]);
}
maxn=log(maxn)/log(2)+1;
//cout<<maxn<<endl;
if(c==1)
{
solveand(maxn);
memset(used,0,sizeof(used));
}
else if(c==2)
{
solvexor(maxn);
}
else solveor(maxn);
printf("%d\n",ans);
}
return 0;
}
T2 bridge
题意:给定若干坐标轴上的点的坐标和移动方向,每秒移动一个单位长度,两个点相遇瞬间改变方向,求某时间之后某个点的位置,每组数据包括多次查询;
题解:
- 10分:大力模拟
- 我也不知道多少分:我们发现了两个规律,第一,只考虑点的位置,两个点相遇之后,可以看成运动方向没有变,只是两个点的编号互换,相当于他们互穿而过;第二,每一个点在整个序列中的相对排名不会变,直观的解释是一个点只会在相邻的两个点之间碰来碰去,那么发现这两个规律之后,我们可以每次直接计算出一个点可以到达的最终位置,排序之后找出第k大即可
- 接近满分:std的算法,结合上面的规律,考虑这样一个性质,将所有初始方向相同的点归为一类,那么只考虑点的位置,这些点最终的相对位置其实是不变的,所有向右走的终点都是 <nobr> pos[i]+t </nobr>,向左走的终点是 <nobr> pos[i]−t </nobr>,那么问题可以转化成在两个排好序的数组中,找出归并后第k大的元素。考虑二分答案,我们二分一个初始位置,每次check一下a数组和b数组的小于这个值的总数是否<=k,在每一个数组里再次二分查找第一个大于二分出答案的位置即可,注意相对位置的变化,复杂度 <nobr> O(nlogn+q∗log2n) </nobr>
- 100分:考虑解决两个排序好的数组求归并后第k大的更优解法,每次二分两个数组的中间值,把较大的中间值的后半段区间砍去,直到二分出答案,
- 复杂度 <nobr> O(nlogn+q∗logn) </nobr>
100分做法其实是一个经典模型,即两个排序好的数组求归并好后的第k大,附讲解,戳一戳
//by sdfzchy
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
const int N=2e5+5;
const long long inf=(1ll<<40);
long long a[N],b[N];
int id[N],q;
struct node
{
int pos,d,id;
bool operator <(const node& a)const
{
return pos<a.pos;
}
}d[N];
inline int binary(long long x,long long *c)
{
if(!c[0]) return 0;
if(x<=c[1]) return 0;
if(x>c[c[0]]) return c[0];
int mid,L=1,R=c[0],xx=x;
while(L<R)
{
mid=(L+R+1)>>1;
if(c[mid]<xx) L=mid;
else R=mid-1;
}
return L;
}
void work()
{
int x;
long long minl=inf,maxn=-inf,t;
scanf("%d%lld",&x,&t);
x=id[x];x++;
if(a[0]) minl=min(minl,a[1]-t),maxn=max(maxn,a[a[0]]-t);
if(b[0]) minl=min(minl,b[1]+t),maxn=max(maxn,b[b[0]]+t);
long long l=minl,r=maxn,mid;
// cout<<l<<" "<<r<<endl;
while(l<r)
{
mid=(l+r+1)>>1;
if(binary(mid+t,a)+binary(mid-t,b)<x) l=mid;
else r=mid-1;
}
printf("%lld\n",l);
}
int main()
{
freopen("bridge.in","r",stdin);
freopen("bridge.ans","w",stdout);
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&d[i].pos);
for(int i=0;i<n;i++) scanf("%d",&d[i].d),d[i].id=i;
sort(d,d+n);
for(int i=0;i<n;i++) id[d[i].id]=i;
for(int i=0;i<n;i++)
if(d[i].d==0) a[++a[0]]=d[i].pos;
else b[++b[0]]=d[i].pos;
sort(a+1,a+a[0]+1);
sort(b+1,b+b[0]+1);
scanf("%d",&q);
while(q--) work();
return 0;
}
T3 Group
题意:对一个序列分组,使得每一组数据的极差的总和不超过K,求分组的方案数
分析:用 <nobr> F[I][J][k] </nobr>表示前i个点,还有j组没有确定最大值,一共极差和已经是k的情况一共有多少种,那么最终的答案就是 <nobr> F[N][0][K] </nobr>, <nobr> K<=kmaxn </nobr>的和
对于每个点有四种情况,一种是新建一组不加,一种是新建一组等待最大值,一种是加入一组等待最大值,一种是加入一组不加。注意,要处理加入一组的最大值问题,本题输入顺序需要排序。
然后就是,对于代价,用差分的方式,每次加上两个点的差乘以j,因为只有没确定最大值的点才需要继续加,每确定一个点,它修改的状态的j就减一,保证了该状态的该组不再被加。
代码稍后补