算法笔记第四章总结

排序

选择排序

void selectSort(){
    for(int i=0;i<=n;i++){
        int k=i;
        for(int j=i;j<=nlj++){
            if(A[j]<A[K]){
                k=j;
            }
        }
        int temp=A[i];
        A[i]=A[k];
        A[k]=temp;
    }
}

插入排序

int A[maxn],n;
void insertSort(){
    for(int i=2;i<=n;i++){//进行n-1趟排序
        int temp=A[i],j=i;//temp临时存放A[i],j从i往前枚举
        while(j>1&&temp<A[j-1]){//只要temp小于前一个元素A[j-1]
            A[j]=A[j-1];//将A[j-1]后移一位
            j--;
        }
        A[j]=temp;//插入位置为j
    }
}

排序题目与sort函数应用

注意:strcmp函数返回值不一定是+1,-1,这与编译器有关,应该判断>0,<0。

分数相同排名一致且占用一个排位

stu[0].rank=1;//第一个排名记为1
for(int i=1,i<n;i++){
    if(stu[i].score==stu[i-1].score){
        stu[i].rank=stu[i-1].rank;
    }else{
        stu[i].rank=i+1;
    }
}

散列

散列的定义与整数散列

整数散列

#include<cstdio>
const int maxn=100010;
int hashTable[maxn]={0};
int main(){
    int n,m,x;
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++){
        scanf("%d",&x);
        hashTable[x]++;
    }
    for(int i=0;i<m;i++){
        scanf("%d",&x);
        printf("%d\n",hashTable[x]);
    }
    return 0;
}

散列

将元素通过一个函数转换为证书,使得该证书可以尽量唯一地代表这个元素,转换函数成为散列函数H,元素在转换前为key,转换后为H(key)。

常用的散列函数,上面整数散列使用直接定址法,恒等变换。

线性变换:H(key)=a*key+b;

平方取中法:key的平方的中间若干位作为hash值,很少用;

除留余数法:较为常用,H(key)=key%mod。表长TSize必须不小于mod,不然会产生越界。

取TSize为一个素数,mod直接取成与TSize相等。

解决冲突的方法
  1. 线性探查法

    不断+1,判断后一个是否已被占用。

  2. 平方探查法

    H(key)+1平方;H(key)-1平方;H(key)+2平方;H(key)-2平方;按照这个顺序。如果超出表长,则对表长取模。为避免负数的麻烦,也可以只进行正向的平方取模。

  3. 链地址法

    哈希值相同的连成链表。

字符串hash初步

A-Z转换为十进制,26-10
int hashFunc(char S[],int len){
    int id=0;
    for(int i=0;i<len;i++){
        id=id*26+(S[i]-'A');
    }
    return id;
}
A-Za-z转化为10进制,52-10
int hashFunc(char S[],int len){
    int id=0;
    for(int i=0;i<len;i++){
        if(S[i]>='A'&&S[i]<='Z'){
            id=id*52+(S[i]-'A');
        }else if(S[i]>='a'&&S[i]<='z'){
            id=id*52+(S[i]-'a')+26;//后面放小写字母
        }
    }
    return id;
}
如果出现了数字:
  • 增大进制数到62;
  • 如果保证末尾为确定个数的数字,即数字的个数和位置确定,可以分开转换然后拼接。
int hashFunc(char S[],int len){
    int id=0;
    for(int i=0;i<len-1;i++){
        id=id*26+S[i]-'A';
    }
    id=id*10+S[len-1]-'0';//最后一位为数字
    return id;
}
问题

N个字符串,由恰好三位大写字母组成,在给出M个查询字符串,问每个查询字符串在N个字符串中出现的次数。

#include <cstdio>
const int manx=100;
char S[maxn][5],temp[5];
int hashTable[26*26*26+10];
int hashFunc(char S[],int len){//hash函数,将S转化为整数
    int id=0;
    for(int i=0;i<len;i++){
        id=id*26+(S[i]-'A');
    }
    return id;
}
int main(){
    //接收M,N输入
    int n,m;
    scanf("%d%d",&n,&m);
    //接收N个字符串,储存在二维数组S中
    for(int i=0;i<n;i++){
        scanf("%s",S[i]);
        int id=hashFunc(S[i],3);//将字符串转为整数
        hashTable[id]++;//哈希表中,该字符串出现次数加一
    }
    for(int i=0;i<m;i++){
        scanf("%s",temp);
        int id=hashFunc(temp,3);
        printf("%d\n",hashTable[id]);//输出该字符串的出现次数
    }
    return 0;
}

