1 基础算法

1、排序

1.1 快速排序

主要思想:快速排序源于分治思想。
在这里插入图片描述
①确定区间:q[l]、q[(l+r)/2]、q[r]等(不一定选中间)
调整区间:使得x左边的值都小于等于x,x右边的值都大于等于x
③递归:递归处理x左右两端

快速排序属于分治算法,分治算法分为三步:
1、分成子问题
2、递归子问题
3、合并子问题

模板代码:

# include<iostream>//练习:快速排序、第k个数

using namespace std;

const int N=1e6+10;

int n;
int q[N];

void quick_sort(int q[],int l,int r)
{
	//递归终止情况
    if(l>=r) return;
    
    //第一步:分成若干子问题
    int x=q[(l+r)/2],i=l-1,j=r+1;   // 设置一个偏移量,让后面移动操作指向边界
    while(i<j)
    {
        do i++; while(x>q[i]);
        do j--; while(x<q[j]);
        if(i<j)  
            swap(q[i],q[j]);
    }
	
	//第二步:递归处理子问题
    quick_sort(q,l,j);
    quick_sort(q,j+1,r);
	
	//第三步:子问题合并,快速排序不需要这一步,但归并排序核心在于此步骤
}

int main()
{
	scanf("%d",&n);
	for(int i=0;i<n;i++)	
		scanf("%d",&q[i]);
		
	quick_sort(q,0,n-1);
	
	for(int i=0;i<n;i++)
		printf("%d ",q[i]);
		
	return 0;
}
#include<iostream>//786第k个数(快速选择排序)
using namespace std;

const int N=1e5+10;

int n,k;
int q[N];

int quick_sort(int l,int r,int k)
{
    if(l>=r)    return q[l];//找到了就直接返回
    
    int x=q[l+r>>1],i=l-1,j=r+1;
    while(i<j)
    {
        do i++; while(x>q[i]);
        do j--; while(x<q[j]);
        if(i<j) swap(q[i],q[j]);
    }
    
    int sl=j-l+1;
    if(sl>=k)    return quick_sort(l,j,k);//如果第k个数在左半边,则递归左半边寻找
    else return  quick_sort(j+1,r,k-sl); //否则递归右半边
}

int main()
{
    cin>>n>>k;
    for(int i=0;i<n;i++)    cin>>q[i];
    
    cout<<quick_sort(0,n-1,k)<<endl;
    
    return 0;
}

1.2 归并排序

主要思想:快速排序也是源于分治思想。

①确定分界点mid=(l+r)/2
②递归排序left、right
归并——将两个有序序列合并为一个
在这里插入图片描述
模板代码:

# include<iostream>//练习:归并排序、逆序对的数量
using namespace std;

const int N =1000010;

int n;
int q[N],tmp[N];	//tmp[N]临时归并后的数组

//归并算法属于分治算法,有三个步骤
void merge_sort(int q[],int l,int r)
{
	if(l>=r) return;
	
	//1、划分为子问题
	int mid=l+r>>1;
	
	//2、递归处理子问题
	merge_sort(q,l,mid),merge_sort(q,mid+1,r);
	
	//3、子问题合并
	int k=0,i=l,j=mid+1;	//k是temp[]的指针,i负责待排序前一半,j负责待排序的后一半

	while(i <= mid && j <= r)
        if(q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    while(i <= mid) tmp[k++] = q[i++];
    while(j <= r) tmp[k++] = q[j++];

    for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];//将排序好的temp[]存回q[]
}

int main()
{
	scanf("%d",&n);
	for(int i=0;i<n;i++) scanf("%d",&q[i]);

	merge_sort(q,0,n-1);
	
	for(int i=0;i<n;i++) printf("%d ",q[i]);
	
	return 0;
}
#include<iostream>//788 逆序对的数量
using namespace std;

typedef long long LL;
const int N=1e6+10;

int n;
int q[N],tmp[N];

LL merge_sort(int l,int r)
{
    if(l>=r)    return 0;
    
    int mid=l+r>>1;
    
    LL res=merge_sort(l,mid)+merge_sort(mid+1,r);//逆序对均在左边或者右边
    
    int k=0,i=l,j=mid+1;
    while(i<=mid && j<=r)
    {
        if(q[i]<=q[j])  tmp[k++]=q[i++];
        else    //逆序对一左一右
        {
            tmp[k++]=q[j++];
            res+=mid-i+1;//q[i]>q[j],此时[i,mid]的所有数与q[j]构成逆序对
        }
    }
    while(i<=mid)   tmp[k++]=q[i++];
    while(j<=r)     tmp[k++]=q[j++];
    
    for(k=0,i=l;i<=r;i++,k++)   q[i]=tmp[k];
    
    return res;
}

