《算法笔记》学习 入门篇

目录

《算法笔记》学习

3.1 简单模拟

题目怎么说就怎么做

代码能力

例1:【PAT B1001】害死人不偿命的(3n+1)猜想

卡拉兹(Callatz)猜想:

对任何一个正整数 n,如果它是偶数,那么把它砍掉一半;如果它是奇数,那么把 (3n+1) 砍掉一半。这样一直反复砍下去,最后一定在某一步得到 n=1。

我们今天的题目不是证明卡拉兹猜想,而是对给定的任一不超过 1000 的正整数 n,简单地数一下,需要多少步(砍几下)才能得到 n=1?

输入格式:每个测试输入包含 1 个测试用例,即给出正整数 n 的值。

3

输出格式:输出从 n 计算到 1 需要的步数。

5

解题思路

n=3 --> n=(3*3+1)/2=5; --> 8 --> 4 --> 2 --> 1

输入一个正整数 n ,用 m 记录步数。

AC代码

#include <cstdio>
using namespace std;

int main(){
	int n,m=0;
	scanf("%d",&n);
	while(n!=1){
	  if(n%2==0){//偶数
	    n /= 2;
	    m++;
	  }
	  else{//奇数
	    n = (3*n+1)/2;
	    m++;
	  }
	}
	printf("%d",m);
	return 0;
}
例2:【PAT B1032】挖掘机技术哪家强

为了用事实说明挖掘机技术到底哪家强,PAT 组织了一场挖掘机技能大赛。现请你根据比赛结果统计出技术最强的那个学校。
输入格式:输入在第 1 行给出不超过 10^5 的正整数 N,即参赛人数。随后 N 行,每行给出一位参赛者的信息和成绩,包括其所代表的学校的编号(从 1 开始连续编号)、及其比赛成绩(百分制),中间以空格分隔。

6
3 65
2 80
1 100
2 70
3 40
3 0

输出格式:在一行中给出总得分最高的学校的编号、及其总分,中间以空格分隔。题目保证答案唯一,没有并列。

2 150

多行输入数组

解题思路

输入 n ,根据 n 进入循环,输入(编号i,分数a),score[i]+=a;

比较score[i]大小,找到最大值。

AC代码

#include <cstdio>
using namespace std;

int main(){
	int n,i=0,a=0;

	scanf("%d",&n);
	int score[n+1] = {0};
	
	for(int j=0;j<n;j++){
	  	scanf("%d %d",&i,&a);
	  	score[i] += a;
	}
	int max=-1,id=0;
	for(int k=1;k<=n;k++){
		if(score[k]>max){
			max = score[k];
			id = k;
		}
	}
	printf("%d %d",id,max);
	return 0;
}

3.2 查找元素

小范围查找——遍历;大范围查找——算法。

例:【codeup 1934B】找x

输入一个数n,然后输入n个数值各不相同,再输入一个值x,输出这个值在这个数组中的下标(从0开始,若不在数组中则输出-1)。

输入:测试数据有多组,输入n(1<=n<=200),接着输入n个数,然后输入x。

4
1 2 3 4
3

输出:对于每组输入,请输出结果。

2

解题思路

遍历。

AC代码

#include <cstdio>
using namespace std;
const int MAX = 210;
int A[MAX];

int main(){
	int n,x;
	while(scanf("%d",&n)!=EOF){//一直输到文件末尾
		for(int i=0;i<n;i++){
			scanf("%d",&A[i]);
		}
		scanf("%d",&x);
		
		int flag = false;
		for(int j=0;j<n;j++){
			if(A[j]==x){
				printf("%d\n",j);
				flag = true;
				break;
			}
		}
		
		if(!flag){
			printf("-1\n");
		}
	}
	return 0;
} 

3.3 图形输出

!弄清规则:

  1. 通过规律直接输出;
  2. 定义一个二维字符数组,通过规律填充,然后输出整个二维数组。
例:【PAT B1036】跟奥巴马一起编程

美国总统奥巴马不仅呼吁所有人都学习编程,甚至以身作则编写代码,成为美国历史上首位编写计算机代码的总统。2014 年底,为庆祝“计算机科学教育周”正式启动,奥巴马编写了很简单的计算机代码:在屏幕上画一个正方形。现在你也跟他一起画吧!
输入格式:输入在一行中给出正方形边长 N(3≤N≤20)和组成正方形边的某种字符 C,间隔一个空格。

10 a

输出格式:输出由给定字符 C 画出的正方形。但是注意到行间距比列间距大,所以为了让结果看上去更像正方形,我们输出的行数实际上是列数的 50%(四舍五入取整)。

aaaaaaaaaa
a        a
a        a
a        a
aaaaaaaaaa

解题思路

直接输出。

AC代码

#include <cstdio>
using namespace std;

int main(){
	int n;
	char c;
	scanf("%d %c",&n,&c);
	int row=n%2==0?n/2:(n+1)/2;
	int col=n;
	int i;
	for(i=0;i<col;i++){
		printf("%c",c);
	}
	printf("\n");
	for(i=0;i<row-2;i++){
		printf("%c",c);
		for(int j=0;j<col-2;j++){
			printf(" ");
		}
		printf("%c\n",c);
	}
	for(i=0;i<col;i++){
		printf("%c",c);
	}
	return 0;
} 
习题:画X

绘制一个X(用*号表示线),其中长、宽、对角线的长度(即可容纳的*号个数)均为同一个奇数n。

输入:n(3<=n<=99)

5

输出:X

*   *
 * *
  *
 * *
*   *

解题思路

二维数组。

AC代码

#include <cstdio>
#include <cstring>
using namespace std;
const int MAX = 100;
char A[MAX][MAX];

int main(){
	int n;
	scanf("%d",&n);
	int i,j;
	for(i=0;i<n;i++){
		if(i<(n+1)/2){
			for(j=0;j<n-i;j++){
				A[i][j] = ' ';
			}
		}
		else{
			for(j=0;j<i;j++){
				A[i][j] = ' ';
			}
		}
		A[i][i] = '*';
		A[i][n-1-i] = '*';
	}
	for(i=0;i<n;i++){
		puts(A[i]);
	}
	return 0;
}

3.4 日期处理

注意平年(365)闰年(366)、大小月的区别。

例:【codeup 1928A】日期差值

有两个日期,求两个日期之间的天数,如果两个日期是连续的我们规定他们之间的天数为两天。
输入:有多组数据,每组数据有两行,分别表示两个日期,形式为YYYYMMDD

20130101
20130105

输出:每组数据输出一行,即日期差值

5

解题思路

假设第一个日期早于第二个日期(否则交换);【都是 int 可直接比较,也方便后面 y,m,d 的拆分】

令日期一不断加1,直到日期一等于日期二。

具体处理:

  • 如果 当前天数d+1==当前月份m所拥有天数+1,则m+1,同时将d置为1;
  • 如果 m+1==13,则y+1,m置为1。

所需数据:

  • 每月天数:用二维数组 int month[13][2],用于存放每月天数。month[i][0]为平年,month[i][1]为闰年。

快速处理:先加年,根据平年闰年加365或366。

AC代码

#include <cstdio>
using namespace std;

int month[13][2] = {{0,0},{31,31},{28,29},{31,31},{30,30},{31,31},{30,30},{31,31},{31,31},{30,30},{31,31},{30,30},{31,31}};

//判断平年?闰年
bool isLeap(int year){
	return (year%4==0&&year%100!=0)||(year%400==0);
} 

