ACM Weekly 1

涉及的知识点

第一周练习主要涉及快速幂算法、素数筛、位运算
拓展知识点:bitset、尺取法

快速幂

理论基础

快速幂算法是建立在取模原则上的算法,其原理为下面几个公式,在此给出证明:

1. ( m × n ) % p = ( ( m % p ) × ( n % p ) ) % p (m\times n)\%p=((m\%p)\times(n\%p))\%p (m×n)%p=((m%p)×(n%p))%p

证明:

m % p = ( i × p + c ) = c m\%p=(i \times p+c)=c m%p=(i×p+c)=c

n % p = ( j × p + d ) = d n\%p=(j \times p+d)=d n%p=(j×p+d)=d

( ( m % p ) × ( n % p ) ) % p = ( c × d ) % p ((m\%p)\times(n\%p))\%p=(c \times d)\%p ((m%p)×(n%p))%p=(c×d)%p

( m × n ) % p = ( ( i × p + c ) × ( j × p + d ) ) % p = ( i × j × p + ( j × d + i × c ) × p + c × d ) % p = ( c × d ) % p (m\times n)\%p=((i\times p+c)\times(j\times p+d))\%p=(i\times j\times p+(j\times d+i\times c)\times p+c\times d)\%p=(c\times d)\%p (m×n)%p=((i×p+c)×(j×p+d))%p=(i×j×p+(j×d+i×c)×p+c×d)%p=(c×d)%p

( m − / + n ) % p = ( ( m % p ) − / + ( n % p ) ) % p (m-/+ n)\%p=((m\%p)-/+(n\%p))\%p (m/+n)%p=((m%p)/+(n%p))%p

2. ( ( m % p ) + / − ( n % p ) ) % p = ( c + / − d ) % p ((m\%p)+/-(n\%p))\%p=(c+/-d)\%p ((m%p)+/(n%p))%p=(c+/d)%p

证明:

( m + / − n ) % p = ( i × p + c + / − j × p + d ) % p = ( ( i + j ) × p + c + / − d ) % p = ( c + / − d ) % p (m+/-n)\%p=(i \times p+c+/-j \times p+d)\%p=((i+j)\times p+c+/-d)\%p=(c+/-d)\%p (m+/n)%p=(i×p+c+/j×p+d)%p=((i+j)×p+c+/d)%p=(c+/d)%p

探寻

快速幂一般所解决的是求算式 a b % p a^b\%p ab%p的值的问题,通过上述运算法则的运用,我们可以尝试用代码的方式来实现快速幂问题的解答

最初解法

利用积之余为余之积的性质,可以得到最初的解法,通过循环来进行逐个消去。

代码

int result=1;
for(int i=1;i<=power;i++)
{
	result=base;
	result=result%p;//p为取余的数,若不进行取余操作,相当于乘了power次*
	return result%p;
}
//根据公式逐步分解
核心解法

因计算an需n次循环,可将其变为(a2)n/2,则运算的结果只需n/2次,以此类推,将an分解,最后得出求出的幂结果实际上就是在变化过程中所有当指数为奇数时底数的乘积。

代码

int result=1;
while(power)
{
	if(power^1)//如果是偶数
	{
		power>>=1;//除2
		base=base*base%p;//底数平方
	}
	else//如果奇数
	{
		power--;//构造偶数,多的一个1单独处理
		result=result*base%p;
		power>>=1;//变成偶数了,同if
		base=base*base%1000;
	}
	return result;
}
//根据公式逐步分解
利用编程特性来优化

编程语言中可以采用位运算的性质,对快速幂算法的速度进行进一步提升。

代码

result=1;
while(power)
{
	if(power&1)
		result=result*base%p;
	power>>=1;
	base=base*base%p;
}
return result;
//根据公式(3)逐步分解

例题1(POJ3641)

在这里插入图片描述
题目大意:给出两个数p,a,如果a的p次方对p取余等于a并且p不是素数,输出yes,否则输出no

思路:首先判断p是否为质数,之后利用快速幂计算出ap对p取余的结果,直接判断结果与a是否相等

代码

