序列子区间问题一般都是问你,求序列子区间的和的和...异或和的和..和的异或和...和是3的倍数的个数...
这类问题一般都是用(前缀和+)记录状态的数组来实现,将O(n*n)的复杂度降为O(k*n)(k为常数)
这类问题可以从如何优化遍历以i为结尾的连续子序列的复杂度为切入点,将O(n)->O(k)
西安电子科技大学第16届程序设计竞赛 E Xieldy And His Password
题意:给你一个串,只由0,1组成,问你该串的子串表示的二进制数是3的倍数的个数(0也是3的倍数,且允许有前导0)
解析:
如果一个二进制数某一位是1,那么它表示2^k,那么2^k%3一定=1 or 2
并且一个二进制数,奇数位(从0开始)上的1一定%3=2,偶数位(从0开始)上的1%3=1
这里一个数要是3的倍数,那么一个偶数位上的1就对应一个奇数位上的1来抵消,
那么这个其实意味着一个1出现在某一位上,那么一定要有一位1跟他相邻偶数位,这样才能抵消它,使得是3的倍数
这样我们就可以一开始就把他按照奇偶位赋值(奇位1,偶位2),一旦num[i]=2,那么前面一定要一个与它相邻偶数位的1,即前面有一奇数位的1
同理num[i]=1,那么前面一定要有一个偶数位的1来抵消它(那么其实奇位2,偶位1也是可以的,两个位置上的数只要互补就可以了)
那么我们就用cnt[]记录当前时刻,前面前缀和%3=0,1,2的相应的个数(不包括当前时刻),
那么我们在遍历到i时,只要看当前i的前缀和%3的余数tt,看cnt[]数组里面有多少个前缀区间值为tt的,
那么相应减去这些前缀区间,就是以i为结尾的满足条件的区间的个数
#include <iostream>
#include<string>
#include<bits/stdc++.h>
#include<cstdio>
using namespace std;
string a;
#define N 1000006
int num[N];
int sum[N];
int cnt[4];
#define ll long long
int main()
{
ios::sync_with_stdio(false);
while(cin>>a){
for(int i = 0;i<a.length();i++){ //在任意一个2进制位上1,表示的2^k%3=1 or 2
if(a[i]=='0')
num[i+1] = 0;
else{
if(i%2)
num[i+1] = 2;
else
num[1+i] = 1;
}
}
memset(cnt,0,sizeof(cnt));
ll ans = 0;
for(int i = 0;i<a.length();i++){
int n = num[i+1];
sum[i+1] = (sum[i]+n)%3;
int tt = sum[i+1];
ans += cnt[tt]; //当前前缀和%3为tt,所以前面只要有前缀和%3为tt,就刚好减掉这个前缀区间,就符合条件
if(sum[i+1]==0) //所以只要加上前面是tt的前缀区间的个数,就可以了,并且这里一定能保证求出来的区间是连续的子区间(以i结尾)
ans++; //同时i的前缀区间满足条件的话,还要加上去,因为只是不需要减的情况
cnt[tt]++; //这里其实就是用cnt[]这个数组优化了遍历(以i结尾)子区间的过程,将O(n)的复杂度降为O(1)
}
cout<<ans<<endl;
}
return 0;
}
题意:
求所有子区间的异或和的和
解析:
这里需要思考的时如何用状态数组来优化遍历到一个点,往前查找答案的过程
因为从值的本身来找状态,无从下手,这里是异或,那么我们就可以从它的二进制来下手
因为是子区间问题,我们要求前缀和,这样才能保证我们的区间是连续的
当遍历到一个前缀和a[i]时,我们需要把以i为结尾的区间的答案全部找出来
这里我们就可以用到二进制了,当a[i]的k位为1时,我们答案要加上2^k*zero[k],zero[k]表示前面前缀区间值对应第k位为0的个数,因为只有与这些前缀区间异或后,
结果中第k位才不为0,这样才能将2^k这个值加到答案里(a[i](k)为1的时候同理)
例如,i>j,sum[j]为j的前缀异或和,sum[i]同理,sum[i](k)表示sum[i]的二进制第k位
当sum[i](k)去遍历前面的前缀区间时,遍历到sum[j]时,如果sum[i](k)=1,sum[j](k)=0,那么[j+1,i]这个区间的异或和的第k位一定是1,这样就可以往答案加上2^k
如果sum[i](k)=0,sum[j](k)=1,那么这个区间的异或和的第k位一定是1,同样可以往答案加上2^k
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define pi acos(-1.0)
#define ll long long
#define mod 20090717
#define C 0.5772156649
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
#define pii pair<int,int>
using namespace std;
const double g=10.0,eps=1e-12;
const int N=200000+10,maxn=200000+10,inf=0x3f3f3f3f;
ll a[N];
ll one[50],zero[50];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],a[i]^=a[i-1];
ll sum=0;
for(int i=0;i<=n;i++) //
{
for(int j=0;j<30;j++)
{
if(((a[i]>>j)&1))//1
{
sum+=zero[j]*(1ll<<j); //表示前缀和a[i]的第j位1于前面的所有前缀和异或之后,有几个1(即前面对应位子0的个数)
one[j]++;
}
else
{
sum+=one[j]*(1ll<<j);
zero[j]++;
}
}
}
cout<<sum<<endl;
return 0;
}
/********************
1 2
********************/
牛客练习赛16 E求值
题意:
给你一个序列,让你找里面的连续子序列的或的和x,问这个序列由多少个不同的x
真的是菜.....刚写完这个系列的博客,遇到这道题还不会,看了别人代码.....
同样这里只不过用了类似前缀和的状态标记数组,该数组里面存的是各个二进制数位上最后一次出现在该数位的数的下标。
之后同样的过程,在找以i为结尾的子序列的值的过程中,将O(n)降低为O(20)。!!!!!
这里巧妙的地方是将数位按照出现在该数位的最后一个数字的下标来分类,
这样可行的原因是当你计算完[i-1,i]的或的和,计算[i-2,i]的或和时,之前i-1的值肯定是要或上的,这样就实现了优化
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 1e5+10;
const int MAX = 2e6+100;
const int N =20;
int a[MAXN];
int sq[N+1];
int pos[N+1];
bool vis[MAX];
bool cmp(int a,int b) //将数位按照最后出现在该数位上的数字的下标排
{
return pos[a]>pos[b];
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
memset(pos,0,sizeof(pos));
for(int i=1;i<=n;i++)
{
for(int j=0;j<N;j++)
{
sq[j]=j;
}
sort(sq,sq+N,cmp);
int k=a[i];
vis[a[i]]=true;
for(int j=0;j<N;j++)
{
if(pos[sq[j]]==0) break;
k=k|(1<<sq[j]);
if(pos[sq[j]]!=pos[sq[j+1]])
{
vis[k]=true;
}
}
k=a[i];
for(int j=0;j<N&&k;j++)
{
if(k&1) pos[j]=i;
k=k>>1;
}
}
int ans=0;
for(int i=0;i<MAX;i++)
{
if(vis[i]) ans++;
}
printf("%d\n",ans);
return 0;
}