纯小白蓝桥杯备赛笔记--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和栈可以将递归转化为循环。
  3. 例题
  • 斐波那契数列
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const ll p=1e9+7;
ll fib(int n)
{
        if(n<=2)
        return 1;
        return (fib(n-1)+fib(n-2))%p; 
}
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        cout<<fib(n)<<"\n";
        return 0;
 } 
  • 带备忘录的斐波那契数列
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+9;
const ll p=1e9+7;
//带备忘录的递归
ll dp[N]; 
ll fib(int n)
{
        if(dp[n])return dp[n];//已经计算过的就不会再计算了
        if(n<=2)
        return 1;
        return (fib(n-1)+fib(n-2))%p; 
}
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        for(int i=1;i<=n;i++)cout<<fib(i)<<(i==n?"\n":" ");
        return 0;
 } 

进制转换

  1. 进制的本质
  • 十进制:每一个数位上的数字乘以这一位上的权重。
  1. 将任意进制转化为十进制
    //公式
ll x=0;
for(int i=0;i<=n;++i)
{
        x=x*k+a[i];
}
cout<<x<<"\n";

这个k进制的数组可以通过对字符串的处理得到。
3. 10进制转化为任意进制

ll x;
cin>>x;
while(x)
{
        a[++cnt]=x%k;
        x/=k;
        reverse(a+1,a+1+cnt);//注意要翻转一下,才能使高位在1的位置 
}
  1. 例题
  • 16进制转化为10进制–2489
    //模版处理法
#include<bits/stdc++.h>
using ll=long long;
using namespace std;
const int N=50;
int a[N];
int main()
{
        string s="2021ABCD";
        //先存放a数组
        for(int i=0;i<s.length();i++)
        {
                if('0'<=s[i]&&s[i]<='9')a[i+1]=s[i]-'0';//希望让数组的第一个元素(即a[1])对应于字符串s的第一个字符
                else a[i+1]=s[i]-'A'+10;
         } 
         ll x=0;
         for(int i=1;i<=s.length();i++)
         {
                 x=x*16+a[i];
         }
         cout<<x<<'\n';
         return 0;
}

//自带方法

#include<bits/stdc++.h>
using ll=long long;
using namespace std;
int main()
{
        unsigned int x=0x2021ABCD;
         cout<<x<<'\n';
         return 0;
}
- 九进制转十进制
#include<bits/stdc++.h>
using ll=long long;
using namespace std;
const int N=50;
int a[N];
int main()
{
        string s="2022";
        for(int i=0;i<=s.length();i++)
        {
                a[i]=s[i-1]-'0';
        }
        ll x=0;
        for(int i=1;i<=4;i++)
        {
                x=x*9+a[i]; 
        }
        cout<<x<<endl;
        return 0;
        
}
  • 任意进制间的转换–都是先转化为10进制再转化为其他进制。
#include<bits/stdc++.h>
using ll=long long;
using namespace std;
const int N=1000;
int a[N];
char ch[]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
void solve()
{
    int n,m;cin>>n>>m;
    string s;cin>>s;
    int len=s.length();
    s="$"+s;
    for(int i=1;i<=len;i++)
    {
        if(s[i]>='0'&&s[i]<='9')a[i]=s[i]-'0';//转化为数字 
        else a[i]=s[i]-'A'+10;
    }
    ll x=0;
    for(int i=0;i<=len;i++)
    {
        x=x*n+a[i];
    } 
    //把十进制转化为m进制
    string ans;
    while(x)
    {
        ans+=ch[x%m];
        x/=m;
    }
    reverse(ans.begin(),ans.end());
    if(ans.empty()) cout<<0<<'\n';
    else cout<<ans<<'\n';
}
int main()
{
    int t;cin>>t;
    while(t--)
    {
        solve();  
    }
}

二分

  1. 二分法的简介
  • 搜索范围一分为2,迭代的缩小搜索范围。
  • 二分法本质是枚举。
  • 解题步骤:
  • 发现数据结构(或答案变量)的单调性
  • 确定最大区间,确保分界点一定在里面。细节:若以r为答案,则答案区间为:[l+1,r],若以l为答案,那么答案区间为[l,r-1]
  • 确定check函数:一般传入mid(区间中某个下标),返回mid所属区域或返回一个值,当check函数较简单时可以直接判断。
  • 计算中点mid=(1+R)/2 ,用check函数判断该移动l或r指针,具体移动哪个需要根据单调性 以及要求的答案来判断。
  • 返回l或r,根据题意来判断。
  1. 整数二分
int l=0;r=1e9;//找到升序数组a中x第一次出现的位置
while(l+1!=r)//l,r相邻时退出 
{
        int mid=(l+r)/2;
        //如果为升序,说明mid偏大了,如果为降序说明mid偏小了
        if(a[mid]>=x)r=mid;//x是要查找的这个数
        else l=mid; 
 } 
 cout<<r<<endl;

例题:1389

#include<bits/stdc++.h>
using namespace std;