int main(){
	int time1,y1,m1,d1;
	int time2,y2,m2,d2;
	while(scanf("%d\n%d",&time1,&time2) != EOF){
		if(time1>time2){
			int t = time1;
			time1 = time2;
			time2 = t;
		}//保证time1<time2
		
		//拆分
		y1 = time1/10000;
		m1 = time1%10000/100;
		d1 = time1%10000%100;
		y2 = time2/10000;
		m2 = time2%10000/100;
		d2 = time2%10000%100;
		
		int day=1;
		
		while(y1<y2||m1<m2||d1<d2){
			d1++;
			if(d1==month[m1][isLeap(y1)]+1){
				m1++;
				d1=1;
			}
			if(m1==13){
				y1++;
				m1=1;
			}
			day++;
		}
		printf("%d\n",day);//codeup需要输出换行
	}
	return 0;
}
习题:周几

给定一个日期day,求它是周几。

输入描述:第一行为给定的日期day(格式为YYYY-MM-DD,范围为1900-01-01<=day<=2199-12-31),数据保证一定合法。

2021-05-01

输出描述:输出一个整数,表示周几。其中周一到周六分别用1-6表示,周天用0表示。

6

解题思路

根据 2021-05-01 是周六推算。

AC代码

#include <cstdio>
using namespace std;

int month[13][2] = {{0,0},{31,31},{28,29},{31,31},{30,30},{31,31},{30,30},{31,31},{31,31},{30,30},{31,31},{30,30},{31,31}};

//判断平年?闰年
bool isLeap(int year){
	return (year%4==0&&year%100!=0)||(year%400==0);
} 

int main(){
	int y1,m1,d1,time1;
	scanf("%d-%d-%d",&y1,&m1,&d1);
	time1 = y1*10000+m1*100+d1;
	int y2=2021,m2=05,d2=01,week2=6;
	int time2 = 20210501;
	int n=0; 
	
	if(time1<time2){
		while(y1<y2||m1<m2||d1<d2){
			d1++;
			if(d1==month[m1][isLeap(y1)]+1){
				m1++;
				d1=1;
			}
			if(m1==13){
				y1++;
				m1=1;
			}
			n++;
		}
		printf("%d",(week2-n)%7+7);	
	}
	else{
		while(y2<y1||m2<m1||d2<d1){
			d2++;
			if(d2==month[m2][isLeap(y2)]+1){
				m2++;
				d2=1;
			}
			if(m2==13){
				y2++;
				m2=1;
			}
			n++;
		}
		printf("%d",(week2+n)%7);
	}
	return 0;
}

3.5 进制转换

将 P 进制转换成 Q 进制:

  1. P 进制 x = a 1 a 2 . . . a n x=a_{1}a_{2}...a_{n} x=a1a2...an 转换成十进制 y y y

    y = a 1 ∗ P n − 1 + a 2 ∗ P n − 2 + . . . + a n − 1 ∗ P + a n y=a_{1}*P^{n-1}+a_{2}*P^{n-2}+...+a_{n-1}*P+a_{n} y=a1Pn1+a2Pn2+...+an1P+an

    int y=0,product=1;
    while(x!=0){
      y += product*(x%10);//(x%10)获取每位数。从a_{n}开始取
      x /= 10;//去掉已取的个位;
      product *= P;
    }
    
  2. 十进制 $ y$ 转换成 Q 进制 z z z —— “除基取余法”。

    int z[40],num=0;
    do{
      z[num++] = y%Q;//除基取余,z[0]=0
      y /= Q;
    }while(y!=0);
    
例:【PAT B1022】D进制的A+B

输入两个非负 10 进制整数 A 和 B ( ≤ 2 30 − 1 \leq 2^{30}-1 2301),输出 A+B 的 D (1<D≤10)进制数。

输入格式:输入在一行中依次给出 3 个整数 A、B 和 D。

123 456 8

输出格式:输出 A+B 的 D 进制数。

1103

解题思路

十进制转换成 D 进制。

AC代码

#include <cstdio>
using namespace std;

void tenToD(int d,int c,int n){
	int z[n],num=0;
	do{
		z[num++] = c%d;
		c /= d;
	}while(c!=0);
	for(int i=num-1;i>=0;i--){
		printf("%d",z[i]);
	}
}

int main(){
	int a,b,sum=0,d;
	scanf("%d %d %d",&a,&b,&d);
	sum = a+b;
	tenToD(d,sum,31);
	return 0;
}
习题:十进制转K进制

给定一个十进制数,输出它的 K 进制形式。

输入描述:一个非负整数n(0<=n<=1024)和一个正整数K(2<=K<=16)。

45 16

输出描述:输出一行,表示n的K进制。其中超过9的位使用大写英文字母表示(10 => A、11 => B、12 => C、13 => D、14 => E、15 => F)。

2D

解题思路

升级,有ABCDEF。printf("%c",z[i]-10+'A');

AC代码

/*
 *title:sunnywhy3.5.3
 *description:十进制转K进制 
 *time:2023-01-06
*/

#include <cstdio>
using namespace std;

void tenToD(int d,int c,int n){
	int z[n];
    int num=0;
	do{
		z[num++] = c%d;
		c /= d;
	}while(c!=0);
	for(int i=num-1;i>=0;i--){
		if(z[i]<10){
			printf("%d",z[i]);
		}
		else{
			printf("%c",z[i]-10+'A');//ABCD 
		}
	}
}

int main(){
	int a,d;
	scanf("%d %d",&a,&d);
	tenToD(d,a,31);
	return 0;
}

3.6 字符串处理

例1:【codeup 5901I】回文串

读入一串字符,判断是否是回文串。“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。

输入:一行字符串,长度不超过255。

12321

输出:如果是回文串,输出“YES”,否则输出“NO”。

YES

解题思路

str[i]==str[len-1-i]

AC代码

#include <cstdio>
#include <cstring>
using namespace std;
const int MAX=256;
char a[MAX];

int main(){
	int k=0;
	scanf("%s",a);
	int len = strlen(a);
	for(int i=0;i<len/2;i++){
		if(a[i]==a[len-i-1]) k++;
		else{
			printf("NO\n");
			break;
		}
	}
	if(k==len/2)
		printf("YES\n");
	return 0;
}
例2:【PAT B1009】说反话

给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。

输入格式:测试输入包含一个测试用例,在一行内给出总长度不超过 80 的字符串。字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区分)组成的字符串,单词之间用 1 个空格分开,输入保证句子末尾没有多余的空格。

Hello World Here I Come

输出格式:每个测试用例的输出占一行,输出倒序后的句子。

Come I Here World Hello

解题思路

使用gets函数读入一整行,以空格为分隔符对单词进行划分,并按序放到二维字符数组中,最后按单词逆序输出。

!PAT不支持gets,需用cin.getline(str,字符数组大小)代替。

#include <iostream>
using namespace std;

cin.getline(str,字符数组大小);//代替gets(str),可读取一行

cin.getline(str,字符数组大小)单词分割二维字符数组

AC代码

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int MAX=88;

int main(){
	char str[MAX];
	cin.getline(str,MAX);
	char words[MAX][MAX];
	int len = strlen(str);
	int j=0,k=0;
	for(int i=0;i<len;i++){
		if(str[i]!=' '){
			words[j][k++] = str[i];
		}
		else{
			words[j][k]='\0';
			j++;
			k=0;
		}
	}
	for(int i=j;i>0;i--){
		printf("%s ",words[i]);
	}
	printf("%s",words[0]);
	return 0;
}