int main()
{
    cin>>n;
    for(int i=0;i<n;i++)    cin>>q[i];
    
    cout<<merge_sort(0,n-1);
    
    return 0;
}

2、二分

2.1 整数二分

算法思想:有单调性一定能二分,但没有也可能可二分。(本质是某种性质,找到左右边界点)
在这里插入图片描述
实例:
在这里插入图片描述

//数的范围
# include<iostream>

using namespace std;

const int N=1e6+10;

int n,m,q[N];

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<n;i++) scanf("%d",&q[i]);
	
	while(m--)
	{
		int x;
		scanf("%d",&x);
		
		int l=0,r=n-1;
		while(l<r)
		{
			int mid=l+r>>1;
			if(q[mid]>=x) 
				r=mid;
			else 
				l=mid+1;
		}
		if(q[l]!=x) cout<< "-1 -1" <<endl;
		else
		{
			cout<<l<<' ';
			
			int l=0;r=n-1;
			while(l<r)
			{
				int mid=l+r+1>>1;
				if(q[mid]<=x) 
					l=mid;
				else
					r=mid-1;
			}
			cout<<l<<endl;
		}
	}
}

2.2 浮点数二分

//练习:数的三次方根
#include<iostream>
using namespace std;

int main()
{
    double n;
    cin>>n;
    
    double l=-10000,r=10000;
    while(r-l>1e-8)
    {
        double mid=(l+r)/2;
        if(n<=mid*mid*mid)  r=mid;
        else    l=mid;
    }
    
    printf("%lf",l);
    return 0;
}

3、高精度整数

做法:
①存储:C++将大整数存入数组中(从低位起逐位存储)
②按小学算法从低位开始计算

3.1 高精度加法 A+B

#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B)
{
    //为了方便计算,让A中保存较长的数字, B中保存较短的数字
    if (A.size() < B.size()) return add(B, A);
    //保存结果的数组
    vector<int> C;
    //进位,开始时是0
    int t = 0;
    //依次计算每一位
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];//加上 A 的第 i 位上的数字
        if (i < B.size()) t += B[i];//加上 B 的第 i 位上的数字
        C.push_back(t % 10); //C 中放入结果
        t /= 10;//t 更新成进位
    }
    //最后如果进位上有数,放进结果数组
    if (t) C.push_back(t);
    return C;//返回结果
}

int main()
{
    string a, b;//以字符串形式保存输入的两个整数
    vector<int> A, B;//保存两个整数的数组
    cin >> a >> b;//接收输入
    for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');//倒序存储第一个数
    for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');//倒序存储第二个数
    auto C = add(A, B);//调用加和函数
    for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];//倒序输出C中的数字
    cout << endl;
    return 0;
}

3.2 高精度减法 A-B

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

bool cmp(vector<int> &A,vector<int> &B)//判断A>=B
{
	if(A.size()!=B.size())	return A.size()>B.size();
	for(int i=A.size()-1;i>=0;i--)
		if(A[i]!=B[i])
			return A[i]>B[i];
	return true;
}

vector<int> sub(vector<int> &A,vector<int> &B)
{
	vector<int> C;
	for(int i=0,t=0;i<A.size();i++)
	{
		t=A[i]-t;
		if(i<B.size())	t-=B[i];
		C.push_back((t+10)%10);
		
		if(t<0)	t=1;
		else 	t=0;
	}
	while(C.size()>1	&& C.back()==0)	C.pop_back();//去前导零
	return C;
}

int main()
{
	string a,b;
	vector<int> A,B;
	cin>>a>>b;
	
	
	for(int i=a.size()-1;i>=0;i--)	A.push_back(a[i]-'0');
	for(int i=b.size()-1;i>=0;i--)	B.push_back(b[i]-'0');
    
    if(cmp(A,B))
	{
	    auto C=sub(A,B);
	    for(int i=C.size()-1;i>=0;i--)	printf("%d",C[i]);
	}else{
	    auto C=sub(B,A);
	    printf("-");
	    for(int i=C.size()-1;i>=0;i--)	printf("%d",C[i]);
	}
	return 0;
}

3.3 高精度乘法 A×b

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

vector<int> mul(vector<int> &A,int b)
{
    vector<int> C;
    int t=0;
    for(int i=0;i<A.size() || t;i++)
    {
        if(i<A.size())  t+=A[i]*b;
        C.push_back(t%10);
        t/=10;
    }
    while(C.size()>1 && C.back()==0) C.pop_back();
    return C;
}