int main()
{
    int data[200];
    for(int i=0;i<200;i++)data[i]=4*i+6; // 生成一个有序数组data,其中每个元素的值为4*i+6
    int x;cin>>x; // 输入要查找的值x
    int l=-1;int r=199; // 初始化左右边界l和r,l初始为-1,r初始为199

    while(l+1!=r) // 当左边界加1不等于右边界时,继续执行循环
    {
        int mid;
        mid=(l+r)>>1; // 计算中间位置mid,使用位运算实现除以2向下取整
        if(data[mid]>=x)r=mid; // 如果中间位置的元素大于等于x,则将右边界更新为mid
        else l=mid; // 否则将左边界更新为mid
    }

    cout<<r<<endl; // 输出最终结果r,即x在数组中的位置
    return 0;
}

注意:当x一定在数组中时,输出的结果是x的下标,否则输出的是第一个大于等于x的元素的下标。

#include<bits/stdc++.h>
using namespace std;
int main()
{
        //使用lower_bound函数来实现
        vector<int>data(200);
        for(int i=0;i<200;++i)
        {
                data[i]=4*i+6;
         } 
         int x;cin>>x;
         auto it=lower_bound(data.begin(),data.end(),x);//进行二分查找
         if(it!=data.end())
         {
                 cout<<distance(data.begin(),it)<<endl;
          } 
        return 0;
}
  1. 浮点二分
  • 搜索范围变成实数范围。
  • 和整数二分的区别就是使用的变量类型和退出条件不一样。
double l=0,r=1e9,eps=1e-6;//设置一个极小量
while(l-r>=eps)//当区间端点无限接近时退出
{
        double mid=(l+r)/2;
        if(data[mid]>=0)r=mid;
        else l=mid; 
 } 
 cout<<r<<endl;
  1. 二分答案(最重要)
  2. 二分答案介绍
  • 常见模型:二分框架+check函数。
  • 对象:一般情况下将答案进行二分,然后枚举出某个可能解后判断其是否可以更优或者是否合法,从而不断逼近最优解。
  • 特征:如果已知某个答案,很容易判断是否合法或更优。某些贪心问题可以转变为二分答案。
  • 模版
bool check(int mid)
{
        bool res=true;
        //do
        return res;
}
int main()
{
        int l=0;r=1e9;
        while(l+1!=r)
        {
                int mid=(l+r)/2;
                if(check(mid))l=mid;
                else r=mid;
        }
        cout<<l<<endl;
}
  • 例题
    • 364跳石头
      [图片]
#include<bits/stdc++.h>
const int N=1e5+9;
using ll=long long;
using namespace std;
int a[N],L,n,m;
int check(int mid)
{
        int res=0;int lst=0; 
        for(int i=1;i<=n;++i)//lst表示上一块石头的位置 
        {
                if(a[i]-a[lst]<mid)
                {
                        res++;
                        continue;
                 } //距离跳不到,跳过这个 
                 lst=i; 
    }        
    if(L-a[lst]<mid)return m+1;//最后一位是不能挪走的 
    return res;
}
int main()
{ 
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin>>L>>n>>m;
        for(int i=1;i<=n;i++)cin>>a[i];
        ll l=0,r=1e9+5;//这个区间是需要根据题意自己确定的
         
        while(l+1!=r)
        {
                ll mid=(l+r)/2;
                if(check(mid)<=m)l=mid;
                else r=mid;
        }
        cout<<l<<endl;
        return 0;
}
  • 肖恩的苹果林3683
#include <bits/stdc++.h>using namespace std;
const int N= 1e5+5;
using ll= long long;
int n,m;
int a[N];
int check(int mid){
  int ans=0,lst=0;
  for(int i=1;i<=n;i++){
    if(lst&&a[i]-a[lst]<mid)continue;
    ans++,lst=i;
  }
return ans;
}
int main(){
  
  cin>>n>>m;
  for(int i =1;i<=n;i++)cin>>a[i];
  sort(a+1,a+n+1);//别忘了对坐标进行排序
  ll l=0,r=1e9;
  ll mid;
  while(l+1!=r){
    mid=(l+r)/2;
    if(check(mid)>=m)l=mid;
    else r=mid;
  }
  cout<<l;
  return 0;
}
  • 3404肖恩的乘法表
    [图片]
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
using ll=long long;
ll n,m,k;
ll rnk(ll mid)
{
        ll res=0;
        for(int i=1;i<=n;++i)
        {
                res+=min(m,mid/i);
        }
        return res;
}
int main()
{
        
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin>>n>>m>>k;
        ll l=0;ll r=1e14;
        while(l+1!=r)
        {
                ll mid=(l+r)>>1;
                if(rnk(mid)>=k)r=mid;
                else l=mid;
        }
        cout<<r<<endl;
        return 0;
}

贪心

  1. 贪心算法介绍
  • 模型很多,需要大量的做题来积累
  • 基本原理:每一步都是局部最优解,最终达到全局最优解。
  • 特征:贪心选择性质,最优子结构性质,很多贪心题目会出现不同操作产生贡献相同,再次特征下每次选择代价最小的。
  1. 实现
    1.确定问题的最优子结构(贪心往往和排序、优先队列等一起出现)。
    2.构建贪心选择的策略,可能通过“分类讨论”、“最小代价”、“最大价值”等方式来思考贪心策略。简单验证贪心的正确性,采用句式一般是:这样做一定不会使得结果变差、不存在比当前方案更好的方案等等。
    3.通过贪心选择逐步求解问题,直到得到最终解。
  2. 常见模型与例题
  • 3412最小化战斗力差距
    进行排序,左边是a右边是b,边界差最小。
    启发:当混乱的数据不好处理且排序不影响答案时,先排序后分析。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