4.1 排序

4.1.1 选择排序

简单选择排序

在待排序列 [1,n] 中找最小的那个,与第 i 个元素 a[i] 交换。

void selectSort(){
   for(int i=1;i<=n;i++){//重复n-1次
    //把第一个未排序元素设为最小值
    min = a[i];
    k = i;
    //遍历所有未排序元素a[i~n],找最小值
    for(int j=i;j<=n;j++){
      if(a[j]<min){
        min = a[j];
        k = j;
      }
    }
    //将最小值与第一个未排序位置交换
    a[k] = a[i];
    a[i] = min;
  }
}
算法分析

时间复杂度: O ( n 2 ) O(n^{2}) O(n2)


4.1.2 插入排序

简单插入排序

将未排序序列中的元素一次插入前面有序序列中。

void insertSort(){
  //第一个元素标记为已排序
  //对于所有未排序元素a[2~n]
  for(int i=2;i<=n;i++){
    //获取元素a[i]
    int temp=a[i],j=i;
    //若temp<前一个元素a[j-1],则a[j-1]后移
    while(j>1 && temp<a[j-1]){
      a[j] = a[j-1];
      j--;
    }
    //temp>=a[j-1],找到temp的位置a[j],插入
    a[j] = temp;
  }
}
算法分析

最好时间复杂度: O ( n ) O(n) O(n);最坏时间复杂度: O ( n 2 ) O(n^{2}) O(n2);平均时间复杂度: O ( n 2 ) O(n^{2}) O(n2)


4.1.3 排序题与 sort 函数应用

sort()
  1. 使用sort()排序

    //实现
    #include <algorithm>
    using namespace std;
    
    sort(首元素地址(*),尾元素地址的下一个地址(*),比较函数(非必填));
    //若无比较函数,默认对前面区间进行递增排序
    //char类型按字典序
    
  2. 实现比较函数cmp

    (1)基本数据类型数组排序

    #include <cstdio>
    #include <algorithm>
    using namespace std;
    
    bool cmp(int a,int b){
      return a>b;//当a>b时,把a放在b前面
    }
    
    int main(){
      int a[] = {3,1,4,2};
      sort(a,a+4,cmp);
      for(int i=0;i<4;i++){
        printf("%d",a[i]);//4,3,2,1
      }
      return 0;
    }
    

    (2)结构体数组排序

    #include <cstdio>
    #include <algorithm>
    using namespace std;
    
    struct node{
      int x,y;
    }ssd[10];
    
    //将ssd数组按x从大到小排序
    bool cmp(node a,node b){
      return a.x>b.x;
    }
    
    sort(ssd,ssd+3,cmp);
    

    (3)容器排序

    在 STL 标准容器中,只有 vector、string、deque 可以用 sort。

    //vector !!注意起始结束!!
    sort(vi.begin(),vi.end(),cmp);
    //string
    sort(s,s+n,cmp);
    
【PAT A1025】PAT Ranking

Programming Ability Test (PAT) is organized by the College of Computer Science and Technology of Zhejiang University. Each test is supposed to run simultaneously in several places, and the ranklists will be merged immediately after the test. Now it is your job to write a program to correctly merge all the ranklists and generate the final rank.

Input Specification:

Each input file contains one test case. For each case, the first line contains a positive number N (≤100), the number of test locations. Then N ranklists follow, each starts with a line containing a positive integer K (≤300), the number of testees, and then K lines containing the registration number (a 13-digit number) and the total score of each testee. All the numbers in a line are separated by a space.

2
5
1234567890001 95
1234567890005 100
1234567890003 95
1234567890002 77
1234567890004 85
4
1234567890013 65
1234567890011 25
1234567890014 100
1234567890012 85

Output Specification:

For each test case, first print in one line the total number of testees. Then print the final ranklist in the following format:

registration_number final_rank location_number local_rank
准考证号 最终排名 考场号 考场内排名

The locations are numbered from 1 to N. The output must be sorted in nondecreasing order of the final ranks. The testees with the same score must have the same rank, and the output must be sorted in nondecreasing order of their registration numbers.

9
1234567890005 1 1 1
1234567890014 1 2 1
1234567890001 3 1 2
1234567890003 3 1 2
1234567890004 5 1 4
1234567890012 5 2 2
1234567890002 7 1 5
1234567890013 8 2 3
1234567890011 9 2 4

解题思路

输入

考场数N
考场人数K1
准考证号 分数
...
考场人数Kn
准考证号 分数
...

定义一个学生结构体

struct Student{
  char uid[13];
  int loc_num;
  int score;
  int all_rank;
  int loc_rank;
}st[30010];

AC代码

/*
 *title:【PAT A1025】PAT Ranking
 *description:
 *time:2023-01-09
*/

#include <cstdio>
#include <iostream>
#include <cstring> 
#include <algorithm> 
using namespace std;

struct Student{
  char uid[13];
  int loc_num;
  int score;
  int all_rank;
  int loc_rank;
}st[30010];

bool cmp(Student a,Student b){
	if(a.score!=b.score)
		return a.score>b.score;//分数从高到低 
	else
		return strcmp(a.uid,b.uid)<0;//准考证号从小到大 
}

int main(){
	int n,k,num=0;
	scanf("%d",&n);
	
	for(int i=0;i<n;i++){
		scanf("%d",&k);
		for(int j=0;j<k;j++){
			scanf("%s %d",st[num].uid,&st[num].score);
			st[num].loc_num = i+1;
			num++;
		}
		sort(st+num-k,st+num,cmp);//loc
		st[num-k].loc_rank = 1;
		for(int j=num-k+1;j<num;j++){
			st[j].loc_rank = (st[j].score!=st[j-1].score)?j+k-num+1:st[j-1].loc_rank;
		}
	}
	
	sort(st,st+num,cmp);//all
	st[0].all_rank = 1;
	for(int i=1;i<num;i++){
		st[i].all_rank = (st[i].score!=st[i-1].score)?i+1:st[i-1].all_rank;
	}
	
	printf("%d\n",num);
	for(int i=0;i<num;i++){
		printf("%s %d %d %d\n",st[i].uid,st[i].all_rank,st[i].loc_num,st[i].loc_rank);
	}
	
	return 0;
}
习题:字符串降序排序

AC代码

!!string类型,引用<iostream>,输入输出cin>>cout<<

#include <cstdio>
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
const int MAX = 52;
string A[MAX];

bool cmp(string a,string b){
    return a>b;
}

int main(){
    int n;
//    scanf("%d",&n);
	cin>>n;
    for(int i=0;i<n;i++){
//        scanf("%s ",A[i].c_str());
		cin>>A[i];
    }
	
    sort(A,A+n,cmp);

    for(int i=0;i<n;i++){
//        printf("%s\n",A[i].c_str());
		cout<<A[i]<<endl;
    }
    return 0;
}

4.2 散列

一般用于对比两个数组的相同项。

4.2.1 散列的定义与整数散列

散列(hash)

将元素 k e y key key 通过一个函数 H H H 转换为整数,使该整数 H ( k e y ) H(key) H(key) 可以尽量唯一地代表这个元素。

整数散列

k e y key key 是整数的情况,常用散列函数

  1. 直接定址法, H ( k e y ) = k e y H(key)=key H(key)=key;【最常见最实用】
  2. 线性变换法, H ( k e y ) = a ∗ k e y + b H(key)=a*key+b H(key)=akey+b
  3. 平方取中法,取 k e y 2 key^{2} key2 中间若干位作为 h a s h hash hash 值;【少用】
  4. 除数取余法 H ( k e y ) = k e y % m o d H(key)=key\%mod H(key)=key%mod m o d mod mod 一般取素数且小于表长 T s i z e Tsize Tsize。【易产生冲突】