int main()
{
    string a;
    int b;
    cin>>a>>b;
    
    vector<int> A;
    for(int i=a.size()-1;i>=0;i--)  A.push_back(a[i]-'0');
    
    auto C=mul(A,b);
    for(int i=C.size()-1;i>=0;i--)  printf("%d",C[i]);
    return 0;
}

2.4 高精度除法 A/b

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

vector<int> div(vector<int> &A,int b,int &r)// A/b C为商,r为余数
{
    vector<int> C;
    r=0;
    for(int i=A.size()-1;i>=0;i--)
    {
        r=r*10+A[i];
        C.push_back(r/b);
        r%=b;
    }
    reverse(C.begin(),C.end());
    while(C.size()>1 && C.back()==0) C.pop_back();
    return C;
}

int main()
{
    string a;
    int b;
    
    cin>>a>>b;
    vector<int> A;
    for(int i=a.size()-1;i>=0;i--)  A.push_back(a[i]-'0');
    
    int r;
    auto C=div(A,b,r);
    
    for(int i=C.size()-1;i>=0;i--)  printf("%d",C[i]);
    cout<<endl<<r<<endl;
    return 0;
}

4、前缀和、差分

4.1 一维前缀和

在这里插入图片描述

在这里插入图片描述

#include<iostream>
using namespace std;

const int N=1e6+10;
int a[N],s[N];

int main()
{
    int n,m;//n队列长度,m求多少个前缀和
    cin>>n>>m;
    for(int i=1;i<=n;i++)   cin>>a[i];//下标从1开始
    
    for(int i=1;i<=n;i++)   
        s[i]+=s[i-1]+a[i];//预处理s[i]
        
    while(m--)
    {
        int l,r;
        cin>>l>>r;
        cout<<s[r]-s[l-1]<<endl;
    }
    
    return 0;
}

4.2 二维前缀和

如图:
在这里插入图片描述
紫色面积是指(1,1)左上角到(i,j-1)右下角的矩形面积, 绿色面积是指(1,1)左上角到(i-1, j )右下角的矩形面积。每一个颜色的矩形面积都代表了它所包围元素的和。
在这里插入图片描述
从图中我们很容易看出,整个外围蓝色矩形面积s[i][j] = 绿色面积s[i-1][j] + 紫色面积s[i][j-1] - 重复加的红色的面积s[i-1][j-1]+小方块的面积a[i][j];

因此得出二维前缀和预处理公式:
s[i] [j] = s[i-1][j] + s[i][j-1 ] + a[i] [j] - s[i-1][ j-1]

接下来回归问题去求以(x1,y1)为左上角和以(x2,y2)为右下角的矩阵的元素的和。

如图:
在这里插入图片描述
紫色面积是指 ( 1,1 )左上角到(x1-1,y2)右下角的矩形面积 ,黄色面积是指(1,1)左上角到(x2,y1-1)右下角的矩形面积;
不难推出:
在这里插入图片描述
绿色矩形的面积 = 整个外围面积s[x2, y2] - 黄色面积s[x2, y1 - 1] - 紫色面积s[x1 - 1, y2] + 重复减去的红色面积 s[x1 - 1, y1 - 1]

因此二维前缀和的结论为:

以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]

总结:
在这里插入图片描述

#include<iostream>

const int N=1010;

int n,m,q;//n行数,m列数,q质询数
int a[N][N],s[N][N];

int main()
{
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&a[i][j]);
    
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];//求前缀和
    while(q--)
    {
        int x1,y1,x2,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        printf("%d\n",s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);//算子矩阵的和    
    }
    return 0;
}

4.3 差分

如图:
在这里插入图片描述
我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

知道了差分数组有什么用呢? 别着急,慢慢往下看。

话说有这么一个问题:

给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c , a[r] + c;

暴力做法是for循环l到r区间,时间复杂度O(n),如果我们需要对原数组执行m次这样的操作,时间复杂度就会变成O(n*m)。有没有更高效的做法吗? 考虑差分做法。

始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i]的修改,会影响到a数组中从a[i]及往后的每一个数。

首先让差分b数组中的 b[l] + c ,a数组变成 a[l] + c ,a[l+1] + c, a[n] + c;

然后我们打个补丁,b[r+1] - c, a数组变成 a[r+1] - c,a[r+2] - c,a[n] - c;

为啥还要打个补丁?

我们画个图理解一下这个公式的由来:
在这里插入图片描述

b[l] + c,效果使得a数组中 a[l]及以后的数都加上了c(红色部分),但我们只要求l到r区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。

因此我们得出一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c。时间复杂度为O(1), 大大提高了效率。

总结:
在这里插入图片描述

#include<iostream>
using namespace std;