#include <iostream>
//证明a^p%p==a且p不为素数
//a^p=(a^2)^(p/2)
using namespace std;
typedef long long ll;
ll p,a;
bool flag;
int main()
{
    while(1)
    {
        cin >>p>>a;
        if(!(p||a))//如果p和a都为0,中断
            break;
        for(ll i=2; i*i<=p; i++)//判断是否为质数
            if(flag=!(p%i))//用flag标记是否为素数
            {
                ll remind=1,power=p,judge=a;//结果,幂数,判断条件
                while(power)//快速幂部分
                {
                    if(power&1)
                        remind=remind*a%p;
                    power>>=1;
                    a=a*a%p;
                }
                if(remind==judge)//判断是否为原值
                    cout <<"yes"<<endl;
                else
                    cout <<"no"<<endl;
                break;
            }
        if(!flag)
            cout <<"no"<<endl;
    }
    return 0;
}

例题2(POJ1995)

在这里插入图片描述
题目大意:给出N个底数指数对,计算它们的和对M取余的结果

思路:多个快速幂累加

代码

#include <iostream>
using namespace std;
typedef long long ll;
ll Z,M,H;
ll divide(ll base,ll power)//利用快速幂分解
{
    ll result=1;
    while(power)
    {
        if(power&1)
            result=result*base%M;
        power>>=1;
        base=base*base%M;
    }
    return result;
}
int main()
{
    //freopen("test.txt","r",stdin);
    cin >>Z;
    while(Z--)
    {
        cin >>M>>H;
        ll sum=0;
        while(H--)//对输入的H个底数指数对进行累和
        {
           ll a,b;
           cin >>a>>b;
           sum=(sum+divide(a,b))%M;
        }
        cout <<sum<<endl;
    }
    return 0;
}

素数筛

素数筛的基本应用是给定一个数据范围,求出该范围内哪些数是质数以及其数量,有多重方法可以实现目的。

探寻

素数筛的方法多样,但时间复杂度各个不一,下面对其进行探究

暴力解法

利用素数的定义,逐个判断,从2到n(针对一个数)

代码

for(int i=2;i<=n;i++)
	if(n%i==0)
		return false;
return true;
平方逼近法

通过平方逼近n,减少不必要的筛选, n n n\sqrt n nn (针对一个数)

代码

for(int i=2;i<=n/i;i++)
	if(n%i==0)
		return false;
return true;
埃氏筛

当我们找到一个合数时,该合数的倍数肯定都不是质数,当找到一个质数时,其倍数也不为质数,以此基准来排除

代码

bool Primes[1e6]={1,1};
for(int i=2;i<=n;i++)
	if(!Primes[i])//如果没被筛掉,那肯定是质数
		for(int j=2*i;j<=r;j+=i)//筛掉它的倍数
			Primes[j]=true;
欧拉筛

欧拉筛的时间复杂度为 O ( n ) O(n) O(n),是非常快速的算法,和埃氏筛有类似的地方,相当于埃氏筛的深层简化

原理:最小质因数×最大因数(非自身)=该合数

代码

bool IsPrime[121212];//真值为素数
int Prime[121212],ans;
void Choose(int n)//筛选到n
{
	memset(IsPrime,1,sizeof(IsPrime));//初始化
	//假设每个数为素数
	IsPrime[1]=IsPrime[0]=1;
	for(int i=2;i<=n;i++)
	{
		if(IsPrime[i])//如果这个数没筛掉,那么将其加入素数序列
			Prime[++ans]=i;
		for(int j=1;j<=ans&&i*Prime[j]<=n;j++)
		{
			IsPrime[i*Prime[j]]=0;
			if(!i%Prime[j])break;
		}
	}
}

对于给定范围内的一个数i,如果它并未被筛去,那么我们便可以它为基础来筛去更多的数,在代码中,我们每次以所遍历到的数i作为基础,首先,如果它是素数,那么直接加入序列中,之后,我们期望它符合最小质因数×最大因数(非自身)=该合数的原理,以每个已筛出的素数为最小质因数作为前提,那么,在循环中Prime[j](最小质因数)×i(最大因数)被筛去,为了确保这一条件,添加了if(!i%Prime[j])break; 这一行,为什么要停下呢?因为在其之后是不属于先前所提到的原理的,注意,我们在筛选的过程中是必须遵循最小质因数×最大因数(非自身)=该合数的原则的,并不是说之后的数字不能通过这一层被筛,而是为了避免重复,限定了条件避免重复筛去。

证明这一行代码可以符合该原理
证明如下:

1.i×Prime[J] (j<J)
i×Prime[J]=Prime[j]×i/Prime[j]×Prime[J],Prime[j]才是最小质因数,Prime[J]不是
2.i×Prime[t] (t<j)
Primep[t]是最小质因数,Prime[j]不是
3.i的最小质因数为Prime[j]
倘若有更小的质因数,则应当在j之前中断