解决冲突的方法:

  1. 线性探测法,若产生冲突,则 H ( k e y ) + 1 H(key)+1 H(key)+1 h a s h hash hash 值不断加一);【易扎堆】
  2. 平方探查法,避免扎堆,按以下顺序检查表中位置: H ( k e y ) + 1 2 、 H ( k e y ) − 1 2 、 H ( k e y ) + 2 2 、 H ( k e y ) − 2 2 、 ⋯ H(key)+1^{2}、H(key)-1^{2}、H(key)+2^{2}、H(key)-2^{2}、\cdots H(key)+12H(key)12H(key)+22H(key)22
  3. 链地址法(拉链法),将所有 H ( k e y ) H(key) H(key) 相同的 k e y key key 连接成一条单链表。

其中方法一、二都计算了新的 h a s h hash hash 值,又称为开放定址法

一般,可使用标准库模板库中的 m a p map map 来直接使用 h a s h hash hash 的功能。


4.2.2 字符串 hash 初步

α \alpha α 假设字符串由大写字母 A~Z 组成,将 A~Z 对应 0~25,将 26 个大写字母对应到二十六进制中。

可知,将二十六进制转换成十进制是唯一的。可通过进制转换,将字符串映射成整数。(最大整数为 2 6 l e n − 1 26^{len}-1 26len1,所以字符串长度不要太长)

int hashFunc(char s[],int len){
  int id=0;
  for(int i=0;i<len;i++){
    id = id*26 + (s[i]-'A');//将二十六进制转换成十进制,字符串从左到右是低位到高位
  }
  return id;
}

β \beta β 假设字符串由小写字母 a~z 和大写字母 A~Z 组成,将 A~Z 对应 025,az 对应 26~51 ⇒ \Rightarrow 将五十二进制转化成十进制

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;
}

γ \gamma γ 假设出现了数字:

  1. 按照处理字母的方法,五十二进制 ⇒ \Rightarrow 六十二进制;

  2. 如果保证字符串末尾是确定个数的数字,则可把前面字母进制转换成整数,再将末尾数字拼接上去。

    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 N N 个字符串(由恰好三位大写字母组成),再给出 M M M 个查询字符串,问:每个查询字符串在 N N N 个字符串中出现的次数。

#include <cstdio>
#include <iostream>
using namespace std;
const int MAX = 100;
char S[MAX][5],temp[5];
int hashTable[26*26*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;
} 

int main(){
	int n,m;
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>S[i];
		int id = hashFunc(S[i],3);
		hashTable[id]++;
	}
	for(int j=0;j<m;j++){
		cin>>temp;
		int id = hashFunc(temp,3);
		cout<<hashTable[id]<<endl;
	}
	return 0;
}

4.3 递归

4.3.1 分治(divide and conquer)

分治法

“分而治之”。将原问题划分成若干个规模较小且结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到原问题的解。

子问题数为 1 称为减治,大于 1 则为分治。

分治法步骤
  1. 分解:将原问题划分成若干个规模较小且结构与原问题相同或相似的子问题;
  2. 解决:递归求解所有子问题。若子问题的规模小到可以直接解决,就直接解决;(可递归也可非递归)
  3. 合并:合并子问题的解得到原问题的解。

**注:**子问题之间应该相互独立、没有交叉。


4.3.2 递归

递归中的重要概念

  1. 递归边界;
  2. 递归式(递归调用)。
【例1】使用递归求解 n ! n! n!
#include <iostream>
using namespace std;
int F(int n){
  if(n==0)
    return 1;
  else
    return F(n-1)*n;
}
int main(){
  int n;
  cin>>n;
  cout<<F(n)<<endl;
  return 0;
}
【例2】求 F i b o n a c c i Fibonacci Fibonacci 数列的第 n n n 项。
#include <iostream>
using namespace std;
int F(int n){
  if(n==0||n==1)
    return 1;
  else
    return F(n-1)+F(n-2);
}
int main(){
  int n;
  cin>>n;
  cout<<F(n)<<endl;
  return 0;
}
【例3】全排列( A ! A! A! 排列)

例如:1、2、3 的全排列是(1,2,3)、(1,3,2)、(2,1,3)、(2,3,1)、(3,1,2)、(3,2,1)。

解题思路

问题:“输出 1~n 这 n 个整数的全排列”

⇒ \Rightarrow 子问题:“输出以 1 为开头的全排列”、“输出以 2 为开头的全排列”、… “输出以 n 为开头的全排列”。

设定数组 P,用于存放当前排列;设定散列数组 hashTable,其中 hashTable[x] 当整数 x 已经在数组 P 中时为 true。

代码

#include <iostream>
using namespace std;
const int MAX = 11;
int n,p[MAX],hashTable[MAX]={false};

void generateP(int index){//index指在P中的位置
  for(index == n+1){//递归边界,说明已完成1~n位全排列
    for(int i=1;i<=n;i++){
      cout<<P[i];
    }
    cout<<"\n";
    return;
  }
  for(int x=1;x<=n;x++){//枚举1~n,试图将x填入P[index]
    if(!hashTable[x]){//x不在P[0]~P[index-1]中
      P[index] = x;//将x加入当前排列
      hashTable[x] = true;
      generateP(index+1);//从前往后排
      hashTable[x] = false;//已处理完P[index]为x的子问题,还原状态
    }
  }
}

int main(){
  n = 3;
  generateP(1);//从P[1]开始填
  return 0;
}
【例4】 n n n 皇后问题

在 n*n 的国际象棋棋盘上放置 n 个皇后,使这 n 个皇后两两均在不同行、不同列、不同对角线上,求合法的方案数。

解决思路

将棋盘每一列皇后位置(行号)取出,用全排列思路排列 1~n,即可得到均在不同行、不同列的一个皇后位置全排列。再根据两两皇后位置是否在同一对角线上判断该排列方案是否合法。

在判断方案是否合法的过程中,一旦出现非法排列,则放弃当前排列,直接返回上层,则需回溯

**回溯法:**一般来说,如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层。

void generateP(int index){
  if(index == n+1){
    count++;//所有位都合法
    return;
  }
  
  for(int x=1;x<=n;x++){//x行
    if(!hashTable[x]){//第x行还没有皇后
      bool flag = true;//当前皇后没有与之前皇后冲突
      for(int pre=1;pre<index;pre++){//遍历之前的皇后
        //第index列皇后行号为x,第pre列皇后行号为P[pre]
        if(abs(index-pre)==abs(x-P[pre])){//abs取绝对值
          flag = false;
          break;
        }
      }
      if(flag){//可放在第x行
        P[index] = x;//第index列皇后行号为x
        hashTable[x] = true;//x行有皇后了
        generateP(index+1);
        hashTable[x] = false;//递归完毕,还原第x行未被占用
      }
    }
  }
}
【习题】棋盘覆盖问题

分治法

每次都对分割后的四个小方块进行判断,判断特殊方格是否在里面。这里的判断的方法是每次先记录下整个大方块的左上角方格的行列坐标,然后再与特殊方格坐标进行比较,就可以知道特殊方格是否在该块中。

如果特殊方块在里面,这直接递归下去求即可;

