纯小白蓝桥杯备赛笔记--DAY7(竞赛必备基础算法)

位运算

二进制数的位进行操作。对二进制的每一位进行逻辑操作,一般情况下位运算的每一位相互独立,各自运算出结果。

  • 用途:优化算法,位掩码操作,位字段处理等领域。
  • 竞赛中的位运算:异或的性质,状态压缩,与位运算有关的特殊数据结构(树状数组,01tire,01线性基),构造题。
  • 常见的位运算:
    • 按位与运算:只有当两位都为1时,结果为才为1,否则为0.(逻辑乘法)。
      一条规则:当两个数字做与运算时,结果不会变大。

    • 按位或运算:当两位只要有一位为1时,结果就为1,否则为0。
      一条规则:当两个数字做与运算时,结果不会变小。

    • 异或运算:^ 两位结果不同为1,否则为0.
      该运算不会进位。
      性质:

    • 按位取反(~):通常用于无符号整数。

    • 按位左移(<<):移动时低位补0,注意移动时不要移动到无符号位上,或者干脆使用无符号整理,1会移动到符号位上。
      左移操作相当于对原数进行乘以2的幂次方的操作。例如:5左移三次==5*(2^3)

    • 按位右移(>>):高位补0,如果数据类型为有符号整型,注意移动时让符号位为0,或者干脆使用无符号整型。如果符号位上有1不会被移走,这是负数位移的规则。
      右移操作相当于对原数进行除以2的幂次方的操作。
      13>>2==13/(2^2)向下取整。

    • 例子:

#include<bits/stdc++.h>
using namespace std;
const int N=1050;
int main()
{
   
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
        cout<<bitset<32>((1 << 31) >> 2)<<'\n';
        //bitset<32> 创建一个32位的二进制数
        //{1<<31}:将数字1左移31位,得到一个32位的二进制数,其中第32位为1,其余位为0
        return 0;
 } 
  • 位运算的技巧:
    • 判断数字的奇偶性:
      x&1;
      结果为1说明是奇数,结果为0说明是偶数。
    • 获取二进制的某一位:
      x>>i&1
      结果必然为0或1,表示x的二进制表示中的第i位。
    • 修改二进制的某一位为1或0
      x|(1<<i) 将x的第i位或上1,则第i位变为1,其他位上或上0没有影响
      x&~(1<<i)
    • 快速判断一个数字是否是2的幂次方:
      x&(x-1)
      结果为0证明x为2的幂次方。(说明该x的二进制表示中只有一个1,x-1就有很多个连续的1并且和x的1没有交集,两者的运算一定为0,可以证明其他情况一定不为0)
      如何计算x-1:当我们从x减去1时,我们需要从最低位开始找到第一个1,并将该位改为0,然后将所有更低位的0改为1。如果最低位是0,我们就从最低位开始,将所有的0都改为1。
    • 获取二进制位中最低位的1:常用于数据结构树状数组中。
      lowbit(x)=x&-x;
      补:二进制数负数的二进制:
    1. 求原码
    2. 取反码
    3. 求补码(反码加1)。加1:从最右一位加一直到不产生进位。
  • 例题:
  1. 求二进制中1的个数:
unsigned int x;重中之重的一句话
//大神的写法:
#include<bits/stdc++.h>using namespace std;
int main(){
   
    unsigned int x;
    cin>>x;
    int ans=0;
    while(x){
   
        if(x%2==1)  ans++;
        x/=2;
    }
    cout<<ans;

    return 0;
}

//我的挣扎:(入栈和出栈)

#include<bits/stdc++.h>
using namespace std;
int main()
{
   
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
    stack<int>a;
    int count=0;
    unsigned int m;cin>>m;
    //求二进制的算法
    while(m > 0)
    {
   
       if(m%2==0)
       a.push(0);
       else
       a.push(1);
        m = m / 2;
    } 
    while(!a.empty())
    {
   
        if(a.top()==1)
        count++;
        a.pop();
    }
    cout<<count<<endl;
    return 0;
}

//老师的解法