正确性证明

证明该算法的正确性,即证明所有的合数在筛选的过程中都被筛去
对于一个合数X,X=Y×Prime,而Y的最小质因数应大于等于Prime,那么当外层到i=Y时,内层遍历所有小于等于Y的质数,因为Y的最小质因数不小于Prime,所以i在内层到Prime之前不会break,即因为内层会遍历到B的最小质因数,而B的最小质因数大于Prime,所以必定会遍历到Prime,而此时i=B,那么此时便有i×Prime=B×Prime被筛去

O(n)时间复杂度证明

欧拉筛的原理为"X×质数"来筛去合数,又由先前的证明,所有合数必然可以被筛去,那么探求时间复杂度的关键便是同一个合数是否会被另一种"X×质数"组合筛去从而产生重复
假设目标合数为T,存在一质数p,使得p×A=Prime×B,因为Prime为T的最小质因数,所以LHS=p×A/Prime×Prime,即A可以整除Prime,且A<B,当外层到达i=A时,如果再筛选一次C,但是因为内层遍历到Prime时(Prime<p,Prime先遍历到)A%Prime==0而中断,无法被筛去,而C除了Prime其他的质因数都不能筛去它

更加详细的说明请参考最后文献

例题1(POJ2739)

在这里插入图片描述
题目大意:给出一系列的数,判断是否有连续的质数序列的和能满足等于本身的条件,如果有,则求出有多少个

思路:先筛选出数据范围内的数据,随后根据查询进行在线累和进行判断

代码

#include <iostream>
#include <cmath>
using namespace std;
int prime[100000]= {2,3},acc=1;
int main()
{
    int n=0;
    //freopen("test.txt","r",stdin);
    while(cin >>n&&n)
    {
        if(n==2||n==3)//情况特判
        {
            cout <<"1"<<endl;
            continue;
        }
        if(n>prime[acc])//acc记录已经筛出的最大质数的下标,
        //如果查询大于当前最大质数,则需要增加收录素数
        {
            int i,j;
            for(i=prime[acc]+2; i<=n; i+=2)
            {
                for(j=2; j*j<=i; j++)
                    if(i%j==0)break;
                if(j*j>i)
                    prime[++acc]=i;
            }
        }
        int low=0,high=0,sum=0,amount=0;
        do//利用两个标记,上限与下限,累和
        {
            if(sum==n)amount++;
            if(sum>=n)sum-=prime[low++];
            else if(prime[high]<=n)
                sum+=prime[high++];
            else
                break;
        }
        while(1);
        cout <<amount<<endl;
    }
    return 0;
}

例题2(POJ3126)

在这里插入图片描述

题目大意:给出一对数A,B(四位数),判断它们是否都为质数且A能否通过每次只变化一位,变化多次来达到B

思路:首先筛选出10000以内素数,之后用BFS的思路去处理

代码

#include <iostream>
#include <cstdlib>
#include <queue>
#include <cstring>
using namespace std;
int N,Prime[12121],ans,start,last;
bool IsPrime[12121]= {true,true},visited[12121];
typedef pair<int,int>PR;
int BFS()//以相邻素数为状态进行寻找
{
    queue<PR>Q;
    int result=0;
    Q.push(make_pair(start,0));
    while(!Q.empty())
    {
        PR tmp=Q.front();
        Q.pop();
        int w=tmp.first;
        visited[w]=true;
        if(visited[last])
        {
            result=tmp.second;
            break;
        }
        for(int i=1; i<=9; i++)
        {
            int t=i*1000+w-w/1000*1000;
            if(t!=w&&!IsPrime[t]&&!visited[t])
                Q.push(make_pair(t,tmp.second+1));
        }
        for(int i=0; i<=9; i++)
        {
            int t=i*100+w-(w/100-w/1000*10)*100;
            if(t!=w&&!IsPrime[t]&&!visited[t])
                Q.push(make_pair(t,tmp.second+1));
        }
        for(int i=0; i<=9; i++)
        {
            int t=i*10+w-(w/10-w/100*10)*10;
            if(t!=w&&!IsPrime[t]&&!visited[t])
                Q.push(make_pair(t,tmp.second+1));
        }
        for(int i=0; i<=9; i++)
        {
            int t=i+w-(w-w/10*10);
            if(t!=w&&!IsPrime[t]&&!visited[t])
                Q.push(make_pair(t,tmp.second+1));
        }

    }
    return result;
}
int main()
{
    for(int i=2; i<=9999; i++) //筛出一万以内的素数
    {
        if(!IsPrime[i])
            Prime[++ans]=i;
        for(int j=1; j<=ans&&i*Prime[j]<=9999; j++)
        {
            IsPrime[i*Prime[j]]=true;
            if(i%Prime[j]==0)
                break;
        }
    }
    scanf("%d",&N);
    while(N--)
    {
        scanf("%d%d",&start,&last);
        if(start==last)
        {
            printf("0\n");
            continue;
        }
        int t=BFS();
        if(IsPrime[start]||IsPrime[last]||t==0)
            printf("Impossible\n");
        else
            printf("%d\n",t);
        memset(visited,0,sizeof(visited));
    }
    return 0;
}