如果不在,这根据分割的四个方块的不同位置,把右下角、左下角、右上角或者左上角的方格标记为特殊方块,然后继续递归。

在递归函数里,还要有一个变量 s 来记录边的方格数,每次对方块进行划分时,边的方格数都会减半,这个变量是为了方便判断特殊方格的位置。

AC代码

/*
 *title:sunnywhy4.3.11
 *description:棋盘覆盖问题
 *time:2023-01-13
*/

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAX = (256*256-1)/3;

struct Point{
	int x;
	int y;
	Point(){}
	Point(int _x,int _y){
		x = _x;
		y = _y;
	}
}P[MAX];

void pan(int ltr,int ltc,int cx,int cy,int size,int &i){
	if(size==1){
		return;
	}
	int n = size/2;
	
	if(cx<ltr+n&&cy<ltc+n){
		pan(ltr,ltc,cx,cy,n,i);
		P[i++] = Point(ltr+n,ltc+n);
	}else{
		pan(ltr,ltc,ltr+n-1,ltc+n-1,n,i);
	}
	if(cx<ltr+n&&cy>=ltc+n){
		pan(ltr,ltc+n,cx,cy,n,i);
		P[i++] = Point(ltr+n,ltc+n-1);
	}else{
		pan(ltr,ltc+n,ltr+n-1,ltc+n,n,i);
	}
	if(cx>=ltr+n&&cy<ltc+n){
		pan(ltr+n,ltc,cx,cy,n,i);
		P[i++] = Point(ltr+n-1,ltc+n);
	}else{
		pan(ltr+n,ltc,ltr+n,ltc+n-1,n,i);
	}
	if(cx>=ltr+n&&cy>=ltc+n){
		pan(ltr+n,ltc+n,cx,cy,n,i);
		P[i++] = Point(ltr+n-1,ltc+n-1);
	}else{
		pan(ltr+n,ltc+n,ltr+n,ltc+n,n,i);
	}
}

bool cmp(Point a,Point b){
	if(a.x!=b.x){
		return a.x<b.x;
	}
	return a.y<b.y;
}

int main(){
	int k,cx,cy;
	int i=0;
	cin>>k>>cx>>cy;
	pan(1,1,cx,cy,pow(2,k),i);
	sort(P,P+i,cmp);
	for(int j=0;j<i;j++){
		cout<<P[j].x<<" "<<P[j].y<<endl;
	}
	return 0;
}

总结

  1. P[MAX] 数组大小,即 L 型骨牌数量: ( 4 k − 1 ) / 3 (4^{k}-1)/3 (4k1)/3,即 ( 所有方格个数 − 特殊方格个数 ) / 3 (所有方格个数-特殊方格个数)/3 (所有方格个数特殊方格个数)/3
  2. 递归过程中参数的设计。尤其是棋盘分块后需要根据相对(左上角)坐标来对特殊方格定位;【递归内有相对坐标时都可以用一个点定位】
  3. 可通过定义 Point 类和 Point(x,y),减轻赋值代码量。

4.4 贪心

贪心法,指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。常用于求解一类最优化问题

4.4.1 简单贪心

【PAT B1020】月饼

月饼是中国人在中秋佳节时吃的一种传统食品,不同地区有许多不同风味的月饼。现给定所有种类月饼的库存量、总售价、以及市场的最大需求量,请你计算可以获得的最大收益是多少。

注意:销售时允许取出一部分库存。

样例给出的情形是这样的:假如我们有 3 种月饼,其库存量分别为 18、15、10 万吨,总售价分别为 75、72、45 亿元。如果市场的最大需求量只有 20 万吨,那么我们最大收益策略应该是卖出全部 15 万吨第 2 种月饼、以及 5 万吨第 3 种月饼,获得 72 + 45/2 = 94.5(亿元)。

输入格式:每个输入包含一个测试用例。每个测试用例先给出一个不超过 1000 的正整数 N 表示月饼的种类数、以及不超过 500(以万吨为单位)的正整数 D 表示市场最大需求量。随后一行给出 N 个正数表示每种月饼的库存量(以万吨为单位);最后一行给出 N 个正数表示每种月饼的总售价(以亿元为单位)。数字间以空格分隔。

3 20
18 15 10
75 72 45

输出格式:对每组测试用例,在一行中输出最大收益,以亿元为单位并精确到小数点后 2 位

94.50

解决思路

求出每种月饼的单价,单价越高卖越多。

AC代码

/*
 *title:PAT B1020 月饼 
 *description:
 *time:2023-02-01
*/

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

struct MoonPie{
	double store;//为方便计算都设置为double型变量 
	double sale;
	double a_sale;
}mp[1010];

bool cmp(MoonPie a,MoonPie b){
	return a.a_sale>b.a_sale;
}

int main(){
	int n;
	double d;
	cin>>n>>d;
	for(int i=0;i<n;i++){
		cin>>mp[i].store;
	}
	for(int i=0;i<n;i++){
		cin>>mp[i].sale;
		mp[i].a_sale = mp[i].sale/mp[i].store;
	}
	sort(mp,mp+n,cmp);
//	double w=0;
//	int j=0;
//	while(w<d){                //循环太多导致段错误
//		w += mp[j++].store;
//	}
	double m=0;
//	for(int i=0;i<j-1;i++){
//		m += mp[i].sale;
//		d -= mp[i].store;
//	}
//	m += mp[j-1].a_sale*d;
	for(int i=0;i<n;i++){
		if(mp[i].store<=d){
			m += mp[i].sale;
			d -= mp[i].store;
		}
		else{
			m += mp[i].a_sale*d;
			break;
		}
	}
	cout<<fixed<<setprecision(2)<<m<<endl;
	return 0;
} 
【PAT B1023】组个最小数

给定数字 0-9 各若干个。你可以以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小(注意 0 不能做首位)。例如:给定两个 0,两个 1,三个 5,一个 8,我们得到的最小的数就是 10015558。

现给定数字,请编写程序输出能够组成的最小的数。

输入格式:输入在一行中给出 10 个非负整数,顺序表示我们拥有数字 0、数字 1、……数字 9 的个数。整数间用一个空格分隔。10 个数字的总个数不超过 50,且至少拥有 1 个非 0 的数字。

2 2 0 0 0 3 0 0 1 0

输出格式:在一行中输出能够组成的最小的数。

10015558

AC代码

/*
 *title:【PAT B1023】组个最小数
 *description:
 *time:2023-02-01
*/

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

int main(){
	int a[10];
	for(int i=0;i<10;i++){
		cin>>a[i];
	}
	for(int i=1;i<10;i++){
		if(a[i]!=0){
			cout<<i;
			a[i] -= 1;
			break;
		}
	}
	for(int i=0;i<10;i++){
		while(a[i]>0){
			cout<<i;
			a[i] -= 1;
		}
	}
	return 0;
}

4.4.2 区间贪心

区间不相交问题

给出 N 个开区间 (x, y),从中选择尽可能多的开区间,使得这些开区间两两没有交集。