using ll=long long;
int a[N];
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        for(int i=1;i<=n;++i)
        {
                cin>>a[i];
        }
        sort(a+1,a+n+1);//进行排序 
        int ans=a[2]-a[1];//初始化 
        for(int i=1;i<n;i++)
        {
                ans=min(ans,a[i+1]-a[i]);
        }
        cout<<ans<<endl; 
        return 0;
}
  • 545谈判
    总操作数下的最小代价–优先队列
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
using ll=long long;
priority_queue<ll,vector<ll>,greater<ll>>pq;
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        for(int i=0;i<n;i++)
        {
                ll x;cin>>x;
                pq.push(x);
        }
        ll ans=0;//计算代价
        while(pq.size()>1)//队列中没有元素 时退出
        {
                ll x=pq.top();pq.pop();//第一小的
                ll y=pq.top();pq.pop();//第二小的
                ans+=x+y;
                pq.push(x+y);//把和也放到队列之中去 
         } 
         cout<<ans<<endl; 
        
        return 0;
}
  • 532纪念品分组
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
int a[N];
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int w;cin>>w;
        int n;cin>>n;
        for(int i=1;i<=n;i++)
        {
                cin>>a[i];
         } 
         sort(a+1,a+n+1); 
         int l=1,r=n,ans=0;//设置区间和组数 
         while(l<=r)
         {
                 ans++;
                 if(l==r)
                 {
                         break;
                 }
                 if(a[l]+a[r]<=w)
                 {
                         l++;r--;
                 }
                 else r--;
         }
        cout<<ans<<endl;
        return 0;
}

STL的解法

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
bool comp (int a,int b) {return a>b;}
int main()
{
    int w,n,num=0;
    cin>>w>>n;
    vector <int> a;
    for(int i=0;i<n;i++)
    {
        int temp;
        scanf("%d",&temp);
        a.push_back(temp);
    }
    sort(a.begin(),a.end(),comp);
    while(1)
    {
        if(n==0) break;
        if( n>=2 && a.front()+a.back()>w)
        {
            num++;
            a.erase(a.begin());n--;
        }
        else if (n==1){ a.erase(a.begin());num++;n--;}
        else
        {
            a.erase(a.begin());
            a.erase(a.begin()+a.size()-1);
            num++;n-=2;
        }
    }
    cout<<num<<endl;
}
  • 2928分糖果
    补充:字典序。字典序通常用于比较两个字符串的顺序。这种比较方法是从左到右逐个字符进行比较,直到找到第一个不相同的字符为止。这个字符的ASCII值决定了两个字符串的大小关系。如果两个字符串从头开始的字符都相同,但长度不同,则较短的字符串被认为较小。如果所有字符都相同,则两个字符串被认为是相等的。
    [图片]
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+9;
char s[N];
int main()
{
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  int n,x;cin>>n>>x;
  cin>>s+1;
  sort(s+1,s+1+n);
  //开始做判断
  if(s[1]==s[n])
  {
    for(int i=1;i<=n/x+(n%x?1:0);i++)cout<<s[i];//括号中判断的是是否会多出来
     
   } 
   else if(s[1]==s[x])
   {
    for(int i=x;i<=n;i++)cout<<s[i];
    } 
    else cout<<s[x];
  return 0;
}

双指针

  1. 简介
    双指针算法是一种常用的算法技巧,它通常用于在做组或字符中进行快速查找匹配排序或移动操作。
    双指针并非真的用指针实现,一般用两个变量来表示下标(在后面都用指针来表示)。
    双指针算法使用两个指针在数据结构上进行迭代,并根据问题的要求移动这些指针。
    双指针往往也和单调性、排序联系在一起,在数组的区间问题上,暴力法的时间复杂度往往是O(n^2)的,但双指针利用“单调性”可以优化到O(n)。
    常见的双指针模型有: 1)对撞指针 2)快慢指针
  2. 对撞指针
    指的是两个指针left、right分别指向序列第一个元素和最后一个元素。然后l 指针不断递增,r不断递减,直到两个指针的值相撞或错开(即1>=r),或者满足其他要求的特殊条件为止。
    对撞指针一般用来解决有序数组或者字符串问题(常见于区间问题):查找有序数组中满足某些约束条件的一组元素问题:比如二分查找、数字之和等问题。
    字符串反转问题:反转字符串、回文数、颠倒二进制等问题。
  • 解题步骤
    1.使用两个指针left,right。.left指向序列第一个元素,即:left=1,right指向序列最后一个元素,即:right=n。
    2.在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移,left++。当满足另外一定条件时,将右指针左移,right–。
    3.直到两指针相撞(即left==right),或者满足其他要求的特殊条件时,跳出循环体。
  • 例题
  • 1371回文判定
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+9;
char s[N];
int main()
{
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  cin>>s+1;//从1开始存
  int n=strlen(s+1); 
  int left=1; int right=n;
  bool ans=true;
  while(left<right)
  {
    if(s[left]!=s[right])ans=false;
    left++;right--;
  }
  cout<<(ans?"Y":"N")<<endl;
  return 0;
}
  1. 快慢指针
    快慢指针一般比对撞指针更难想,也更难写。
    指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢。移动快的指针被称为快指针,移动慢的指针被称为慢指针
    。为了方便理解,我们称快指针为r,慢指针为1,这样慢指针和快指针构成区间 [L,R] .两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。
  • 解题步骤
    1.使用两个指针l、r。l一般指向序列第一个元素,即:l=1,r一般指向序列第零个元素,即:r=0。即初始时区间 [l,r] = [1,0] 表示为空区间。
    2.在循环体中将左右指针向右移动。当满足一定条件时,将慢指针右移,即 l ++。当满足另外一定条件时(也可能不需要满足条件),将快指针右移,即r++,保持 [l,r] 为合法区间。
    3.到指针移动到数组尾端(即ln|| rn),或者两指针相交,或者满足其他特殊条件时跳出循环体。
  • 例题
  • 1372美丽的区间
    i和j都要右移,典型的快慢指针问题。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