例题3(POJ2689)

在这里插入图片描述

题目大意:给出一个上界与下界,判断在这个上下界之中存在的相邻素数的最大素数对与最小素数对

思路:首先,题目所涉及到的数据过于庞大,直接筛选出给定范围内的所有素数并不可行,但是对于给定的L,R区间,区间长度相对于整个范围较小,所以我们可以只筛选出一部分来使用,筛选的素数范围为1~ R \sqrt R R 。我们的目标是L~R内的所有素数,换言之,我们的目标是筛去L ~R内的所有合数,那么,筛去这些合数,我们只需要1 ~ R \sqrt R R 内的素数即可,也就是说,我们先筛出1 ~ R \sqrt R R 的素数,再用这些素数来筛出L~R内的素数,下面给出证明:

对于一个合数X,它可以分为两个质因子相乘,一个大于 X \sqrt X X ,设为B,一个设为最小质因子小于 X \sqrt X X ,设为S,对于L ~R范围,则所有S< R \sqrt R R ,在欧拉筛中,在到达B之前,S必定已经筛去了X(由上述的欧拉筛证明可知)

那么,在筛选出所有的素数之后,对所求的相邻素数进行两两比对并记录,最后可得出结果

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
typedef long long ll;
int Prime[121212],cnt,v[1212121],ans[1212121];
/*Prime存储素数,cnt为素数计数器,v为标记数组,记录每个数的最小质因数
对于素数来说,最小质因数为本身,ans为*/
void Choose(int N)
{
    for(int i=2; i<=N; i++)
    {
        if(!v[i])//如果该数未被筛去,即为素数
        {
            v[i]=i;//标记,此时的i为自己的最小质因数
            Prime[++cnt]=i;//存储素数
        }
        for(int j=1; j<=cnt; ++j)
        {
            if(Prime[j]>v[i]||Prime[j]>N/i)break;
            //如果此时的素数大于i已知的最小质因数(欧拉筛原理)或者素数与i的乘积超出了N,中断
            v[i*Prime[j]]=Prime[j];//标记&记录最小质因数
        }
    }
}
int main()
{
    Choose(46345);//预处理
    ll L,R;
    while(cin >>L>>R)
    {
        memset(v,1,sizeof(v));//初始化标记数组,每个数为素数
        if(L==1)v[0]=0;//防止v[i-l]被误算
        for(ll i=1; i<=cnt; ++i)
            for(ll j=(L+Prime[i]-1)/Prime[i]; j<=R/Prime[i]; ++j)
            /*L+Prime[i]-1为L~R范围内首个大于等于Prime[j]的数,j存储的是该数为Prime[i]的几倍,
      之后的循环分别是以Prime[i]的属于L~R的j、j+1、j+2...的倍数来筛去,即Prime[i]的j倍存在于L~R中*/
                if(j>1)
                    v[Prime[i]*j-L]=0;
                    /*在这里,v变成筛选L~R内的标记数组,Prime[i]*j在L~R内,
        ,通过减L来获得相对于L的偏移量,v记录的是相对于L偏移量为Prime[i]*j的数字的素数状态*/
        //j*Prime[i]必须小于R
        int m=0;
        for(ll i=L; i<=R; ++i)
            if(v[i-L])ans[++m]=i;//如果没有被筛去,记录,将素数单独抽出来
        int minn=0x7fffffff,maxx=0,x1,y1,x2,y2;
        for(int i=1; i<m; ++i)//遍历L~R内的素数
        {
            int num = ans[i+1] - ans[i];
            if (num < minn)
            {
                minn = num;
                x1 = ans[i];
                y1 = ans[i+1];
            }
            if (num > maxx)
            {
                maxx = num;
                x2 = ans[i];
                y2 = ans[i+1];
            }
        }
        if (!maxx) puts("There are no adjacent primes.");//如果不存在
        else printf("%d,%d are closest, %d,%d are most distant.\n", x1, y1, x2, y2);
    }
    return 0;
}