两个区间之间存在两种情况:

  1. I 1 I_{1} I1 I 2 I_{2} I2 包含,此时选择 I 1 I_{1} I1 就有更多的空间容纳其它区间;
  2. 去掉包含区间之后,将剩余区间按左端点 x x x 从大到小排序,即 x 1 > x 2 > . . . > x n x_{1}>x_{2}>...>x_{n} x1>x2>...>xn,右端点只可能是 y 1 > y 2 > . . . > y n y_{1}>y_{2}>...>y_{n} y1>y2>...>yn(否则就包含了),可见 I 1 I_{1} I1 右侧部分一定不会与其它区间重合*(同理* I 4 I_{4} I4 左侧部分也是),所以选择 I 1 I_{1} I1(或 I 4 I_{4} I4。因此这种情况下,总是先选择左端点最大的区间*(或右端点最小)*。
区间不相交问题.png

参考代码

I[i].y<=lastX#include <iostream>
#include <algorithm>
using namespace std;
const int MAX = 110;

struct Inteval{
	int x,y;//左右端点 
}I[MAX];

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(cin>>n&&n!=0){
		for(int i=0;i<n;i++){
			cin>>I[i].x>>I[i].y;
		}
		sort(I,I+n,cmp);
		//用ans记录不相交区间个数,lastX记录上一个被选中区间的左端点
		int ans=1,lastX=I[0].x;
		for(int i=1;i<n;i++){
			if(I[i].y<=lastX){
				ans++;
				lastX = I[i].x;
			}
		}
		cout<<ans<<endl; 
	}
	return 0;
}
区间选点问题

给出 N 个区间 (x, y),求最少需要确定多少个点,才能使每个闭区间中都至少存在一个点。

区间选点问题.png

同理,两个区间之间存在两种情况:

  1. I 1 I_{1} I1 I 2 I_{2} I2 包含,只需要一个公共点;
  2. 将剩余区间按左端点 x x x 从大到小排序,右端点只可能是 y 1 > y 2 > . . . > y n y_{1}>y_{2}>...>y_{n} y1>y2>...>yn,选出的 I 1 I_{1} I1 左端点即可作为有相交部分闭区间的公共点。

因为是闭区间只需将I[i].y<=lastX改为I[i].y<lastX

区间选点问题实质上就是找不相交区间个数。

贪心算法的适合场景

贪心是用来解决一类最优化问题,并希望由局部最优策略来推出全局最优结果的算法思想。

贪心算法适用的问题一定满足最优子结构性质,即一个问题的最优解可以由它的子问题的最优解有效构造出来。


4.5 二分

4.5.1 二分查找

二分查找是基于有序序列的查找,时间复杂度为 O ( l o g n ) O(logn) O(logn)

严格有序条件下的二分查找

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J6Ei3tSB-1675660390298)(0e17c94977ad4392f2bd058fe07af17b.png)]

  1. A [ m i d ] = = x A[mid]==x A[mid]==x,查找成功;
  2. A [ m i d ] > x A[mid]>x A[mid]>x,x 在 mid 左边,在 [left, mid-1] 继续查找;
  3. A [ m i d ] < x A[mid]<x A[mid]<x,x 在 mid 右边,在 [mid+1, right] 继续查找;
  4. 循环结束条件:left>right。
//A[n]为严格递增序列,二分区间为[left,right]闭区间
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;
}

binarySearch(A,0,n-1,x);

严格递减序列则将A[mid]>x换成A[mid]<x

tip:若二分上界 right 超过 int 型数据范围的一半,当 x 在序列靠后位置时,mid=(left+right)/2left+right可能超过 int 而导致溢出,此时可使用mid=left+(right-left)/2代替,避免溢出。

非严格有序条件下的二分查找

若递增序列中元素可能重复,对于欲查询元素 x,求出序列中第一个大于等于 x 的元素位置 L 以及第一个大于 x 的元素位置 R,则 x 在序列中的存在区间就是 [L, R)。

  1. 求序列中第一个大于等于 x 的元素位置 L

    二分查找(非严格,L).png
    1. A [ m i d ] ≥ x A[mid]\geq x A[mid]x,则 L 应当在 mid 处或 mid 左侧,在 [left, mid] 区间继续查询;
    2. A [ m i d ] < x A[mid]<x A[mid]<x,则 L 在 mid 右侧,在 [mid+1,right] 区间继续查询;
    3. x x x 不在序列中,返回的则是假设序列中存在 x,x 所在的位置。
    //A[n]为严格递增序列,二分区间为[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){
          right = mid;
        }
        else{
          left = mid+1;
        }
      }
      return left;
    }
    

    tips:

    1. 循环条件left<right而不是left<=right,因为在严格序列中若 x 不存在,返回 -1;而在非严格序列中会返回假设 x 存在时,它所在的位置。当left==right时意味着找到了 x 在的唯一位置,即所返回结果。
    2. 考虑到 x 可能比序列中所有元素都要大,此时 x 所在位置为 A[n],即二分区间上界为 n,二分初始区间 [left, right]=[0,n]。
  2. 求序列中第一个大于 x 的元素位置 R

    二分查找(非严格,R).png
    1. A [ m i d ] > x A[mid]>x A[mid]>x,R 在 mid 处或 mid 左侧,在 [left, mid] 区间继续查询;
    2. A [ m i d ] ≤ x A[mid]\leq x A[mid]x,R 在 mid 右侧,在 [mid+1,right] 区间继续查询;
    //A[n]为严格递增序列,二分区间为[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){
          right = mid;
        }
        else{
          left = mid+1;
        }
      }
      return left;
    }
    

可见,lower_bound()upper_bound()都在寻找有序序列中第一个能满足某条件的元素位置

//[left, right]
int solve(int left, int right){
  int mid;
  while(left<right){//left==right意味着找到了唯一位置
    mid = (left+right)/2;
    if(某条件成立){
      right = mid;
    }
    else{
      left = mid+1;
    }
  }
  return left;
}
//(left, right]
int solve(int left, int right){
  int mid;
  while(left+1<right){//left==right意味着找到了唯一位置
    mid = (left+right)/2;
    if(某条件成立){
      right = mid;
    }
    else{
      left = mid;
    }
  }
  return right;
}
二分查找适用场景

如果目的是查找“序列中是否存在满足某条件的元素”,则可用二分查找解决。


4.5.2 二分法拓展

【例】计算 2 \sqrt{2} 2 的近似值。

以精确到 1 0 − 5 10^{-5} 105 为例。

已知,对于 f ( x ) = x 2 f(x)=x^{2} f(x)=x2,在 x ∈ [ 1 , 2 ] x\in[1,2] x[1,2] 时, f ( x ) f(x) f(x) x x x 增大而增大。

令 float 型 left 和 right 初值分别为 1 和 2,根据 f ( m i d ) f(mid) f(mid) 与 2 的大小来选择子区间。

  1. f ( m i d ) > 2 f(mid)>2 f(mid)>2,即 m i d > 2 mid>\sqrt{2} mid>2 ,在 [left, mid] 内继续逼近;
  2. f ( m i d ) < 2 f(mid)<2 f(mid)<2,即 m i d < 2 mid<\sqrt{2} mid<2 ,在 [mid, right] 内继续逼近;
  3. 结束条件: r i g h t − l e f t < 1 0 − 5 right-left<10^{-5} rightleft<105,mid 即为所求的近似值。
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;
}

⟹ \Longrightarrow 给定一个定义在 [L, R] 上的单调函数 f ( x ) f(x) f(x),求方程 f ( x ) = 0 f(x)=0 f(x)=0 的根。

const double eps=1e-5;
double f(double x){
  return ......
}
double calSqrt(){
  double left=L,right=R,mid;
  while(right-left>eps){
    mid = (left+right)/2;
    if(f(mid)>0) right=mid;
    else left=mid;
  }
  return mid;//即f(x)=0的根
}
【例】装水问题