using ll=long long;
int a[N],s;
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n,s;
        cin>>n>>s;
        for(int i=1;i<=n;++i)
        {
                cin>>a[i];
        }
        int ans=n+1;
        for(int i=1,j=0,sum=0;i<=n;++i)
        {
                //考虑移动,当区间不合法一定要移动 
                while(i>j||((j+1)<=n&&sum<s))sum+=a[++j];//等同于j++;sum+=a[j];
                if(sum>=s)ans=min(ans,j-i+1); 
                sum-=a[i];
        }
        cout<<(ans>n?0:ans)<<endl; 
        return 0;
}
  • 1621挑选子串
    先找到最小的区间。再移动时的新区间不会比这个区间小。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+9;
using ll=long long;
int a[N];
int main()
{
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  int n,m,k;cin>>n>>m>>k;
  int ans=0;
  for(int i=1;i<=n;i++)cin>>a[i];
  for(int i=1,j=0,cnt=0;i<=n;++i)
  {
    while(i>j||(j+1<=n&&cnt<k))cnt+=(a[++j]>=m);
    if(cnt>=k)ans+=n-j+1;
    cnt-=(a[i]>=m);
  }
  cout<<ans<<endl;
  return 0;
}

补充解释:cnt+=(a[++j]>=m);

  1. a[++j]:这部分表示将数组 a 中索引为 j 的元素值赋给变量 j,并将 j 自增 1。换句话说,它将数组 a 中的下一个元素值赋给 j,并使 j 指向下一个位置。
  2. (a[++j]>=m):这部分是一个条件表达式,用于判断 a[++j] 是否大于等于 m。如果条件成立,即 a[++j] 大于等于 m,则返回值为 1;否则返回值为 0。
  3. cnt+=(a[++j]>=m):这部分将上述条件表达式的结果加到变量 cnt 上。如果条件成立,即 a[++j] 大于等于 m,则 cnt 的值增加 1;否则 cnt 的值不变。
    枚举
  4. 枚举算法介绍
    枚举算法是一种基本的算法思想,它通过穷举所有可能的情况来解决问题。
    它的基本思想是将问题的解空间中的每个可能的解都枚举出来,并进行验证和比较,找到满足问题条件的最优解或者所有解。
    枚举算法适用于问题规模较小、解空间可穷举的情况。它的优点是简单直观,不需要复杂的数学推导,易于实现。
    但是,由于需要穷举所有可能的情况,对于问题规模较大的情况,枚举算法的时间复杂度可能会非常高,效率较低。
  5. 解空间的类型
    解空间可以是一个范围内的所有数字(或二元组、字符串等数据),或者满足某个条件的所有数字。
    当然也可以是解空间树,一般可分为子集树和排列树,针对解空间树,需要使用回溯法进行枚举。
    我们目前仅使用循环去暴力枚举解空间,具体的解空间类型需要根据题目来理解构造。
  6. 循环枚举解空间
    1首先确定解空间的维度,即问题中需要枚举的变量个数。例如当题目要求的是满足条件的数字时,我们可以循环枚举某个范围内的数字。如果要求的是满足条件的二元组,我们可以用双重循环分别枚举第一个和第二个变量,从而构造出一个二元组。
    2.对于每个变量,确定其可能的取值范围。这些范围可以根据问题的性质和约束条件来确定。这一步往往是时间复杂度优化的关键。
    3.在循环体内,针对每个可能解进行处理。可以进行问题的验证、计算、输出等操作。
  7. 例题
  • 191特别数的和
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
bool f(int x)
{
        while(x)
        {
                int y=x%10;
                if(y==2||y==0||y==1||y==9)
                return true;
                x/=10;//这一步是为了退出循环条件 
        }
        return false;
}
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        ll sum=0;
        for(int i=1;i<=n;++i)
        {
                if(f(i))
                sum+=i;
        }
        cout<<sum<<endl;
        return 0;
 } 

注意这里的处理格式和思想。

  • 152反倍数