递归

分治

分治法的三个步骤

  1. 分解,分解成与原问题有相似或相同结构的子问题。子问题之间应该是相互独立、没有交叉的。
  2. 解决,递归解决所有子问题。
  3. 合并,子问题的解合并为原问题的解。

子问题个数为1的情况成为减治,子问题个数大于1的情况成为分治。

可以通过递归来实现,也可以通过非递归实现。

递归

递归来实现分治的思想。递归逻辑中的两个重要概念:递归边界,递归式(递归调用)

递归求阶乘
#include<stdio.h>
int F(int n){
    if(n==0)return 1;
    else return F(n-1)*n;
}
递归求斐波那契数列
#include<stdio.h>
int F(int n){
    if(n==0||n==1)return 1;
    else return F(n-1)+F(n-2);
}
递归求全排列

输出1-n这n个整数的全排列

#include<cstdio>
const int maxn=11;
//p为当前排列,hashTable记录整数X是否已经在P中
int n,P[manx],hashTable[maxn]={false};
//当前处理排列的第index号位
void generateP(int index){
    if(index==n+1){//递归边界,已经处理完排列的1~n位
        for(int i=1;i<=n;i++){//输出当前排列
            printf("%d",P[i]);
        }
        printf("\n");
        return;
    }
    for(int x=1;x<=n;;x++){//枚举1-n,试图将x填入P中
        P[index]=x;//另P的第index位为x,把x加入当前序列
        hashTable[x]=true;//记录x已经在P中
        generateP(index+1);//处理排列的第n+1号位
        hashTable[x]=false;//处理完P[index]为x的子问题之后,还原状态
    }
}
int main(){
    n=3;
    generateP(1);//从P[1]开始填
    return 0}
递归解决N皇后问题
int count=0;
void generateP(int index){
    if(index==n+1){//递归边界
        bool flag=true;//当前排列为一个合法方案
        for(int i=1;i<=n;i++){
            for(int j=i+1;j<=n;j++){
                if(abs(i-j)==abs(P[i]-P[j])){//两个点位于对角线
                    flag=false;//不合法
                }
            }
        }
        if(flag){
            count++;
        }
        return;
    }
    for(int x=1;x<=n;x++){
        if(hashTable[x]==false){
            P[index]=x;
            hashTable[x]=true;
            generateP(index+1);
            hashTable[x]=false;
        }
    }
}
回溯法解决N皇后问题

到达递归边界前的某层,由于某些事实已经不需要往任何一个子问题回归,就可以直接返回上一层。这种方法成为回溯法。

void generateP(int index){
    if(index==n+1){
        count++;
        return;
    }
    for(int x=1;x<=n;x++){
        if(hashTable[x]==false){
            bool flag=true;
            for(int pre=1;pre<index;pre++){//遍历之前的皇后
                //第index列皇后的行号为x,第pre列的皇后的行号为P[pre]
                if(abs(index-pre)==abs(x-P[pre])){
                    flag=false;//冲突了
                    break;
                }
            }
            if(flag){
                P[index]=x;
                hashTable[x]=true;
                generateP(index+1);
                hashTable[x]=false;
            }
        }
    }
}

贪心

简单贪心

贪心法

局部最优解,从而达到全局最优。需要严谨使用贪心法求阶最优化问题需要对采取的策略进行证明。证明方法一般为反证法和数学归纳法。如果想到某个似乎可行的策略,并且自己无法举出反例,那么就勇敢地实现它。

例子:月饼 B1020

#include<cstdio>
#include<algorithm>
using namespace std;
//月饼结构体,静态链表?
struct mooncake{
    double store;//库存
    double sell;//总售价
    double price;//单价
}cake[1010];
//排序,比较函数
bool cmp(mooncake a,mooncake b){
    return a.price>b.price;
}