有一个侧面看去是半圆的储水装置,该半圆的半径为 R,要求往里面装入高度为 h 的水,使其在侧面看去的面积 S 1 S_{1} S1 与半圆面积 S 2 S_{2} S2的比例恰好为 r,先给定 R 和 r,求高度 h。

装水问题.png

S 2 = π R 2 2 , r = S 1 S 2 S_{2}=\frac{\pi R^{2}}{2},r=\frac{S_{1}}{S_{2}} S2=2πR2,r=S2S1,已知 R R R r r r ,根据 S 1 = α π × S 2 − ( R − h ) × R 2 − ( R − h ) 2 S_{1}=\frac{\alpha}{\pi}\times S_{2}-(R-h)\times \sqrt{R^{2}-(R-h)^{2}} S1=πα×S2(Rh)×R2(Rh)2 ,其中 α = 2 × arccos ⁡ ( R − h R ) \alpha=2\times \arccos(\frac{R-h}{R}) α=2×arccos(RRh),将 r r r 当作函数 f ( h ) f(h) f(h),易知 h ∈ [ 0 , R ] h\in[0,R] h[0,R]。可在其范围 [ 0 , R ] [0,R] [0,R] 内对 h h h 进行二分。

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

const double PI=acos(-1.0);
const double eps=1e-5;

double f(double R,double h){
	double s2=PI*R*R/2;
	double alpha=2*acos((R-h)/R);
	double s1=alpha*R*R/2-(R-h)*sqrt(R*R-(R-h)*(R-h));
	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;
	cin>>R>>r;
	cout<<solve(R,r)<<endl;
	return 0;
}

4.5.3 快速幂

快速幂基于二分的思想,也成为二分幂。快速幂基于:

  1. 如果 b b b 为奇数,则有 a b = a ∗ a b − 1 a^{b}=a*a^{b-1} ab=aab1
  2. 如果 b b b 为偶数,则有 a b = a b / 2 ∗ a b / 2 a^{b}=a^{b/2}*a^{b/2} ab=ab/2ab/2

这样,在 l o g ( b ) log(b) log(b) 级别次转换后, b b b 可以变为 0,而 a 0 = 1 a^{0}=1 a0=1

例如:求 2 10 2^{10} 210

2 10 = 2 5 × 2 5 2^{10}=2^{5}\times 2^{5} 210=25×25

$2^{5}=2\times 2^{4}$;

	$2^{4}=2^{2}\times 2^{2}$;

		$2^{2}=2^{1}\times 2^{1}$;

			$2^{1}=2\times 2^{0}$;

				$2^{0}=1$。
快速幂的递归写法
//求a^b%m
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;
  }
}
//不太懂为什么每次取模和最后取模结果一样,但应该是怕超过longlong长度所以要取完再乘
LL binaryPow2(LL a, LL b){
  if(b==0) return 1;
  if(b%2==1) return a*binaryPow2(a,b-1);
  else{
    LL mul = binaryPow2(a,b/2);
    return mul*mul;
  }
}
cout<<binaryPow(3,16,7)<<endl;
cout<<binaryPow2(3,16)%7<<endl;
快速幂的迭代写法

a b a^{b} ab 来说,如果把 b b b 写成二进制,则可将 b b b ‘写成若干次幂之和。例如: 1 3 10 = 110 1 2 13_{10}=1101_{2} 1310=11012,则 13 = 2 0 + 2 2 + 2 3 = 1 + 4 + 8 13=2^{0}+2^{2}+2^{3}=1+4+8 13=20+22+23=1+4+8,所以 a 13 = a 1 + 4 + 8 = a 1 ∗ a 4 ∗ a 8 a^{13}=a^{1+4+8}=a^{1}*a^{4}*a^{8} a13=a1+4+8=a1a4a8

⟹ \Longrightarrow 可以将任意 a b a^{b} ab 表示成 a 2 k 、 … 、 a 4 、 a 2 、 a 1 a^{2^{k}}、…、a^{4}、a^{2}、a^{1} a2ka4a2a1 中若干项的乘积,其中,如果 b b b 的二进制 i i i 号位为 1,则 a 2 i a^{2^{i}} a2i 被选中。

计算 a b a^{b} ab 的大致思路 ⟹ \Longrightarrow i i i 从 0 到 k 枚举 b b b 二进制的每一位,如果当前位是1,则累积 a 2 i a^{2^{i}} a2i

因为 a 2 k 、 … 、 a 4 、 a 2 、 a 1 a^{2^{k}}、…、a^{4}、a^{2}、a^{1} a2ka4a2a1 中前一项总是等于后一项的平方,因此可以用一个 a a a 记录 a 2 k a^{2^{k}} a2k 的值。

b=13b&1ansa
1 a a a
11011 1 ∗ a = a 1*a=a 1a=a a 2 a^{2} a2
1100 a a a a 4 a^{4} a4
111 a ∗ a 4 = a 5 a*a^{4}=a^{5} aa4=a5 a 8 a^{8} a8
11 a 5 ∗ a 8 = a 13 a^{5}*a^{8}=a^{13} a5a8=a13
//求a^b%m
typedef long long LL;
LL binaryPow(LL a, LL b, LL m){
  LL ans=1;
  while(b>0){
    if(b&1){//b%2==1
      ans = ans*a%m;
    }
    a = a*a%m;
    b>>=1;//b右移1位,即b=b/2
  }
  return ans;
}

4.6 two pointers

4.6.1 two pointers 介绍

two pointers,利用问题本身与序列特性,使用两个下标 i、j 对序列进行扫描,以较低复杂度解决问题。

序列合并问题

将两个递增序列 A、B 合并为一个递增序列 C。

int merge(int A[],int B[],int C[],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++];
    }else{
      C[index++] = B[j++];
    }
  }
  while(i<n) C[index++] = A[i++];
  while(j<m) C[index++] = B[j++];
  return index;
}

4.6.2 归并排序

2-路归并排序

原理:分解(递归实现,主要是要将序列分成 n 个归并段)、归并(算法,合并归并段)

递归实现
二路归并排序.png
//将两个归并段[L1,R1]、[L2,R2]合并为有序段
void merge(int A[],int L1,int R1,int L2,int R2){
  int i=L1,j=L2,index=0;
  int temp[maxn];
  while(i<R1&&j<R2){
    if(A[i]<=A[j]){
      temp[index++] = A[i++];
    }else{
      temp[index++] = A[j++];
    }
  }
  while(i<=R1) temp[index++] = A[i++];
  while(j<=R2) temp[index++] = A[j++];
  for(int k=0;k<index;k++){
    A[L1+k]=temp[k];
  }
}
//将当前区间段[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);
  }
}
非递归实现

与递归相比,没有分解的动作

令步长 s t e p = 2 step=2 step=2,将数组中每 s t e p step step 个元素作为一组,对其内部进行归并排序;再令 s t e p × 2 step\times2 step×2,循环,直到 s t e p / 2 > n step/2>n step/2>n

void mergeSort(int A[]){
  for(int step=2;step/2<=n;step*=2){//step为组内元素个数,step/2为左子区间元素个数
    for(int i=0;i<n;i+=step){
      //每step元素一组,
      int mid=i+step/2;
      if(mid<n){//右子区间存在元素,合并
        int min=i+step>n?i+step:n-1;
        merge(A,i,mid,mid+1,min);
      }
    }
  }
}

如果只要求给出每一趟的归并序列,可用 sort() 代替 merge()