位运算

例题1(POJ1753)

在这里插入图片描述

题目大意:给出一个4×4的期盼,每个位置非正即反,现在进行操作,每次翻转以一枚棋子为中心的上下左右中五颗棋子,求出至少翻转多少次才能使所有棋子状态相同

思路:将棋盘的状态看做一个16位的二进制数,每4位为一行,使用BFS,每次对一层进行拓展来方便记录层数

代码

#include <iostream>
#include <bitset>
#include <queue>
#include <cstdlib>
#include <cstdio>
using namespace std;
bool visited[212121];
bitset<16>Maze;
int cnt,ans=-1,Next[]= {0,1,-1,-4,4}; //Next为下一个拓展的坐标
int main()
{
    for(int i=0; i<16; i++)
    {
        char ch;
        cin >>ch;
        if(ch=='b')
            Maze[i]=1;
        else
            Maze[i]=0;
    }
    queue<bitset<16>>Q;
    Q.push(Maze);
    while(!Q.empty())//每次拓展一层
    {
        int t=Q.size();//记录要拓展层数的元素个数
        while(t--)
        {
            bitset<16> now=Q.front();
            Q.pop();
            int a=0,b=0;
            for(int i=0; i<16; i++)//统计状态
                if(now[i])
                    ++a;
                else
                    ++b;
            if(a==16||b==16)//全反或全正停止
            {
                ans=cnt;
                break;
            }
            if(ans!=-1)
                break;
            int x=now.to_ulong();//将状态转换成整数进行标记
            if(visited[x])
                continue;
            visited[x]=true;
            for(int i=0; i<16; i++)
            {
                bitset<16>T(now);
                int row=i/4,col=i%4;//记录当前中心点的横纵坐标
                for(int j=0; j<5; j++)
                {
                    if(j<3&&(col+Next[j]<0||col+Next[j]>=4))//如果列数不规范,跳过
                        continue;
                    if(j>=3&&(i+Next[j]<0||i+Next[j]>=16))//如果操作后范围不规范,跳过
                        continue;
                    T.flip(col+4*row+Next[j]);//翻转这一位
                }
                Q.push(T);//记录翻转结果
            }
        }
        if(ans!=-1)
            break;
        ++cnt;//记录层数
    }
    if(ans!=-1)
        cout <<ans<<endl;
    else
        cout <<"Impossible\n";
    return 0;
}

例题2(POJ2453)

在这里插入图片描述

题目大意:给出一个数I,找出一最小的大于I且对应二进制位与I相等的数

思路:该题有两种思路,第一种为暴力解法,直接从大于I的序列中遍历寻找,第二种使用贪心,为了找到第一个比I大,而二进制数1的个数又等于I的数,首先需要找到I中第一个二进制位为1的地方(从小到大),如果在0位开始变化的话,势必会使一个高位的1变成一个低位的1,这样得出来的数不会大于I,找到第一个个1位后,就需要找到之后的第一个0位,以此来将首个1位移动到第一个比它大的0位上,将修改后的对应二进制位数转换为十进制输出便是答案

代码(暴力)

#include <iostream>
#include <bitset>
using namespace std;
int I;
int main()
{
    while(cin >>I&&I)
    {
        bitset<32> t=I;
        int ans=t.count();
        I++;
        while(1)
        {
            bitset<32>tmp=I;
            if(tmp.count()==ans)
            {
                cout <<I<<endl;
                break;
            }
            I++;
        }
    }
    return 0;
}

代码(贪心)

#include <iostream>
#include <bitset>
using namespace std;
int I;
int main()
{
    while(cin >>I&&I)
    {
        bitset<32> t=I;
        int num=0;//记录1的个数
        for(int i=0; i<32; ++i)//bitset为大端存储
        {
            if(num&&!t[i])//找到了1,又找到了0
            {
                t[i]=1;//0位置1
                t[i-1]=0;//相邻位置0
                for(int j=0;j<i;++j)//清空所有位
                    t[j]=0;
                for(int j=0;j<num-1;++j)//已经设置了一个1,还有num-1个1
                //为确保最小,从低位开始放置
                    t[j]=1;
                break;
            }
            else if(t[i])
                ++num;
        }
        cout <<t.to_ulong()<<endl;
    }
    return 0;
}

