第2章 递归与分治思想
文章目录
一、递归
一定要有个递归的终止条件
1.1 例4 归并排序、快速排序
- 三种O(n^2)的排序:冒泡,选择,插入
- 三种不基于比较的排序:桶,基数,计数
- 归并排序,快速排序
1.桶排序:
排序过程:
(1)设置一个定量的数组当作空桶子;
(2)寻访序列,并且把记录一个一个放到对应的桶子去;
(3)对每个不是空的桶子进行排序。
(4)从不是空的桶子里把项目再放回原来的序列中。
对于重复出现的数字排序也可以在桶内类似于计数一样,对应数字的桶计数(桶是排序好的),然后按照桶逐个输出
2.计数排序:
可以看作是简易桶排序的基础上,用了前缀和。比如有有1~100排好序的一个数组,在1的位置记小于等于1的数有几个,2的位置记小于等于2的有几个,这样记好后,对于7马上就能知道排在7前面的有多少个数
3.基数排序:
对于数字较大的情况,此时开个相同大小的数组的开销比较大
按照个位桶排序后,再按照十位,百位桶排序
从上到下,从左往右的顺序
1.2 归并排序:
每次把待排序区间一分为二,将两个子区间排序,然后将两个已经排好序的序列合并
归并排序的复杂度为O(nlogn)
为什么?
归并排序的递归过程如下,该递归树的高度为log2n(计算过程:假设待排序的数组元素个数为n,设高度为x,x意味着n个元素需要连续二分x次才剩下1个元素,即n/2^x=1,x=log2n),每一层的总比较次数为n,所以时间复杂度为nlogn。
快速排序的分析过程类似。快速排序的平均时间复杂度同样为nlogn。假设在平均情况下每次选取的基准值均为该数组的中间值,因此每次都将数组分成两半,直到分割到只剩一个元素。假设n个元素平分了x次后只剩1个元素,则n/2^x=1,x=log2n。每次分割后的比较次数为n(参考上图),所以时间复杂度为nlogn。
原文链接:https://blog.csdn.net/weixin_38314865/article/details/113839571
看是不是O(n)简单的技巧是看是不是把元素只访问了一遍
代码实现:
#include<iostream>
using namespace std;
int n;
int a[1005];
//借助一个数组来存排序小的值
int b[1005];
void hebin(int l,int mid,int r)
{
//左边的第一个指针
int q=l;
//右边区域的第一个指针
int p=mid+1;
for (int i=l;i<=r;i++){
if ((p>r) || q<=mid&&a[q]<=a[p]){
b[i]=a[q];
q++;
}
else{
b[i]=a[p];
p++;
}
}
for (int j=l;j<=r;j++){
a[j]=b[j];
}
}
void sort_gui(int l,int r)
{
if (l==r){
return;
}
int mid=(l+r)/2;
sort_gui(l,mid);
sort_gui(mid+1,r);
hebin(l,mid,r);
}
int main()
{
cin >> n;
for (int i=0;i<n;i++){
cin >> a[i];
}
sort_gui(0,n-1);
for (int i=0;i<n;i++){
cout << a[i];
}
return 0;
}
应用场景:
1.求逆序对个数
其实排序的过程都是在消除逆序对的过程,而冒泡排序等一次只能消除一个。
归并排序在合并的过程中其实就是在消除逆序对,只需要在上面代码中增加一条语句,当右边区间的被选为最小加进来的时候就是在消除逆序对,但是注意的一点是这个条件判断一定要加等于a[q]<=a[p]
#include<iostream>
using namespace std;
int n;
int a[1005];
int count=0;
//借助一个数组来存排序小的值
int b[1005];
void hebin(int l,int mid,int r)
{
//左边的第一个指针
int q=l;
//右边区域的第一个指针
int p=mid+1;
for (int i=l;i<=r;i++){
if ((p>r) || q<=mid&&a[q]<=a[p]){
b[i]=a[q];
q++;
}
else{
b[i]=a[p];
//diou
p++;
//记录逆序对
//如 1345 | 2233
//排 12
//别忘记这个+1
count+=mid-q+1;
}
}
for (int j=l;j<=r;j++){
a[j]=b[j];
}
}
void sort_gui(int l,int r)
{
if (l==r){
return;
}
int mid=(l+r)/2;
sort_gui(l,mid);
sort_gui(mid+1,r);
hebin(l,mid,r);
}
int main()
{
cin >> n;
for (int i=0;i<n;i++){
cin >> a[i];
}
sort_gui(0,n-1);
for (int i=0;i<n;i++){
cout << a[i];
}
return 0;
}
1.3 快速排序
选择一个基准,将小于基准的放在基准左边,大于基准的放在基准右边,然后对基准左右都继续执行如上操作直到全部有序
注意的小细节
基准的位置也是参与交换的
代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
int a[1005];
int n;
void quick_sort(int l,int r)
{
//定义基准位置
int mid = (l+r)/2;
int x=a[mid];
//定义两个区间的一头一尾的指针
int q=l;
int p=r;
while(q<=p){
//这里不能写等于号是为了mid也参与交换否则碰到13754这样的情况会变死循环
while(a[q] < x){
q++;
}
while(a[p] > x){
p--;
}
//保险起见还需要确认一次,其实我觉得不需要
if (q<=p){
swap(a[q],a[p]);
q++;
p--;
}
}
if (l<p){
quick_sort(l,p);
}
if (q<r){
quick_sort(q,r);
}
}
int main()
{
cin >> n;
for (int i=0;i<n;i++){
cin >> a[i];
}
quick_sort(0,n-1);
for (int i=0;i<n;i++){
cout << a[i] << " ";
}
cout << endl;
return 0;
}
应用场景:
求一个序列的第k小数,若数组大的话显然不能按照O(n**2)的方法进行排序。而且用快排的话对于第k个数不在的区间就可以不管。
代码实现:
该题由于输入数量大,普通的读入会造成runtime error。
这样就没事
#include<iostream>
#include<algorithm>
using namespace std;
int a[5000005];
int n,k,t,result;
int FindK(int l,int r,int k)
{
if (l==r){
return a[l];
}
//定义基准位置
int mid = (l+r)/2;
int x=a[mid];
//定义两个区间的一头一尾的指针
int q=l;
int p=r;
while(q<=p){
//这里不能写等于号是为了mid也参与交换否则碰到13754这样的情况会变死循环
while(a[q] < x){
q++;
}
while(a[p] > x){
p--;
}
//保险起见还需要确认一次,其实我觉得不需要
if (q<=p){
swap(a[q],a[p]);
q++;
p--;
}
}
if (k<=p){
return FindK(l,p,k);
}
else if (q<=k){
return FindK(q,r,k);
}
else{
//这部分属于排好序的,所以直接返回即可
return a[k];
}
}
int main()
{
std::ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> t;
while (t--){
cin >> n >> k;
for (int i=0;i<n;i++){
cin >> a[i];
}
result = FindK(0,n-1,k-1);
// for (int i=0;i<n;i++){
// cout << a[i] << " ";
// }
// cout << endl;
cout << result << endl;
}
return 0;
}
另外,题目给了快读读取的方式,这个只能读数字
inline int read(){
int x = 0, f = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){
if (ch == '-')
f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9'){
x = (x<<1) + (x<<3) + (ch^48);
ch = getchar();
}
return x * f;
}
1.4 小q的数列
链接:https://ac.nowcoder.com/acm/contest/21763/1002
题目描述:
小q最近迷上了各种好玩的数列,这天,他发现了一个有趣的数列,其递推公式如下:
f[0]=0 f[1]=1;
f[i]=f[i/2]+f[i%2];(i>=2)
现在,他想考考你,问:给你一个n,代表数列的第n项,你能不能马上说出f[n]的值是多少,以及f[n]所代表的值第一次出现在数列的哪一项中?(这里的意思是:可以发现这个数列里某几项的值是可能相等的,则存在这样一个关系f[n’] = f[n] = f[x/2]+f[x%2] = f[x]…(n’<n<x) 他们的值都相等,这里需要你输出最小的那个n’的值)(n<10^18)
输入描述:
输入第一行一个t
随后t行,每行一个数n,代表你需要求数列的第n项,和相应的n'
(t<4*10^5)
题解:
上课老师的思路:
可以看到这道题卡时间卡的很紧,尤其是看到t的大小的时候cin和cout就有点危险了。可以看到每次函数的时候n在除半所以进行一次递归的操作还是可以接受的,可以用递归来实现函数,但是第二个问题就不能用递归来一个个找了。可以先看这个函数的规律,f[i%2]写成i%2是等价的(这样可以少一次递归)因为从两个if可以得到。
规律:
f(60)
=f(30)+0
=f(15)+0+0
=f(7)+1+0+0
=f(3)+1+1+0+0
=f(1)+1+1+1+0+0
=1+1+1+1+0+0
其实这就是60的二进制111100,加的结果就是60的二进制有几个1
一个数是否于1,跟它的二进制最后一位是否是1有关
这样下来以二进制的角度去看,除2的操作就是右移一位的操作(n>>1),综合下来看就是n的二进制有多少个1.
第二问由于问的是最小的,所以f(n)等于1的个数没有任何多余的0
这样就可以写为
n
′
=
2
f
(
n
)
−
1
(这是二进制的运算)
n'=2^{f(n)}-1(这是二进制的运算)
n′=2f(n)−1(这是二进制的运算)
等价于(1<<f(n) )-1(cpp里这样写后运算是二进制之后结果会自动转为int),这样就求到了有相同二进制1的个数的最小整数。
另外实现上要注意小细节,由于n是10**18次的起码有18个1因此,左移1的时候这个1要开longlong否则会左移到int的尽头时会溢出
#include<iostream>
using namespace std;
int Fun(long long int n)
{
if (n==1){
return 1;
}
if (n==0){
return 0;
}
return Fun(n/2)+n%2;
}
int main()
{
int t,fn,n1;
long long n;
cin >> t;
while (t--){
cin >> n;
fn = Fun(n);
//1LL是long long型的1
cout << fn << " " << ((1LL << fn)-1) << endl;
}
return 0;
}
1.5 树
二叉树的左右孩子是严格区分的不能随便交换
1、完全二叉树:深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称为完全二叉树。
2、满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树和满二叉树好用的地方,可以根据左子节点是父节点的2倍,右子节点的父节点2倍加1的特性,只需要数组就能存储并且不丢失关系
先序遍历:先根-左右
中序遍历:左-中根-右
后序遍历:左右-后根
层序遍历:
已知前序后序的情况下中序是不唯一的,因为前序是根左右,后序是左右根,因此左右是分不清的
例题:
求先序排列
#include<iostream>
#include<string>
using namespace std;
string middle;
string back;
//参数列表:前两个为中序排列序列的左右区间,后两个为后序排列序列的左右区间
void solution(int l1,int r1,int l2,int r2)
{
if (l1>r1){
return;
}
char root=back[r2];
cout << root;
//这里可以省略因为最后都会变成l1>r1情况,不省略的话可以提前结束
if (l1==r1){
return;
}
int pos = -1;
for (int i=l1;i<=r1;i++){
if (middle[i]==root){
pos=i;
break;
}
}
//递归左子树
solution(l1,pos-1,l2,l2+(pos-1-l1));
//递归右子树
solution(pos+1,r1,l2+(pos-1-l1)+1,r2-1);
}
int main()
{
cin >> middle >> back;
int n = middle.length();
solution(0,n-1,0,n-1);
cout << endl;
return 0;
}
二、分治
合久必分,分久必合
2.1 FBI树
链接:https://ac.nowcoder.com/acm/contest/21763/1013
我们可以把由“0”和“1”组成的字符串分为三类:全“0”串称为B串,全“1”串称为I串,既含“0”又含“1”的串则称为F串。
FBI树是一种二叉树[1],它的结点类型也包括F结点,B结点和I结点三种。由一个长度为2N的“01”串S可以构造出一棵FBI树T,递归的构造方法如下:
(1)T的根结点为R,其类型与串S的类型相同;
(2)若串S的长度大于1,将串S从中间分开,分为等长的左右子串S1和S2;由左子串S1构造R的左子树T1,由右子串S2构造R的右子树T2。
现在给定一个长度为2N的“01”串,请用上述构造方法构造出一棵FBI树,并输出它的后序遍历[2]序列。
#include<iostream>
using namespace std;
int n;
string s;
char solution(int l,int r)
{
if (l==r){
if (s[l]=='0'){
cout << 'B';
return 'B';
}
else{
cout << 'I';
return 'I';
}
}
int mid = (l+r)/2;
//左子树
char lchild=solution(l,mid);
//右子树
char rchild=solution(mid+1,r);
//根(自己本身)
if (lchild=='B' && rchild=='B'){
cout << 'B';
return 'B';
}
else if (lchild=='I' && rchild=='I'){
cout << 'I';
return 'I';
}
else{
cout << 'F';
return 'F';
}
}
int main()
{
cin >> n >> s;
//这样写是不行的
// int N=2**n;
solution(0,(1<<n)-1);
cout << endl;
return 0;
}
2.2 求最大子串和
若采用最原始的方式时间复杂度是O(n**2)因为要遍历起点再用前缀和相减
1.动态规划
2.把序列分成两部分
左边有最大和
右边有最大和
最大和在中间(此时以中间边界为起点对左边求出最大后缀和,右边求出最大前缀和)
实现还是按照递归实现的
此方法的时间复杂度为O(nlogn)
#include<iostream>
using namespace std;
int n;
int a[10005];
int maxs(int l,int r)
{
int mid=(l+r)/2;
if (l==r){
return a[l];
}
int ans=max(maxs(l,mid),maxs(mid+1,r));
//当最大和在中间
//求左边区域的最大后缀和
int tmp=0,ll=0,rr=0;
for (int i=mid;i>=l;i--){
tmp+=a[i];
ll=max(tmp,ll);
}
tmp=0;
for (int i=mid+1;i<=r;i++){
tmp+=a[i];
rr=max(rr,tmp);
}
ans = max(ans,ll+rr);
return ans;
}
int main()
{
cin >> n;
for (int i=0;i<n;i++){
cin >> a[i];
}
int res=maxs(0,n-1);
cout << res << endl;c++
return 0;
}
3.基于贪心一题思路的解法
贪心原题:
给定长度为n的整数数列ai,找出两个整数ai和aj(i<j),使得ai-aj最大。
解法:
#include<iostream>
#include<cstring>
using namespace std;
int n;
int a[10005];
int minn[10005];
int main()
{
cin >> n;
for (int i=0;i<n;i++){
cin >> a[i];
}
memset(minn,0,10005);
//求出最小后缀
for (int i=n;i>=1;i--){
minn[i]=min(minn[i+1]+a[i])
}
int maxx=-MAXINT;
int ans
for (int j=0;j<n-1;j++){
ans=a[j]-minn[j+1];
if (ans>maxx){
maxx=ans;
}
}
return 0;
}
因此,本题同样道理找sum[j]-sum[i-1];
使得最大的组合,与上面思想一样有个小要求就是j>i-1即i在0到j-2的范围内,sum【i-1】在此范围求后缀min
for (int i=0;i<n;i++){
for (int j=i+1;j<n;j++){
sum[j]-sum[i-1];
}
}
2.3计算表达式
可以不用数据结构那样使用后缀树和栈来解决(比较复杂)
新的一种简单的方法
#include<iostream>
#include<string>
#include<cmath>
using namespace std;
string s;
int cal_num(int l,int r)
{
//这个初始化很重要包含了当-90的情况,可以变为0-90
int num=0;
for (int i=l;i<=r;i++){
num*=10;
num+=s[i]-'0';
}
return num;
}
int solution(int l,int r)
{
int cnt=0;
//加减号的位置
int pos1=-1;
//乘除的位置
int pos2=-1;
//次方符的位置
int pos3=-1;
for (int i=l;i<=r;i++){
if (s[i]=='('){
cnt++;
}
if (s[i]==')'){
cnt--;
}
//包括了((5+1)))+3)
if (cnt<=0){
if (s[i]=='+' || s[i]=='-'){
pos1=i;
}
if (s[i]=='/' || s[i]=='*'){
pos2=i;
}
if (s[i]=='^'){
pos3=i;
}
}
}
if (pos1==-1 && pos2==-1 && pos3==-1){
//((5+4)+6)
if (cnt==0 && s[l]=='('){
//去掉左右括号
return solution(l+1,r-1);
}
//((5+4)+(6*8)
if (cnt>0 && s[l]=='('){
return solution(l+1,r);
}
//((5+4)+6))))
if (cnt<0 && s[r]==')'){
return solution(l,r-1);
}
//当只剩下数字时
return cal_num(l,r);
}
//处理pos有数字的情况
if (pos1!=-1){
if (s[pos1]=='+'){
return solution(l,pos1-1) + solution(pos1+1,r);
}
if (s[pos1]=='-'){
return solution(l,pos1-1) - solution(pos1+1,r);
}
}
if (pos2!=-1){
if (s[pos2]=='/'){
return solution(l,pos2-1) / solution(pos2+1,r);
}
if (s[pos2]=='*'){
return solution(l,pos2-1) * solution(pos2+1,r);
}
}
if (pos3!=-1){
return pow(solution(l,pos3-1) , solution(pos3+1,r));
}
}
int main()
{
cin >> s;
int res=solution(0,s.length()-1);
cout << res << endl;
return 0;
}
在自己的编译器上各种例子都通过了,但奇怪的是牛客的编译器一直在报的错误。网上查了说可能是有return的语句少了else的情况,编译器觉得不安全
编译错误:您提交的代码无法完成编译
a.cpp:90:1: error: non-void function does not return a value in all control paths [-Werror,-Wreturn-type]
}
^
1 error generated.
解决了
在最后填个return 0;即可
2.4 中序序列
链接:https://ac.nowcoder.com/acm/contest/21763/1012
来源:牛客网
给定一棵有n个结点的二叉树的先序遍历与后序遍历序列,求其中序遍历序列。
若某节点只有一个子结点,则此处将其看作左儿子结点
解法:
先序:根左右
后序:左右根
因为有了第二个条件,意味着先序序列中,根的第二个元素一定是左子节点,这样在后序中左的最后一个元素确定了,根也知道就能知道右了。
#include <iostream>
#include <vector>
using namespace std;
vector<int> res;
int n;
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param n int 二叉树节点数量
* @param pre intvector 前序序列
* @param suf intvector 后序序列
* @return intvector
*/
void deal(int pl,int pr,int sl,int sr,vector<int>& pre, vector<int>& suf)
{
if (pl > pr){
return;
}
if (pl==pr){
res.push_back(pre[pl]);
return;
}
int left = pre[pl+1];
int pos=-1;
for (int i=sl;i<=sr;i++){
if (suf[i]==left){
pos=i;
}
}
//左
deal(pl+1,pl+1+pos-sl,sl,pos,pre,suf);
//根
res.push_back(pre[pl]);
//右
deal(pl+1+pos-sl+1,pr,pos+1,sr,pre,suf);
}
vector<int> solve(int n, vector<int>& pre, vector<int>& suf) {
deal(0,n-1,0,n-1,pre,suf);
return res;
}
};
int main()
{
cin >> n;
vector<int> pre(n,0);
vector<int> suf(n,0);
vector<int>::iterator it;
for (int i=0;i<n;i++){
cin >> pre[i];
}
for (int i=0;i<n;i++){
cin >> suf[i];
}
// for (it=suf.begin();it!=suf.end();++it){
// cin >> *it;
// }
Solution sol;
vector<int> final = sol.solve(n,pre,suf);
// for (int i=0;i<final.size();i++){
// cout << final[i] << endl;
// }
for (it=final.begin();it!=final.end();++it){
cout << *it << endl;
}
return 0;
}
2.5 平面最近点对问题
(先按照x轴排序)先按照x轴垂直的方向将平面分成一半,在左边区间和右边区间找两点之间的最小值记录下来(这个找的过程其实也是一个递归的过程因为实际这部分不要明确写实现的过程用本函数的递归返回左右区间的各最小值再取个min得到d),接下来与靠近中间分割线两边的点之间的距离与刚刚的最小值进行比较。至于中间分割线附近的最近点只要找在对纵轴方向排序后以类似两个for循环的方式固定一个点找在纵轴方向小于d的另个点,如果大于d则直接跳出循环这样下来的复杂度在O(Nlognlogn)
实现代码课上给了,暂时没自己写。有空再补
三、倍增 – 直接从小问题合成大问题答案
如a的b次方的模p,把b拆成m+n的形式,一般以2的幂次,这样算a的m次放后以a的m次方倍增到a的b次方