int main(){
    int n;//n种月饼
    double D;//市场需求量D
    scanf("%d%lf",&n,&D);
    //接收输入库存
    for(int i=0;i<n;i++){
        scanf("%lf",&cake[i].store);
    }
    //接收输入总售价
    for(int i=0;i<n;i++){
        scanf("%lf",&cake[i].sell);
        //算出单价
        cake[i].price=cake[i].sell/cake[i].store;
    }
    //数据填充完整后
    //单价排序
    sort(cake,cake+n,cmp);//价格从高到低排序
    //计算收益
    double ans=0;
    //自高到低遍历每个品种
    for(int i=0;i<n;i++){
        if(cake[i].store<=D){//如果需求高于月饼库存
            D-=cake[i].store;//月饼全部卖出,需求减少
            ans+=cake[i].sell;//该月饼总售价全部变为收益
            cake[i].store=0;//该月饼库存改为0,此步骤不需要
        }else{//月饼的需求低于库存
            ans+=cake[i].price*D;//能赚到的收益
            cake[i].store-=D;
            break;
        }
    }
    printf("%.2f\n",ans);
    return 0;
}

例子:组个最小数 B1023

#include<cstdio>
int main(){
    int count[10];//记录10个数字的个数
    for(int i=0;i<10;i++){
        scanf("%d",&count[i]);
    }
    for(int i=1;i<10;i++){//从0-9种选出不为0的最小数字
        if(count[i]>0){
            printf("%d",i);
            count[i]--;
            break;
        }
    }
    for(int i=0;i<10;i++){
        for(int j=0;j<count[i];j++){
            printf("%d",i);
        }
    }
    return 0;
}

区间贪心

所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。

区间