#include<bits/stdc++.h>
using namespace std;
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        int count=0;
        int x,y,z;cin>>x>>y>>z;
        for(int i=1;i<=n;++i)
        {
                if(i%x!=0&&i%y!=0&&i%z!=0)count++;
        }
        cout<<count<<endl;
        return 0;
 } 
  • 3227找到最多的数
#include<bits/stdc++.h>
using namespace std;
map<int,int>mp;
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*m;++i)
        {
                int x;cin>>x;
                mp[x]++;
        }
        for(const auto &pair:mp)
        {
                if(2*pair.second>n*m)cout<<pair.first<<endl;
        }       
        return 0;
 } 

时空复杂度

  1. 时间复杂度
    1)时间复杂度是衡量算法执行时间随输入规模增长的增长率。
    2)通过分析算法中基本操作的执行次数来确定时间复杂度。
    3)常见的时间复杂度包括:常数时间O(1)、线性时间O(n)、对数时间O(logn)、平方时间 0(n^2)等。
    4)在计算的时候我们关注的是复杂度的数量级,并不要求严格的表达式。
    一般我们关注的是最坏时间复杂度,用O(f(n))表示,大多数时候我们仅需估算即可。
    一般来说,评测机1秒大约可以跑2e8次运算,我们要尽可能地让我们的程序运算规模级控制在1e8以内。
  2. 空间复杂度
    1)空间复杂度是衡量算法执行过程中所需的存储空间随输入规模增长的增长率。
    2)通过分析算法中所使用的额外存储空间的大小来确定空间复杂度。
    3)常见的空间复杂度包括:常数空间O(I)、线性空间O(n)、对数空间O(logn)、平方空间 0(n^2)等。一般我们关注的是最坏空间复杂度,用O(f(n))表示,大多数时候程序占用的空间一般可根据开的数组大小精确算出,但也存在需要估算的情况。
    题目一般不会卡空间,一般是卡时问举个例子,假如题目限制128MB,1int32bit4Byte5,128MB32*220int3e7int
  3. 分析技巧
    1。理解基本操作:基本操作可以是算术运算(加法、乘法、位运算等)、比较操作、赋值操作等。
    2。关注循环结构:循环是算法中常见的结构,它的执行次数对于时间复杂度的分析至关重要。
    3。递归算法:递归算法的时间和空间复杂度分析相对复杂。需要确定递归的深度以及每个归调用的时间和空间开销。(一般画一个递归树来估算)
    例:n层二叉树,有2^n(-1)个结点。执行这些次递归调用。
    4.最坏情况分析:对于时间复杂度的分析,通常考虑最坏情况下的执行时间。要考虑输入数据使得算法执行时间达到最大值的情况。
    5.善用结论:某些常见算法的时间和空间复杂度已经被广泛研究和证明。可以利用这些已知结果来分析算法的复杂度。

    [图片]

![[图片](https://img-blog.csdnimg.cn/direct/4960ee61b89642a28fa8befc042c37c2.png)

离散化

  1. 简介
    把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。
    离散化是一种将数组的值域压缩,从而更加关注元素的大小关系的算法。
    当原数组中的数字很大、负数、小数时(大多数情况下是数字很大),难以将“元素值”表示为“数组下标”,一些依靠下标实现的算法和数据结构无法实现时,我们就可以考虑将其离散化。
    例如原数组的范围是[1,19],而数组大小仅为1e5,那么说明元素值的“种类数”最多也就 1e5种,从而可以利用一个数组(即离散化数组)来表示某个元素值的排名(即第几小)现值域的压缩,将原数组的元素值作为下标来处理。
  • 离散化数组一般是有序且去重的。
  • 可以通过离散化下标得到值也可以通过值得到离散化下标。
  1. 实现方法
  • 开一个vector数组
#include<bits/stdc++.h>
using namespace std;
vector<int>l;//离散化数组 
int getidx(int x)
{
        return lower_bound(l.begin(),l.end(),x)-l.begin();//得到元素x的下标 
 } 
 const int N=1e5+9;
 int a[N];//原数组 
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        for(int i=1;i<=n;++i)cin>>a[i];
        for(int i=1;i<=n;++i)l.push_back(a[i]);//将元素存入离散化数组
        l.erase(unique(l.begin(),l.end()),l.end());//排序去重 
        //将重复元素移到末尾然后去除重复元素 
        return 0;
 } 
  • 离散化一般不会单独考察,都是结合其他算法或数据结构一起考查,例如树状数组,线段树,二维平面的计算几何等等。
  • 例题:给定a数组,求a的离散化数组,并可以通过值找到下标。
#include<bits/stdc++.h>
using namespace std;
vector<int>l;//离散化数组 
int getidx(int x)
{
        return lower_bound(l.begin(),l.end(),x)-l.begin();//得到元素x的下标 
 } 
 const int N=1e5+9;
 int a[N];//原数组 
int main()
{
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        int n;cin>>n;
        for(int i=1;i<=n;++i)cin>>a[i];
        for(int i=1;i<=n;++i)l.push_back(a[i]);//将元素存入离散化数组
        sort(l.begin(),l.end());//如果是无序数组需要先进行排序 
        l.erase(unique(l.begin(),l.end()),l.end());//排序去重 
        //将重复元素移到末尾然后去除重复元素 
        cout<<"离散化数组为:"<<endl;
        for(const auto &i:l)
        {
                cout<<i<<' ';
         } 
         cout<<endl;//要想输入和输出之间有间隔必须要换行才可以
         int val;cin>>val;
         cout<<val<<"的下标为:"<<getidx(val)<<'\n';
        return 0;
 } 

模拟

  1. 模拟算法介绍
    模拟算法通过模拟实际情况来解决问题,一般容易理解但是实现起来比较复杂,有很多需要注意的细节,或者是一些所谓很“麻烦”的东西。
    模拟题一般不涉及太难的算法,一般就是由较多的简单但是不好处理的部分组成的,考察选手的细心程度和整体的逻辑思维。
    一般为了使得模拟题写的逻辑清晰一些,经常会写比较多的小函数来帮助解题,例如it和 string的相互转换、回文串的判断、日期的转换、各种特殊条件的判断等等。
  2. 例题
  • 549扫雷
#include<bits/stdc++.h>
using namespace std;
const int N=150;
int mp[N][N],ans[N][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)//输入矩阵 
        {
                for(int j=1;j<=m;++j)
                {
                        cin>>mp[i][j];
                }
        }
        for(int i=1;i<=n;++i)//扫描整个二维数组 
        {
                for(int j=1;j<=m;++j)
                {
                        if(mp[i][j]==1)
                        {
                                ans[i][j]=9;
                                continue;
                        }
                        //扫描九宫格
                        for(int i_=max(1,i-1);i_<=min(n,i+1);++i_)//遍历周围行 
                        {
                                for(int j_=max(1,j-1);j_<=min(m,j+1);++j_)//遍历周围列
                                {
                                        if(mp[i_][j_])ans[i][j]++;//有1则加上
                                }
                        }         
                }
        }
        for(int i=1;i<=n;++i)//输出 
        {
                for(int j=1;j<=m;++j)
                {
                        cout<<ans[i][j]<<' ';
                }
                cout<<"\n";
        }
        
        return 0;
 } 