拓展

本周拓展包括了两道竞赛题目以及bitset的相关用法、竞赛书上提及的尺取法,属于课后延伸内容

题目

HDU 6702

题目大意:找到最小 C,使(A xor C)& (B xor C)最小(xor为异或)

思路:需要求的是 ( A ⊕ C ) & ( B ⊕ C ) (A\oplus C)\& (B\oplus C) (AC)&(BC),以某一位来看,设某一位结果为X,进行化简
X = ( A ⊕ C ) & ( B ⊕ C ) = ( A B C ˉ + A ˉ B ˉ C ) X=(A\oplus C)\& (B\oplus C)=(AB\bar{C}+\bar{A}\bar{B}C) X=(AC)&(BC)=(ABCˉ+AˉBˉC)
要求取最小值,所以X尽量为0,根据真值表可得AB相等时, X = C = A & B X=C=A\&B X=C=A&B,不等时为0,所以取C为A&B即可,考虑特殊情况,注意使用long long

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
long long T,A,B;
int main()
{
    scanf("%lld",&T);
    while(T--)
    {
        scanf("%d%d",&A,&B);
        if((A&B)==0)
            printf("1\n");
        else
            printf("%lld\n",A&B);
    }
    return 0;
}

HDU 6186

题目大意: 从 n 个数中去除一个数后,计算它们的与、或、异或值

思路:第一种方法是利用前缀和的思想,预处理前缀后缀与或异或的值的值,对于每个查询直接输出,第二种方法是利用位运算的性质,我们先得到所有的数的与或异或值。接下来考虑每次查询,对于异或而言,只需要再异或一次 p 位置上的数,对于与而言,只有当 a p a_p ap的这一位为 0 并且所有数在这一位为 0 的个数为 1 时,它才能对这一位产生决定性影响,同理,对于或而言,只有当 a p a_p ap这一位为 1 并且所有数在这一位为 1 的个数为 1 时,它才能对这一位产生决定性影响。所以我们预处理出每一位的 1 和 0 的个数,对于每个 p 位置的数,判断它能否产生决定性影响即可。

证明异或的只需异或一次p即可:
设所有数异或的总结果为X,p之前的异或结果为Pre,p之后的异或结果为Aft,X=Pre^p^Aft
X^p=Pre^p^Aft^p=Pre^p^Aft^p=Pre^p^p^Aft=Pre^0^Aft=Pre^Aft

代码(前缀和思想)

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
int data[121212],PreAnd[121212],PreOr[121212],PreXor[121212],AftAnd[121212],AftOr[121212],AftXor[121212];
using namespace std;
int n,q;
int main()
{
    while(scanf("%d%d",&n,&q)!=EOF)
    {
        for(int i=1; i<=n; i++)
            scanf("%d",&data[i]);
        PreAnd[1]=PreOr[1]=PreXor[1]=data[1];
        for(int i=2; i<=n; i++)
        {
            PreAnd[i]=PreAnd[i-1]&data[i];//前缀与
            PreOr[i]=PreOr[i-1]|data[i];//前缀或
            PreXor[i]=PreXor[i-1]^data[i];//前缀异或
        }
        AftAnd[n]=AftOr[n]=AftXor[n]=data[n];
        for(int i=n-1; i>0; i--)
        {
            AftAnd[i]=AftAnd[i+1]&data[i];//后缀与
            AftOr[i]=AftOr[i+1]|data[i];//后缀或
            AftXor[i]=AftXor[i+1]^data[i];//后缀异或
        }
        while(q--)
        {
            int x;
            scanf("%d",&x);
            if(x==1)//特判
                printf("%d %d %d\n",AftAnd[2],AftOr[2],AftXor[2]);
            else if(x==n)//特判
                printf("%d %d %d\n",PreAnd[n-1],PreOr[n-1],PreXor[n-1]);
            else
                printf("%d %d %d\n",(PreAnd[x-1]&AftAnd[x+1]),(PreOr[x-1]|AftOr[x+1]),(PreXor[x-1]^AftXor[x+1]));
        }
    }
    return 0;
}