给出N个开区间,选择尽可能多的开区间,使它们两两没有交集。

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=110;
struct Inteval{
    int x,y;//开区间左右端点
}I[maxn];
bool cmp(Inteval a,Inteval b){
    if(a.x!=b.x)return a.x>b.x;//左端点从大到小
    else return a.y<b.y;//右端点从小到大
}
int main(){
    int n;
    while(scanf("%d",&n),n!=0){
        for(int i=0;i<n;i++){
            scanf("%d%d",&I[i].x,&I[i].y);
        }
        sort(I,I+n,cmp);//区间排序
        //ans记录不相交区间个数,lastX记录上一个被选中区间的左端点
        int anx=1,lastX=I[0].x;
        for(int i=0;i<n;i++){
            if(I[i].y<=lastX){//该区间的右端点在lastX的左边
                lastX=I[i].x;//以I[i]作为新选中的区间
                ans++;//不相交区间个数加一
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

二分

二分查找

在严格递增序列中找到给定的数x

//left为二分下界,right为二分上届,x为想要查询的数字
#include<cstdio>
//区间[left,right],初始值为[1,n-1]
int binarySearch(int A[],int left,int right,int x){
    int mid;
    while(left<=right){
        mid=(left+right)/2;
        if(A[mid]==x)return mid;
        else if(A[mid]>x){
            right=mid-1;
        }else{
            left=mid+1;
        }
    }
    return -1;//查找失败,返回-1
}

mid=left+(right-left)/2可以避免溢出。

在严格递增序列中找到第一个大于等于x元素的位置

此时元素x存在的位置,存在区间是一个左闭右开的区间[left,right)

//初始值[left,right],传入0,n
int lower_bound(int A[],int left,int right,int x){
    int mid;
    while(left<right){//如果是left=right时,找到了唯一的位置
        mid=(left+right)/2;
        if(A[mid]>=x){//中间的数>=x
            right=mid;
        }else{
            left=mid+1;
        }
    }
    return left;
}

假设它存在,则它应该在的位置

序列中第一个大于x的元素的位置

//初始值[left,right],传入0,n
int upper_bound(int A[],int left,int right,int x){
    int mid;
    while(left<right){//如果是left=right时,找到了唯一的位置
        mid=(left+right)/2;
        if(A[mid]>x){//中间的数>=x
            right=mid;
        }else{
            left=mid+1;
        }
    }
    return left;
}

以上问题的解决模板

//解决“寻找有序序列第一个满足某条件的元素的位置的固定模板”
//二分区间为左闭右闭的[left,right],初始值必须是能够覆盖解的所有可能的值
int solve(int left,int right){
    int mid;
    while(left<right){//如果left=right,则找到了唯一位置
        mid=(left+right)/2;
        if(条件成立){//条件成立,则第一个满足条件的元素位置<=mid
            right=mid;//往左子区间查找
        }else{//条件不成立,则第一个满足该条件的元素>mid
            left=mid+1;//往右子区间查找
        }
    }
    return mid;//返回夹出来的位置
}

其他左开右闭的写法与之等价。

二分法拓展

计算根号2的值

const double eps=1e-5;
double f(double x){
    return x*x;
}
double calSqrt(){
    double left=1,right=2,mid;
    while(right=left>eps){
        mid=(left+right)/2;
        if(f(mid)>2){
            right=mid;
        }else{
            left=mid;
        }
    }
    return mid;
}

在闭区间上的单调函数f(x),求f(x)=0的根

const double eps=1e-5;
double f(double x){
    return ...;
}
double calSqrt(double L,double R){
    double left=L,right=R,mid;
    while(right=left>eps){
        mid=(left+right)/2;
        if(f(mid)>2){
            right=mid;
        }else{
            left=mid;
        }
    }
    return mid;
}

半圆形储水装置的装水问题

#include<cstdio>
#include<cmath>
const double PI=acos(-1.0)const double eps=1e-5;
double f(double R,double h){
    double alpha=2*acos((R-h)/R);
    double L=2*sqrt(R*R-(R-h)*(R-h));
    double S1=alpha*R*R/2-L*(R-h)/2;
    double S2=PI*R*R/2;
    return S1/S2;
}
double solve(double R,double r){
    double left=0,right=R,mid;
    while(right-left>eps){
        mid=(left+right)/2;
        if(f(R,mid)>r){
            right=mid;
        }else{
            left=mid;
        }
    }
    return mid;
}
int main(){
    double R,r;
    scanf("%lf%lf",&R,&r);
    printf("%.4f\n",solve(R,r));
    return 0;
}

其他问题略

快速幂

快速幂的递归写法

typedef long long LL;
LL binaryPow(LL a,LL,b,LL m){
    if(b==0){
        return 1;
    }
    if(b%2==1){
        return a*binaryPow(a,b-1,m)%m;
    }else{
        LL mul=binaryPow(a,b/2,m);
        return mul*mul%m;
    }
}

快速幂的迭代写法

typedef long long LL;
LL binaryPow(LL a,LL b,LL m){
    LL ans=1;
    while(b>0){
        if(b&1){//如果b的二进制末尾为1,奇数
            ans=ans*a%m;
        }
        a=a*a%m;
        b>>=1;//二进制右移一位,即b=b/2
    }
    return ans;
}

two pointers

什么是two pointers

序列求合相等问题

求一个递增序列中的两个位置的数,它们的和恰好等于M

while(i<j){
    if(a[i]+a[j]==m){
        printf("%d %d\n",i,j);
        i++;
        j--;
    }else if(a[i]+a[j]<m){
        i++;
    }else{
        j--;
    }
}

序列合并问题

两个递增序列A和B,要求合并为一个递增序列C

int merge(int A[],int B[],int n,int m){
    int i=0,j=0,index=0;
    while(i<n&&j<m){
        if(A[i]<=B[j]){
            C[index++]=A[i++];//如果A小,则把A的元素加进去
        }else{
            C[index++]=B[j++];
        }
    }
    while(i<n)C[index++]=A[i++];//将A剩余元素加入C中
    while(j<m)C[index++]=B[j++];
    return index;
}

归并排序

这里主要讲2路归并排序:将序列凉凉分组,将序列归并为n/2(向上取整)个组,组内单独排序,然后再将组两两归并,生成n/4(向上取整)个组,再组内单独排序,以此类推,直到剩下一个组未知。

递归实现

const int maxn=100;
int merge(int A[],int L1,int R1,int L2,int R2){
    int i=L1,j=L2;
    int temp[maxn],index=0;//临时数组
    while(i<=R1&&j<=R2){
        if(A[i]<=A[j]){
            temp[index++]=A[i++];//如果A小,则把A的元素加进去
        }else{
            temp[index++]=A[j++];
        }
    }
    while(i<R1)A[index++]=A[i++];//将A剩余元素加入C中
    while(j<R2)A[index++]=A[j++];
    for(i=0;i<index;i++){
        A[L1+i]=temp[i];//合并后的序列赋值给A
    }
}
//将array数组当前区间[left,right]进行归并
void mergeSort(int A[],int left,int right){
    if(left<right){
        int mid=(left+right)/2;
        mergeSort(A,left,mid);
        mergeSort(A,mid+1,right);
        merge(A,left,mid,mid+1,right);//左右子区间合并
    }
}

非递归实现

如果只要求给出归并排序每一趟结束时的序列,那么可以使用sort函数代替merge函数,时间允许的情况下。

void mergeSort(int A[]){
    //step为组内元素个数,step/2为左子区间的元素个数,注意等号可以不取
    for(int step=2;step/2<=n;step*=2){
        //每step个元素一组,组内前step/2和组内后step/2个元素进行合并
        for(int i=1;i<=n;i+=step){
            int mid=i+step/2-1;//左子区间元素个数为step/2
            if(mid+1<=n){//右子区间存在元素即合并
                merge(A,i,mid,mid+1,min(i+step-1,n));
            }
        }
    }
}

快速排序

划分区间
//对区间[left,right]进行划分
int Partition(int A[],int left,int right){
    int temp=A[left];//将A[left]保存至临时变量
    while(left<right){//left和right不相遇
        while(left<right&&A[right]>temp){
            right--;//反复左移right
        }
        A[left]=A[right];
        while(left<right&&A[left]<=temp){
            left++;
        }
        A[right]=A[left];
    }
    A[left]=temp;//把temp放在相遇的位置
    return left;//返回相遇的下标
}
快速排序过程
  1. 调整序列中的元素,使当前序列最左端的元素在调整后满足左侧所有元素均不超过该元素,右侧所有元素均大于该元素。
  2. 对该元素的左侧和右侧分别递归进行1的调整,知道当前调整区间的长度不超过1。
快速排序实现
//left和right初始值为序列首尾下标
void quickSort(int A[],int left,int right){
    if(left<right){
        //将[left,right]按照A[left]一分为二
        int pos=Partition(A,left,right);
        quickSort(A,left,pos-1);
        quickSort(A,pos+1,right);
    }
}

生成随机数

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main(){
    srand((unsigned)time(NULL));
    for(int i=0;i<10;i++){
        printf("%d ",rand());
        printf("%d",rand()%2);//[0,1]
        printf("%d",rand()%5+3);//[3,7]
        printf("%d ",(int)(round(1.0*rand()/RAND_MAX*50000+10000));
    }
    return 0;
}
随机主元划分
//对区间[left,right]进行划分
int randPartition(int A[],int left,int right){
    //生成[left,right]内的随机数p
    int p=(round(1.0*rand()/RAND_MAX*(right-left)+left);
    swap(A[p],A[left]);//交换A[p],A[left]
    //以下是原先的划分过程,不需要改变
    int temp=A[left];//将A[left]保存至临时变量
    while(left<right){//left和right不相遇
        while(left<right&&A[right]>temp){
            right--;//反复左移right
        }
        A[left]=A[right];
        while(left<right&&A[left]<=temp){
            left++;
        }
        A[right]=A[left];
    }
    A[left]=temp;//把temp放在相遇的位置
    return left;//返回相遇的下标
}

其他

打表

活用递推

PAT B1040

#include<cstdio>
#include<cstring>
const int MAXN=100010;
const int MOD=1000000007;
char str[MAXN];//字符串
int leftNumP[MAXN]={0};//每一位左边含P的个数
int main(){
    gets(str);//读入字符串
    //遍历填充左边P的个数
    int len=strlen(str);//长度
    for(int i=0;i<len;i++){//遍历字符串
        if(i>0){//除了0号位以外,先继承上一位的结果
            leftNumP[i]=leftNumP[i-1];
        }
        if(str[i]=='P'){
            leftNumP[i]++;//当前位是P,则加一
        }
    }
    int ans=0,rightNumT=0;
    for(int i=len-1;i>=0;i--){//遍历获取右边T个数以及计算结果
        if(str[i]=='T'){
            rightNumT++;
        }else if(str[i]=='A'){
            ans=(ans+leftNumP[i]*rightNumT)%MOD;//直接算出结果
        }
    }
    printf("%d\n",ans);
    return 0;
}

随机选择算法

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值