解释:

  1. 第三个循环:for(int i_=max(1,i-1);i_<=min(n,i+1);++i_),这个循环用于遍历当前元素周围的行。其中,max(1,i-1)和min(n,i+1)确保了不会超出矩阵的范围。
  2. 第四个循环:for(int j_=max(1,j-1);j_<=min(m,j+1);++j_),这个循环用于遍历当前元素周围的列。其中,max(1,j-1)和min(m,j+1)确保了不会超出矩阵的范围。
  • 551灌溉
    设置两个数组–一直更新
#include<bits/stdc++.h>
using namespace std;
const int N=100;
bool a[N][N],b[N][N];//仅表示是否被灌溉到即可 


int main()
{

         int n,m;cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
                for(int j=1;j<=m;++j)
                {
                        cin>>a[i][j];
                }
    }        
    int t;cin>>t;
    for(int i=1;i<=t;++i)
    {
            int x,y;
            cin>>x>>y;
            a[x][y]=1;//表示已经被灌溉到 
        }
        int k;cin>>k;
        while(k--)
        {
                //要用a数组来更新b数组,先扫描a数组
                for(int i=1;i<=n;++i)
                {
                        for(int j=1;j<=m;++j)
                        {
                                if(a[i][j])//此位置已经被灌溉,则下一秒钟: 
                                b[i][j]=b[i-1][j]=b[i+1][j]=b[i][j-1]=b[i][j+1]=1;
                                //因为这里要复制回去,所以b本身也要复制为1
                        }
                 } 
                 //把b数组复制回给a
                 for(int i=1;i<=n;i++)
                {
                        for(int j=1;j<=m;++j)
                        {
                                a[i][j]=b[i][j];
                        }
                }
         } 
         int ans=0;
         for(int i=1;i<=n;i++)
         {
                 for(int j=0;j<=n;++j)
                 {
                         if(a[i][j])ans++;//扫描a数组中有多少个元素被灌溉 
                 }
         }
         cout<<ans<<endl;
         return 0; 
}
  • 498回文日期
    先拆分:
    year:2020~9999
    month:判断闰年1~12
    day:
    我们需要这样几个函数:
  1. int转化为string函数
  2. string转化为int函数
  3. 判断闰年
  4. 判断日期是否合法
  5. 判断字符串是否回文
  6. 判断字符串是否是ABABBABA型
    想法:
    模拟题中string类型常用且好用
#include<bits/stdc++.h>
using namespace std;
//string转化为int的函数
int s2i(string s)
{
    int res=0;
    for(const auto &i:s)res=res*10+i-'0';
    return res;
 } 
//int转化为string的函数
string i2s(int x,int w)
{
    string res;
    while(x)res+=(x%10)+'0',x/=10;
    while(res.length()<w)res+='0';
    reverse(res.begin(),res.end());
    return res;
 } 
 //判断是否是闰年
 bool isleepyear(int year)
 {
     return ((year%4==0&&year%100!=0)||(year%400==0));
 }
 //判断日期是否合法
 bool isok(int year,int month,int day)
 {
     int days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};
     if(isleepyear(year))days[2]=29;
     return day<=days[month];//在合法的数组中,日期合法 
  } 
  //判断是否回文
  bool ispa(string s)
  {
    for(int i=0;i<s.length()/2;++i)
    {
        if(s[i]!=s[s.length()-i-1])return false;
    }
    return true;
   } 
   //判断类型
   bool isab(string s)
   {
       if(!ispa(s))return false;//连回文都不是
    return s[0]==s[2]&&s[1]==s[3]; 
   } 