代码(位运算)

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <bitset>
#include <cstring>
int data[121212],And,Or,Xor,sum[32];
using namespace std;
int n,q;
int main()
{
    while(scanf("%d%d",&n,&q)!=EOF)
    {
        for(int i=1; i<=n; i++)
            scanf("%d",&data[i]);
        And=Or=Xor=data[1];
        for(int i=2; i<=n; i++)//记录计算结果
        {
            And=And&data[i];
            Or=Or|data[i];
            Xor=Xor^data[i];
        }
        for(int i=1; i<=n; i++)
        {
            bitset<32>t(data[i]);
            for(int j=0; j<32; j++)
                sum[j]+=t[j];//记录1的个数
        }
        while(q--)
        {
            int x;
            scanf("%d",&x);
            bitset<32>bx=data[x],tand=And,tor=Or;
            for(int i=0;i<32;++i)
                if(bx[i]&&sum[i]==1)//如果去除数的这一位为1,这一位所有数的和为1
                //代表去掉这个数后,这一位所有数和为0,或值为0
                    tor[i]=0;
                else if(!bx[i]&&sum[i]==n-1)//如果去除数的这一位为0,这一位所有数的和为n-1
                //代表去掉这个数后,这一位所有数和为n-1,与值为1
                    tand[i]=1;
            cout <<tand.to_ulong()<<" "<<tor.to_ulong()<<" "<<(Xor^data[x])<<endl;
        }
        memset(data,0,sizeof(data));
        memset(sum,0,sizeof(sum));
    }
    return 0;
}

bitset

这一部分具体参考后面给出的文献,只强调一点bitset0位为低位

尺取法

该知识点以POJ 3061为例

题目大意:给出长度为n的数列整数 a 0 , a 1 , . . . a n − 1 a_0,a_1,...a_{n-1} a0,a1,...an1以及整数S,求出总和不小于S的连续子序列的长度的最小值,如果解不存在,输出0

思路:第一种方法是前缀和,先提前计算好和,可以确定所需序列的起点,之后采用二分查找的方法确定序列和不小于S的结尾的最小值,时间复杂度为O(nlogn),第二种方法便是尺取法,对于一个区间来说,如果它已经满足序列和大于等于S,其右端点就没有必要再向右延伸,此时只需要左端点向右拓展,进行试探,观察是否能缩短区间,如果右端点到达总区间末尾序列和仍然小于S,则中断,时间复杂度为O(n)

代码(前缀和)

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,S,data[121212],T,sum[121212];
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&S);
        for(int i=0; i<n; i++)
            scanf("%d",&data[i]);
        for(int i=0;i<n;i++)
            sum[i+1]=sum[i]+data[i];//记录前缀和,sum记录的是前i个数的和
        if(sum[n]<S)
        {
            printf("0\n");
            continue;
        }
        int res=n;
        for(int i=0;sum[i]+S<=sum[n];i++)//确保sum[n]-sum[i]>=S,找到起始点
        {
            int t=lower_bound(sum+i,sum+n,sum[i]+S)-sum;//lower_bound找出首个不小于sum[i]+S的元素的位置
            res=min(t-i,res);//头尾相减得数量
        }
        printf("%d\n",res);
        memset(data,0,sizeof(data));
        memset(sum,0,sizeof(sum));
    }
    return 0;
}

代码(尺取法)

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,S,data[121212],T;
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&S);
        for(int i=0; i<n; i++)//录入
            scanf("%d",&data[i]);
        int low=0,high=0,res=n+1,sum=0;//low为左边界,high为右边界
        while(1)
        {
            while(high<n&&sum<S)//找到第一个大于S的值(在没有到n的时候)
                sum+=data[high++];//边界拓展与累和
            if(sum<S)break;//如果累和小于S(代表即使到了n累和也小于S),则中断
            res=min(res,high-low);//记录最小值
            sum-=data[low++];//左边界右移,sum减去相应的值
        }
        if(res>n)//如果找不到这个值
            res=0;
        printf("%d\n",res);
    }
    return 0;
}

参考文献

  1. P3383 【模板】线性筛素数 题解
  2. C++ bitset 常用函数及运算符
  3. 素数的筛选的简化。合数存在素因子小于它的开方的证明。
  4. 《挑战程序设计竞赛》
  5. bitset(位图)原理与用法
  6. 关于bitset中低阶位与高阶位的理解
  7. 尺取法 — 详解 + 例题模板(全)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值