const int N=1e6+10;
int n,m;
int a[N],s[N];

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
        a[i]=s[i]-s[i-1];//构造差分数组
    }
    
    while(m--)
    {
        int l,r,c;
        cin>>l>>r>>c;
        a[l]+=c;//将序列中[l, r]之间的每个数都加上c
        a[r+1]-=c;
    }
    
    for(int i=1;i<=n;i++)
    {
        s[i]=a[i]+s[i-1];//前缀和运算
        cout<<s[i]<<" ";
    }
    
    return 0;
}

4.4 差分矩阵

如图:
在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], s[N][N];

int main()
{
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            cin>>s[i][j];//输入前缀和矩阵
            //前缀和公式:
            //s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];可推出得到差分矩阵
            a[i][j] = s[i][j] - s[i - 1][j] - s[i][j - 1] + s[i - 1][j - 1];
        }


    while (q -- )
    {
        int x1, y1, x2, y2, c;
        cin>>x1>>y1>>x2>>y2>>c;
        a[x1][y1] += c;//左上角+c
        a[x1][y2 + 1] -= c;
        a[x2 + 1][y1] -= c;
        a[x2 + 1][y2 + 1] += c;//右下角+c
    }

    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ )
        {
            s[i][j] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];//根据上面差分矩阵公式可得到
            cout<<s[i][j]<<" ";
        }
        cout<<endl;
    }

    return 0;
}

5、双指针算法

如图:
在这里插入图片描述

5.1、最长连续不重复子序列

核心思路:

1、遍历数组a中的每一个元素a[i], 对于每一个i,找到j使得双指针[j, i]维护的是以a[i]结尾的最长连续不重复子序列,长度为i - j + 1, 将这一长度与r的较大者更新给r。
2、对于每一个i,如何确定j的位置:由于[j, i - 1]是前一步得到的最长连续不重复子序列,所以如果[j, i]中有重复元素,一定是a[i],因此右移j直到a[i]不重复为止(由于[j, i - 1]已经是前一步的最优解,此时j只可能右移以剔除重复元素a[i],不可能左移增加元素,因此,j具有“单调性”、本题可用双指针降低复杂度)。
3、用数组s记录子序列a[j ~ i]中各元素出现次数,遍历过程中对于每一个i有四步操作:cin元素a[i] -> 将a[i]出现次数s[a[i]]加1 -> 若a[i]重复则右移j(s[a[j]]要减1) -> 确定j及更新当前长度i - j + 1给r。

#include<iostream>
using namespace std;

const int N=1e5+10;
int n;
int a[N],s[N];

int main()
{
    cin>>n;
    for(int i=0;i<n;i++)    cin>>a[i];
    
    int res=0;
    for(int i=0,j=0;i<n;i++)
    {
        s[a[i]]++;//s[N]记录每个数出现的次数
        while(s[a[i]]>1)
        {
            s[a[j]]--;//a[j]出现两次,先把出现次数-1
            j++;//从最大序列中移出a[j]
        }
        res=max(res,i-j+1);
    }
    cout<<res<<endl;
    return 0;
}

6、位运算

6.1 二进制中1的个数

#include<iostream>
using namespace std;

int lowbit(int x)
{
    return x&-x;//-x=~x+1;
}

int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int x;
        cin>>x;
        int res=0;
        while(x)    
        {
            x-=lowbit(x);//每次去掉x最后一个1和后面的数
            res++;
        }
        cout<<res<<" ";
    }
    
    return 0;
}

5.2、数组元素的目标和

双指针算法思路:
1、暴力方法
2、在暴力的基础上,利用单调性来优化

#include<iostream>
using namespace std;

const int N=1e5+10;
int n,m,q;
int a[N],b[N];

int main()
{
    cin>>n>>m>>q;
    for(int i=0;i<n;i++)    cin>>a[i];
    for(int i=0;i<m;i++)    cin>>b[i];
    
    for(int i=0,j=m-1;i<n;i++)//i、j指针起始分别指向a数组的头部和b数组的尾部
    {
        while(j>=0 && a[i]+b[j]>q)  j--;//j左移
        if(j>=0 && a[i]+b[j]==q)    cout<<i<<" "<<j;
    }
    
    return 0;
}

5.3、判断子序列

#include<iostream>
using namespace std;

const int N=1e5+10;
int n,m;
int a[N],b[N];

int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)    cin>>a[i];
    for(int i=0;i<m;i++)    cin>>b[i];
    
    int i=0;//i指针只有a[i]==b[j]的条件才后移
    for(int j=0;j<m;j++)//j指针遍历整个b数组
    {
        if(i<n && a[i]==b[j])   i++;
    }
    if(i==n)    cout<<"Yes";
    else        cout<<"No";
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值