int main()
{
    string s;cin>>s;
    //分割
    int year=s2i(s.substr(0,4)),month=s2i(s.substr(4,2)),day=s2i(s.substr(6,2));
    bool ans1=false;bool ans2=false;//留两个变量表示是否找到了答案 
    for(int i=year;i<=9999;++i)
    {
        for(int j=1;j<=12;++j)
        {
            if(i==year&&j<month)continue;
            for(int k=1;k<=31;++k)
            {
                if(i==year&&j==month&&k<=day)continue;
                if(!isok(i,j,k)) continue;
                string date=i2s(i,4)+i2s(j,2)+i2s(k,2);
                if(!ans1 && ispa(date))
                {
                    cout<<date<<'\n';
                    ans1=true;
                }
                if(!ans2 && isab(date))
                {
                    cout<<date<<'\n';
                    ans2=true;
                }
                if(ans1 && ans2) break;
            }
            if(ans1 && ans2) break;
        }   
        if(ans1 && ans2) break;
    }   
    return 0;
}

//另一种解法

#include <iostream>
using namespace std;
bool isLeap(int y){
    return (y%4==0&&y%100!=0)||(y%400==0);
}

bool check(int year,int month,int day){//判断是否为合法日期if(month>12||month==0) return false;
    if(day>31) return false;
    if(month==2){
        if(isLeap(year)&&day>29)
            return false;
        if(!isLeap(year)&&day>28)
            return false;
    }
    if(month==4||month==6||month==9||month==11){
        if(day>30) return false;
    }
    return true;
}
int main()
{
    int n,i;
    cin>>n;
    int a,b,c,d,e,f,g,h;//8位数字int year,month,day;
    bool flag=false;
    for(i=n+1;i<=99999999;i++){
        year=i/10000;
        month=(i%10000)/100;
        day=i%100;
        a=i%10;
        b=(i/10)%10;
        c=(i/100)%10;
        d=(i/1000)%10;
        e=(i/10000)%10;
        f=(i/100000)%10;
        g=(i/1000000)%10;
        h=(i/10000000)%10;
        if(a==h&&b==g&&c==f&&d==e&&flag==false){
            if(check(year,month,day)){
                cout<<i<<endl;
                flag=true;//只输出一个回文
            }
        }
        if(a==h&&b==g&&c==f&&d==e&&a==c&&b==d){
            if(check(year,month,day)){
                cout<<i<<endl;
                break;
            }
        }
    }
    return 0;
}

构造

介绍
构造题在比赛和解决问题的过程中确实是常见的一类题型。它们通常要求解题者通过观察问题的结构和规律,找到一种通用的方法或模式,使得在问题规模增大时,依然能够高效地得到答案。
在解决构造题时,以下几点思考是很重要的:观察问题规模的增长:了解问题随着规模的增大,答案的变化趋势。这可以帮助你找到一种通用的解决方案。
·推广规律:尝试将你观察到的规律推广到更大的问题规模上。这可能涉及到数学归纳法或者其他类似的思考方式。
特点

  1. 高自由度。
  2. 形式灵活和多样。
  3. 步骤:
  • 分析题目要求和条件:
  • 仔细阅读题目,确保你理解了题目的要求和所给的条件。
  • 弄清楚问题的背景和目标,明确你需要构造的内容或解决的问题。
    尝试特例和极端情况:
    试图找出一些特殊情况下的解法,这可以帮助你更好地理解问题的本质。
  • 探索极端情况,看看在极端条件下是否会出现特殊的构造方式或者规律。
    寻找模式和规律:
    观察题目中是否存在一些明显的模式或者规律,这可能是解决问题的关键。
  • 尝试从已知的情况中找到一般性的解法,并推广到更一般的情况。
  1. 特点:
  • 尝试逆向构造:·从问题的反面思考也可以给出有用的线索。尝试反向推导出符合条件的情况。
  • 使用数学归纳法:·尝试使用数学归纳法证明某种构造方式在所有情况下都成立。
  • 灵活运用已知知识:·将已学的数学、物理、逻辑等知识灵活应用,可能会为你找到新的解法。
  • 反复实践和总结:·多做类似的题目,总结解题经验,找出有效的解题方法,虽然不存在通解,但是部分构造题可能会用到相似的套路,如果能够有做题的广度并且经常总结,那么对于你解决构造题目是非常有帮助的。
  • 保持耐心和信心:·构造题可能需要时间和多次尝试才能找到合适的解法,保持耐心和信心是很重要的
  1. 应用场景:
  • 数学:构造满足一定的数列、集合或排列组合。利用数学关系构造出特定的解。
  • 图论:构造特定的图结构,如树、图、有向图。
  • 字符串处理:构造出满足某种性质的字符串,如回文串。
  • 组合排列。
  • 游戏策略:设计游戏规则,构造出有趣的游戏场景。
  • 逻辑推理:构造逻辑谜题或者推理思想。
  • 数据结构:堆,树,图。
  • 动态规划:构造状态转移方程,设计合适的状态表示,构造动态规划解。
  • 贪心算法:构造出合适的贪心策略,使贪心策略能得到最优解。
  • 模拟问题:设计模拟题,要求选手模拟特定的场景或过程,根据模拟的结果得出答案。
    例题