void mergeSort(int A[]){
  for(int step=2;step/2<=n;step*=2){//step为组内元素个数,step/2为左子区间元素个数
    for(int i=0;i<n;i+=step){
      int min=i+step>n?i+step:n-1;
      sort(A+i,A+min+1);
    }
    //此处可输出每一趟
  }
}
2-路归并算法分析

2-路归并排序的核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列

时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN),空间复杂度为 O ( N ) O(N) O(N),是一种稳定的排序方法。


4.6.3 快速排序

two pointers 思想的解决一次快速排序划分
  1. 将 A[1] 存至临时变量 temp,并令两个下标 left、right 分别指向序列首位(left=1,right=n);

    快排01.png
  2. while(A[right]>temp),只要条件为真,right 不断左移;直到 A[right]<=temp,将 A[right] 处的元素移到 left 处,A[left]=A[right];

    快排02.png
  3. while(A[left]<temp),只要条件为真,left 不断右移;直到 A[left]>=temp,将 A[left] 处的元素移到 right 处,A[right]=A[left];

    快排03.png
  4. 重复2、3直到 left==right,left 与 right 相遇,把 temp 放到相遇的地方。

    快排04.png
//对[left,right]进行划分
int Partition(int A[],int left,int right){
  int temp=A[left];
  while(left<right){
    while(left<right&&A[right]>temp) right--;//!!别忘了left<right!!
    A[left]=A[right];
    while(left<right&&A[left]<=temp) left++;//!!非严格情况下要<=,否则陷入死循环!!
    A[right]=A[left];
  }
  A[left]=temp;//!!要赋值回去!!
  return left;
}
完整快速排序
//递归实现
void quickSort(int A[],int left,int right){
  if(left<right){//当前区间长度大于1。!!if就行,不用while!!
    //按A[left]划分
    int pos = Partition(A,left,right);
    quickSort(A,left,pos-1);//pos位置已确定,就是A[left]
    quickSort(A,pos+1,right);
  }
}

快速排序算法在序列元素排列随机时效率最高,但元素接近有序时则会达到 O ( n 2 ) O(n^{2}) O(n2),主要原因是主元没有办法将当前区间划分为两个长度接近的子区间。

⟹ \Longrightarrow 随机选择主元。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//选择随机主元,对[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]);
  
  int temp=A[left];
  while(left<right){
    while(left<right&&A[right]>temp) right--;
    A[left]=A[right];
    while(left<right&&A[left]<temp) left++;
    A[right]=A[left];
  }
  A[left]=temp;
  return left;
}

4.7 其他高效技巧与算法

4.7.1 打表

将所有可能用到的结果事先计算出来,后面用到时即可直接查表获得。用空间换时间,技巧性强。

  1. 在程序中一次性计算所有需要用到的结果,之后的查询直接取这些结果;
  2. 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,这样程序A即可直接使用这些结果;
  3. 对于不会做的题,先暴力计算小范围数据结果,然后找规律。【数据范围大时可用】

4.7.2 活用递推

过程中可能存在递推关系,找到递推关系可降低时间复杂度。

【PAT B1040/A1093】有几个PAT

字符串 APPAPT 中包含了两个单词 PAT,其中第一个 PAT 是第 2 位§,第 4 位(A),第 6 位(T);第二个 PAT 是第 3 位§,第 4 位(A),第 6 位(T)。

现给定字符串,问一共可以形成多少个 PAT?

输入格式:输入只有一行,包含一个字符串,长度不超过105,只包含 P、A、T 三种字母。

APPAPT

输出格式:在一行中输出给定字符串中包含多少个 PAT。由于结果可能比较大,只输出对 1000000007 取余数的结果。

2

解题思路

  1. 找 A;
  2. 假如 A 左边有 a 个 P,右边有 b 个 T,则该 A 可以组成 a*b 个 PAT;
  3. 可设置数组 nump[] 计数每个位置左边有多少个 P,numt 计数右边有多少个 T。【递推技巧】

AC代码

/*
 *title:【PAT B1040/A1093】有几个PAT
 *description:
 *time:2023-02-05
*/

#include <iostream>
#include <string>
using namespace std;
const int maxn=100005;
const int mod=1000000007;

//递推法
int main(){
	string str;
	cin>>str;
	int nump[maxn];
	int numt=0;
	int len=str.length();
	nump[0]=0;
	for(int i=0;i<len;i++){
		if(i>0){
			nump[i]=nump[i-1];	
		}
		if(str[i]=='P'){
			nump[i]++;
		}
	}
	int sum=0;
	for(int i=len-1;i>=0;i--){
		if(str[i]=='T'){
			numt++;
		}
		else if(str[i]=='A'){
			sum = (sum+nump[i]*numt)%mod;//!!每次都要 %mod !! 
		}
	}
	cout<<sum;
	return 0;
} 


4.7.3 随机选择算法

从无序数组中求出第K大的数

根据前面的随机快速排序算法randPartition函数,执行一次之后,主元 A[p] 左边元素个数确定且都小于主元,则 A[p] 就是 A[left,right] 中第 p-left+1 大的数。令 M=p-left+1:

  1. K==M,主元 A[p] 就是第 K 大的数;
  2. K<M,第 K 大的数在 A[p] 左边;
  3. K>M,第 K 大的数在 A[p] 右边。
int randSelect(int A[],int left,int right,int K){
  if(left==right) return A[left];
  int p=randPartition(A,left,right);
  int M=p-left+1;
  if(K==M) return A[p];
  if(K<M) return randSelect(A,left,p-1,K);
  else return randSelect(A,p+1,right,K-M);
}
≈ \approx 2016年408统考算法题

n n n 个整数构成的集合 A A A 划分成两个不相交的子集 A 1 A_{1} A1 A 2 A_{2} A2,元素个数分别为 n 1 、 n 2 n_{1}、n_{2} n1n2,元素之和分别为 S 1 、 S 2 S_{1}、S_{2} S1S2,要使 ∣ n 1 − n 2 ∣ |n_{1}-n_{2}| n1n2 最小且 ∣ S 1 − S 2 ∣ |S_{1}-S_{2}| S1S2 最大,求 ∣ S 1 − S 2 ∣ |S_{1}-S_{2}| S1S2

⟹ \Longrightarrow 求集合中第 n / 2 n/2 n/2 大的数,根据这个数把集合分为两部分,其中 A 1 A_{1} A1 中的元素都不小于这个数, A 2 A_{2} A2 中的元素都大于这个数。

/*
 *title:eg 4.7.3
 *description:|n1-n2|最小,|S1-S2|最大 
 *time:2023-02-06
*/

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <algorithm>
#include <cmath>//用于round 
using namespace std;
const int maxn=100010;
int A[maxn],n;

//选择随机主元,对[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]);
  
  int temp=A[left];
  while(left<right){
    while(left<right&&A[right]>temp) right--;
    A[left]=A[right];
    while(left<right&&A[left]<temp) left++;
    A[right]=A[left];
  }
  A[left]=temp;
  return left;
}

void randSelect(int A[],int left,int right,int K){
  if(left==right) return;
  int p=randPartition(A,left,right);
  int M=p-left+1;
  if(K==M) return;
  if(K<M) randSelect(A,left,p-1,K);
  else randSelect(A,p+1,right,K-M);
}

int main(){
	srand((unsigned)time(NULL));//初始化随机数种子
	int sum=0,sum1=0;
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>A[i];
		sum+=A[i];
	} 
	randSelect(A,0,n-1,n/2);
	for(int i=0;i<n/2;i++){
		sum1+=A[i];
	}
	cout<<(sum-sum1)-sum1<<endl;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值