#include<bits/stdc++.h>
using namespace std;
int main()
{
   
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
    int count=0;
    unsigned int m;cin>>m;
    while(m)
    {
   
      if(m&1) count++;//x的最低位为1
    m>>=1;//x不断向右移动1位 
  }
    cout<<count<<endl;
    return 0;
}
  1. 3691:区间或–难理解
    重要思想:拆位做贡献。
    将整个问题划分为31个部分,分别表示数组元素二进制表示的第0位,第一位,,第30位。
    计算每一部分的前缀和,对于每次询问,判断这一位是否是1(即前缀和>0),如果是就算上这一位对答案的(求和的)贡献。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
int a[N];
int prefix[35][N];//前缀和 
int main()
{
   
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
        int n,q;cin>>n>>q;
        for(int i=1;i<=n;i++)
        {
   
                cin>>a[i];
        }
        for(int w=0;w<=30;w++)
        {
   
                for(int i=1;i<=n;i++)
                {
   
                        prefix[w][i]=prefix[w][i-1]+(a[i]>>w&1);
//将当前元素的第w位的值(0或1)加到前缀和数组prefix[w]中对应的位置上。这里使用了右移操作符>>和按位与操作符&来判断当前元素的第w位是否为1。
                }
        }
        while(q--)
        {
   
                int l,r;cin>>l>>r;//输入区间l到r
                int ans=0;
                for(int w=0;w<=30;w++)
                {
   
                        ans+=(1<<w)*(prefix[w][r]-prefix[w][l-1]?1:0);
//对于每个二进制位的位置w,根据前缀和数组prefix计算出区间l到r内该位为1的个数,并将其乘以2的w次方后累加到ans中
                 } 
                cout<<ans<<'\n';
        }
        
        
    return 0;
}
  1. 3400异或森林:
    题目:子数组所有元素异或的运算结果的因数个数为偶数。
    因数个数为偶数–不是完全平方数–根号x为整数。
    什么是完全平方数?
  2. 非负性:完全平方数是非负数,包括零和所有正整数的平方。
  3. 乘法定义:如果一个数可以表示为某个整数n的平方,即 ( n^2 ),那么这个数就是一个完全平方数。例如,( 1^2 = 1 ), ( 2^2 = 4 ), ( 3^2 = 9 ) 等。
  4. 平方根为整数:完全平方数的平方根是一个整数,这是判断一个数是否为完全平方数的重要依据。
  5. 数字特征:完全平方数的个位数字只能是0, 1, 4, 5, 6, 或 9。此外,如果完全平方数的个位是奇数,则其十位上的数字必为偶数;如果个位是6,则十位上的数字必为奇数。
    试除法:x 根号下x
    正难则反:计算有多少个子数组的异或和为完全平方数,用总区间减去即可。

前缀和

它不是已实现的数组,是需要额外定义的。

  • 前缀和的原理和特点:
    prefix表示前缀和,前缀和由一个用户输入的数组生成。
    可理解 为数组前i个元素之和。
    prefix的重要特性:快速获得prefix
    prefix[i]=prefix[i-1]+a[i];
    求静态数组一段区间的和
    sum(l,r)=prefix[r]-prefix[l-1];
    注:如果要实现“先区间修改,再区间查询”可以使用差分数组,如果需要“一边修改,一边查询”需要使用树状数组或线段树等数据结构。
  • 实现前缀和:
int a[N],prefix[N];
for(int i=1;i<=n;i++)//注意数组下标从1开始
{
   
prefix[i]=prefix[i-1]+a[i];
}
  • 例题:
  1. 区间次方和:我的想法是先找出区间求k次方的和。老师的想法是存放5个数组,第一个数组放1次方,第二个数组放而次方,再求区间和。这样能更方便的利用前缀和的特性
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+9;
const ll p=1e9+7;
ll a[6][N];
ll prefix[6][N];//前缀和 
int main()
{
   
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
        int n,m;cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
   
                cin>>a[1][i];
        }
        //求k次方
        for(int i=2;i<=5;i++)//1次方不用求,从2开始 
        {
   
                for(int j=1;j<=n;j++)
                {
   
                        a[i][j]=a[i-1][j]*a[1][j] % p;//条件意义是a[j]的i次方等于。。
//                        a[i][j]表示第j个元素的i次方的结果,a[i-1][j]表示第j个元素的(i-1)次方的结果,a[1][j]表示第j个元素的1次方的结果。 
                }
         }
         //定义前缀和
         for(int i=1;i<=5;i++) 
        {
   
        
                for(int j=1;j<=n;j++)
                {
   
                        prefix[i][j]=(prefix[i][j-1]+a[i][j])%p;
                }
         }
        while(m--)
        {
   
                int l,r,k;
                cin>>l>>r>>k;
                cout<<(prefix[k][r]-prefix[k][l-1]+p)%p<<'\n';//+p是为了防止出现负数 
        }
        
    return 0;
}
  1. 3419:最长平衡串的长度,出现了读题错误。我的想法是找到出现次数最少的字符串的个数*2.这种做法只能求出最小字符串,不能求出最长字符串,要想求出最长字符串需要考虑“最少要满足什么条件”。
    可以这样做:
    把L看做1,Q看做-1,只有当某个区间的和为0时,字符串是平衡的。
    可以预处理出前缀和,枚举所有区间,得到所有平衡区间的长度最后取大输出。
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
char s[N];
int prefix[N];
int main()
{
   
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
        cin>>s+1;
        int n=strlen(s+1);
        for(int i=1;i<=n;i++)
        {
   
                prefix[i]=prefix[i-1]+(s[i]=='L'?1:-1);
        }
        int ans=0;
        for(int i=1;i<=n;i++)
        {
   
                for(int j=i;j<=n;j++)
                {
   
                        if(prefix[j]-prefix[i-1]==0)//如果区间和为0 
                        ans=max(ans,j-i+1);//取最大的区间 
                }
        }
        cout<<ans<<'\n'; 
    return 0;
}

差分

  1. 定义
    diff[i]=a[i]-a[i-1];
    对差分数组求前缀和可以还原为原数组。
  2. 特点
    可以实现快速的区间修改。
    将区间[L,R]都加上x
    diff[l]+=x;
    diff[r+1]-=x;
  • 需要最终用前缀和还原为原数组
  1. 差分的实现
    直接用循环
for(int i=0;i<=n;i++)
{
   
        diff[i]=a[i]-a[i-1];
}
  1. 例题
  • 区间更新
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+3;
int a[N],diff[N];
void solve(int n,int m)
{
   
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int i=1;i<=n;i++)diff[i]=a[i]-a[i-1];
        while(m--)
        {
   
                int l,r,k;
                cin>>l>>r>>k;
                diff[l]+=k;diff[r+1]-=k;
        }
        for(int i=1;i<=n;i++)a[i]=diff[i]+a[i-1];//还原数组
        for(int i=1;i<=n;i++)cout<<a[i]<<(i==n?"\n":" "); //控制换行

}
int main()
{
   
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n,m;
        while(cin>>n>>m)
        solve(n,m);        
        return 0;
}
  • 小明的彩灯1276
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+5;
ll a[N],diff[N];
int main()
{
   
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n,q;cin>>n>>q;
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int i=1;i<=n;i++)diff[i]=a[i]-a[i-1];
        while(q--)
        {
   
        int l,r,k;
        cin>>l>>r>>k;
        diff[l]+=k;
        diff[r+1]-=k;        
        }
        for(int i=1;i<=n;i++)a[i]=diff[i]+a[i-1];
        for(int i=1;i<=n;i++)cout<<max(0ll,a[i])<<(i==n?"\n":" ");
        return 0;
 } 

说明:这段代码中的max函数是C++标准库中的一个函数,用于比较两个值并返回较大的那个。在这个代码中,它被用来确保数组a中的元素不会小于0。如果a[i]小于0,那么max(0ll, a[i])将返回0,否则返回a[i]本身。
递归

  1. 递归的介绍
    函数直接或间接的调用自身的过程。
  2. 递归的实现
  • 主要:出口+分治
    [图片]在这里插入图片描述

  • 递归与循环的比较

  1. 递归:直观。递归调用不断减少,常用于树和图的遍历,有栈溢出的风险。
    补充:栈一般只有8MB,所以递归层数不宜过深,不超过1e6层。
  2. 循环:常数小,效率高,有特定的迭代次数,适合大部分的动态规划。
    补:dfs和栈可
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值