[图片]

[图片]

//举例子:发现规律--n--n--3n--6n
#include <iostream>
using namespace std;
int main() {
    int N;
    cin >> N;
    bool found = false;
    for (int x = 1; x <= N; x++) {
        for (int y = 1; y <= N; y++) {
            for (int z = 1; z <= N; z++) {
                if (x * y * z == N && (x % N + y % N + z % N) == N) {
                    cout << x << " " << y << " " << z << endl;
                    found = true;
                    break;
                }
            }
            if (found) break;
        }
        if (found) break;
    }
    if (!found) cout << "No Solution" << endl;
    return 0;
}

[图片]

解析:
所有数字间距离最小的间隔是公差吗?等差数列的最小间隔(实际上不是公差),例如{2,5,7},最小的间隔是2,但公差不是2,是1。
这其实一个GCD构造问题,可以通过计算给定数字间的所有间隔的最大公约数(GCD)来确定。把n个数据排序,计算它们的间隔,对所有间隔做GCD,结果为公差。同时,通过最小值、最大值和公差,可以计算出等差数列的最少数量。
最少数量等于=(最大值-最小值)/公差+1。从最小值到最大值依次输出即可。

#include<bits/stdc++.h>
using namespace std;
const int N=100;
int a[N];
//计算最大公约数
int gcd(int a,int b) {
    return b ? gcd(b, a % b) : a;
}
int main()
{
    int n;cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    //计算间隔
    sort(a,a+n);
    int maxa=0;
    for(int i=0;i<=n;i++)
    {
        maxa=gcd(maxa,a[i]-a[i-1]);
     } 
     //控制输出
         if(!maxa)cout<<n;
         else
         cout<<"等差数列最少的项数为:"<<(a[n-1]-a[0])/maxa+1<<endl;
     //输出序列
          for(int i=0;i<(a[n-1]-a[0])/maxa+1;i++)
          {
                  cout<<a[0]+i*maxa<<" ";
          }
    return 0;
}

题目描述:给定一个正整数N,你需要构造一个包含N个节点的简单连通图。节点编号从 1到N。同时,设任意边的两个节点的编号为a,b。a,b需要满足:(a&1)Xor(b&1)=1。每条边的权值为边所连接的两个节点的编号和,使得的最长边和最短边之间的差值小于等于3。
输入描述:输入包含一个正整数N(2≤N≤1800)。
输出描述:先输出一个M表示总共M条连边。然后输出包含M行,每行包含两个整数,表示图中一条边的两个节点编号。

#include <iostream>
using namespace std;

int main() {
    int N;
    cin >> N;
    int M = 0;
    for (int i = 1; i <= N; i++) {
        for (int j = i + 1; j <= N; j++) {
            if (((i & 1) ^ (j & 1)) == 1 && (i + j) - min(i, j) <= 3) {
                cout << i << " " << j << endl;
                M++;
            }
        }
    }
    cout << M << endl;
    return 0;
}

[图片]

#include <iostream>
#include <vector>
using namespace std;

// 判断n是否能被divisor整除
bool isDivisible(int n, int divisor) {
    return (n % divisor == 0);
}

// 判断n的每一位数字是否为负数
bool isNegativeDigit(int n) {
    while (n > 0) {
        int last_digit = n % 10;
        if (last_digit < 0) {
            return true;
        }
        n /= 10;
    }
    return false;
}

// 判断n是否满足条件:不能被2到sqrt(n)之间的任何数整除,且这些数的每一位数字都不是负数
bool isSatisfyCondition(int n) {
    for (int i = 2; i * i <= n; i++) {
        if (isDivisible(n, i)) {
            return false;
        }
        if (isNegativeDigit(i)) {
            if (isDivisible(n, -i)) {
                return false;
            }
        }
    }
    return true;
}

int main() {
    int t;
    cin >> t; // 输入测试用例数量
    vector<int> testCases(t); // 存储测试用例
    for (int i = 0; i < t; i++) {
        cin >> testCases[i]; // 输入每个测试用例
    }
    for (auto n : testCases) { // 遍历每个测试用例
        if (isSatisfyCondition(n)) { // 如果满足条件
            cout << n << endl; // 输出该数
        } else {
            cout << "No suitable number found." << endl; // 不满足条件,输出提示信息
        }
    }
    return 0;
}

[图片]

#include <iostream>
using namespace std;
int main() {
    int t;
    cin >> t;
    while (t--) {
        long long n;
        cin >> n;
        if (n % 3 == 0) {
            cout << "0 " << n / 3 << " " << n / 3 << endl;
        } else if (n % 3 == 1) {
            cout << "0 " << (n - 1) / 3 << " " << (n - 1) / 3 + 1 << endl;
        } else {
            cout << "0 " << (n - 2) / 3 << " " << (n - 2) / 3 + 2 << endl;
        }
    }
    return 0;
}
  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值