算法与数据结构--- 树和二叉树

树和二叉树练习–利用递归解决问题

# [NOIP2001 普及组] 求先序排列

题目描述

给出一棵二叉树的中序与后序排列。求出它的先序排列。(约定树结点用不同的大写字母表示,且二叉树的节点个数 $ \le 8$)。

输入格式

共两行,均为大写字母组成的字符串,表示一棵二叉树的中序与后序排列。

输出格式

共一行一个字符串,表示一棵二叉树的先序。

样例 #1

样例输入 #1

BADC
BDCA

样例输出 #1

ABCD

分析

首先,一点基本常识,给你一个后序遍历,那么最后一个就是根(如ABCD,则根为D)。

因为题目求先序,意味着要不断找根。

那么我们来看这道题方法:(示例)

中序ACGDBHZKX,后序CDGAHXKZB,首先可找到主根B;

那么我们找到中序遍历中的B,由这种遍历的性质,可将中序遍历分为ACGD和HZKX两棵子树,

那么对应可找到后序遍历CDGA和HXKZ(从头找即可)

从而问题就变成求1.中序遍历ACGD,后序遍历CDGA的树 2.中序遍历HZKX,后序遍历HXKZ的树;

接着递归,按照原先方法,找到1.子根A,再分为两棵子树2.子根Z,再分为两棵子树。

就按这样一直做下去(先输出根,再递归);

模板概括为:

  1. step1:找到根并输出
  2. step2:将中序,后序各分为左右两棵子树;
  3. step3:递归,重复step1,2;

代码

#include<bits/stdc++.h>
using namespace std;
//const int N = 1e6+10;
char a[105]/*存中序*/,b[105];/*存后序*/;
void tree(int d,int e){//递归函数
	int i,f,c,j;
	c = 0; //c归零
	if(d > e ) return;//查找范围为d-e(一颗树),范围中没东西了,结束 
	for(j=strlen(a) - 1;j>=0;j--){//倒着找,以便找出最后一个
		for(i = d;i<=e;i++){//!!!整棵树都要找!!
			if(a[i] == b[j]){//!!!整棵树都要找!!
				c = i;
				break;//c有变动,退出循环(没变动说明没找到或者最后才找到) 
			}
		}
		if(c) break;
	}
	cout<<a[c];//输出根
	tree(d,c-1);//左子树先
	tree(c+1,e);
} 

int main()
{
	int c;
	scanf("%s%s",a,b);
	tree(0,strlen(a) - 1);
	
}

# 新二叉树

题目描述

输入一串二叉树,输出其前序遍历。

输入格式

第一行为二叉树的节点数 n n n。( 1 ≤ n ≤ 26 1 \leq n \leq 26 1n26)

后面 n n n 行,每一个字母为节点,后两个字母分别为其左右儿子。特别地,数据保证第一行读入的节点必为根节点。

空节点用 * 表示

输出格式

二叉树的前序遍历。

样例 #1

样例输入 #1

6
abc
bdi
cj*
d**
i**
j**

样例输出 #1

abdicj

分析

给出前序,组成新的二叉树。画图理解会更加直观。

代码

#include<bits/stdc++.h>
using namespace std;
//const int N = 1e6+10;
//char a[30]/*存中序*/,b[105];/*存后序*/;
struct node{
	char l,r;
}t[130];
char a,a1;

void tree(char x)
{
	if(x=='*') return;//如果是 * 说明此乃空节点,那就不用再往下探了 
	cout << x;//先把它给输出出来,碰着一个就踢出去一个,输出的顺序是可以保障的
	tree(t[x].l);//找到他的左孩子,继续往下探(如果左孩子是*的话,会返回的,可以看上一句的上一句) 
	tree(t[x].r);//找到他的右孩子,继续向下探索
	
/*这里我举个例子: 例如输入abc和bcd,a的ASC码是73,所以t[73].l是b(ASC码74),接着再从b开始探,t[74].l之前有过输入,是个c。再从序号为'c'(75)的t数组继续往下探索,一探索到*,就会往回跑。回到c数组的r,往回探,所以顺序问题可以保证,要是还是理解不了,可以画画试试,亲测有效。这个函数和输入其实就是在数组的各个部分之间不断穿梭,用字符的ASC码值作为连接节点的线,数组的左右孩子就是下一个要寻找的数组的代号*/ 
}

int main()
{
	int n;
	cin>>n;
	cin>>a1;
	cin>>t[a1].l;
	cin>>t[a1].r;//输入第一个字母,第一个字母比较特殊,所以单独输入,左孩子,a1所代表的字符再次会转换为ASC码 , 
	for(int i=2;i<=n;i++){
		
		cin>>a;
		cin>>t[a].l;
		cin>>t[a].r;
		
	}
	tree(a1);//进入函数,用的是递归
	return 0;
	
}

# [NOIP2004 普及组] FBI 树

题目描述

我们可以把由 0 和 1 组成的字符串分为三类:全 0 串称为 B 串,全 1 串称为 I 串,既含 0 又含 1 的串则称为 F 串。

FBI 树是一种二叉树,它的结点类型也包括 F 结点,B 结点和 I 结点三种。由一个长度为 2 N 2^N 2N 的 01 串 S S S 可以构造出一棵 FBI 树 T T T,递归的构造方法如下:

  1. T T T 的根结点为 R R R,其类型与串 S S S 的类型相同;
  2. 若串 S S S 的长度大于 1 1 1,将串 S S S 从中间分开,分为等长的左右子串 S 1 S_1 S1 S 2 S_2 S2;由左子串 S 1 S_1 S1 构造 R R R 的左子树 T 1 T_1 T1,由右子串 S 2 S_2 S2 构造 R R R 的右子树 T 2 T_2 T2

现在给定一个长度为 2 N 2^N 2N 的 01 串,请用上述构造方法构造出一棵 FBI 树,并输出它的后序遍历序列。

输入格式

第一行是一个整数 N ( 0 ≤ N ≤ 10 ) N(0 \le N \le 10) N(0N10)

第二行是一个长度为 2 N 2^N 2N 的 01 串。

输出格式

一个字符串,即 FBI 树的后序遍历序列。

样例 #1

样例输入 #1

3
10001011

样例输出 #1

IBFBBBFIBFIIIFF

提示

对于 40 % 40\% 40% 的数据, N ≤ 2 N \le 2 N2

对于全部的数据, N ≤ 10 N \le 10 N10

分析

1.建树。按照题意是在递归过程中建立树,建树的方法实际上就是树的先序遍历(先根节点,再左右子树)。当本节点长度大于1时递归建立子树。

2.输出。而输出过程是对树的后序遍历(先左右子树,再根节点),这里有个技巧就是可以和建树过程集成在一起。只需将代码放在递归调用之后就可以了。

3.判断。最后是判断当前节点的FBI树类型,可以用B(初始值为1)保存全是‘0’的情况,如果遇到‘1’就将B置为0,用I(初始值为1)保存全是‘1’的情况,如果遇到‘0’就将I置为0。最后判断B和I中的值,如果两个都为0则输出F(不全为‘0’,不全为‘1’)。

代码

#include<bits/stdc++.h>
#include<string>
using namespace std;
string s;
int n;
void tree(int x,int y){
	if(y>x){
		tree(x,(x+y)/2); //先遍历左树
		tree((x+y+1)/2,y);//遍历右树
	}
	int b = 1,i = 1;
	for(int j=0;j<= y-x;j++){
		if(s[x+j] == '1') b=0;//数组中开始出现1
		else if(s[x+j] == '0') i=0;//数组中开始出现0
		
	} 
	if(b) cout<<"B";//b不为0
	else if(i){
		cout<<"I";
	} 
	else cout<<"F"; //b 和 i 都为0
}
int main()
{
	cin>>n>>s;
	tree(0,(1<<n)-1);//2的n次方为总节点 而减1是因为从0开始
	return 0;	
}

这道题给出了一个新的思路,想求一个都为 0 的数组,可以先让b = 1,当出现别的数字时 b = 0;这样更方便判断。

# 【模板】堆

题目描述

给定一个数列,初始为空,请支持下面三种操作:

  1. 给定一个整数 x x x,请将 x x x 加入到数列中。
  2. 输出数列中最小的数。
  3. 删除数列中最小的数(如果有多个数最小,只删除 1 1 1 个)。

输入格式

第一行是一个整数,表示操作的次数 n n n
接下来 n n n 行,每行表示一次操作。每行首先有一个整数 o p op op 表示操作类型。

  • o p = 1 op = 1 op=1,则后面有一个整数 x x x,表示要将 x x x 加入数列。
  • o p = 2 op = 2 op=2,则表示要求输出数列中的最小数。
  • o p = 3 op = 3 op=3,则表示删除数列中的最小数。如果有多个数最小,只删除 1 1 1 个。

输出格式

对于每个操作 2 2 2,输出一行一个整数表示答案。

样例 #1

样例输入 #1

5
1 2
1 5
2
3
2

样例输出 #1

2
5

提示

【数据规模与约定】

  • 对于 30 % 30\% 30% 的数据,保证 n ≤ 15 n \leq 15 n15
  • 对于 70 % 70\% 70% 的数据,保证 n ≤ 1 0 4 n \leq 10^4 n104
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1n106 1 ≤ x < 2 31 1 \leq x \lt 2^{31} 1x<231 o p ∈ { 1 , 2 , 3 } op \in \{1, 2, 3\} op{1,2,3}

分析

1.堆是一颗完全二叉树
2.堆的顶端一定是“最大”,最小”的,但是要注意一个点,这里的大和小并不是传统意义下的大和小,它是相对于优先级而言的,当然你也可以把优先级定为传统意义下的大小,但一定要牢记这一点,初学者容易把堆的“大小”直接定义为传统意义下的大小,某些题就不是按数字的大小为优先级来进行堆的操作的
3.堆一般有两种样子,小根堆和大根堆,分别对应第二个性质中的“堆顶最大”“堆顶最小”,对于大根堆而言,任何一个非根节点,它的优先级都小于堆顶,对于小根堆而言,任何一个非根节点,它的优先级都大于堆顶(这里的根就是堆顶啦)

代码

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int h[N];
int len = 0;//堆的大小 
void push(int x){//要插入的数,上浮
	h[++len]=x;
	int i=len;//插入到堆底 
	while(i > 1 && h[i] < h[i/2]){//找到它的父亲且父亲比它大,那就交换 
		swap(h[i],h[i/2]);
		i/=2;//找到它的父亲 
	}
}
void pop(){//下沉,删除队头,调整堆
	h[1] = h[len]; //交换堆顶和堆底,然后直接弹掉堆底 
	len --;
	int i = 1;
	while(2 * i <=len){  //对该节点进行向下交换的操作 
		int s = 2 * i;
		if(s < len && h[s + 1] < h[s]){
			s ++;
		}
		if(h[s] < h[i]){
			swap(h[s],h[i]);
			i = s;
		}
		else{
			break;
		}
	}
}
int main(){
	int n;
	cin >> n;
	int op ,x;
	while(n--){
		cin >> op;
		if(op==1){
			cin >> x;
			push(x);//将x存到数组
		}
		else if(op ==2){
			cout << h[1] << endl;//输出堆顶
		}
		else{
			pop();
		}
	} 
}

事实上堆的插入就是把新的元素放到堆底,然后检查它是否符合堆的性质,如果符合就丢在那里了,如果不符合,那就和它的父亲交换一下,一直交换交换交换,直到符合堆的性质,那么就插入完成了

# [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 3 3 种果子,数目依次为 1 1 1 2 2 2 9 9 9 。可以先将 1 1 1 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。

输入格式

共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n(1\leq n\leq 10000) n(1n10000) ,表示果子的种类数。

第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1ai20000) 是第 i i i 种果子的数目。

输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231

样例 #1

样例输入 #1

3 
1 2 9

样例输出 #1

15

提示

对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n1000

对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n5000

对于全部的数据,保证有 n ≤ 10000 n \le 10000 n10000

分析

一开始看到这个题,就想到跟以前做的贪心题有点类似,大致思路也就是,先找到两个最小的,取出来,相加再放进去,不过我犯了一个致命性的错误,我每一次取之前都对剩下的进行排序,导致时间超时,后来不排序直接找到最小的和次小的,把它们相加再放进去,这样就省下来排序的时间了,直接过了。看到洛谷里面有人用优先队列做的,其实更简单,定义一个以质量从小到大的队列,每次出队两个,入队一个(入队的就是出队两个数的和),这样也不用排序,同样也可以过。

代码

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

int n;
int x,sum = 0;
priority_queue<int,vector<int>,greater<int>> q; //从小到大:
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++) {
		cin>>x;
		q.push(x); //x进入队列
	}
	for(int i=1;i<n;i++){
		int a = q.top();//最小的
		q.pop();
		int b = q.top();//次小的
		q.pop();
		sum += a+b;
		q.push(a+b);//将合并后的堆加入优先队列
	}
	cout<< sum <<endl;
	return 0;
}

# 中位数

题目描述

给定一个长度为 N N N 的非负整数序列 A A A,对于前奇数项求中位数。

输入格式

第一行一个正整数 N N N

第二行 N N N 个正整数 A 1 … N A_{1\dots N} A1N

输出格式

⌊ N + 1 2 ⌋ \lfloor \frac{N + 1}2\rfloor 2N+1 行,第 i i i 行为 A 1 … 2 i − 1 A_{1\dots 2i - 1} A12i1 的中位数。

样例 #1

样例输入 #1

7
1 3 5 7 9 11 6

样例输出 #1

1
3
5
6

提示

对于 20 % 20\% 20% 的数据, N ≤ 100 N \le 100 N100

对于 40 % 40\% 40% 的数据, N ≤ 3000 N \le 3000 N3000

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 100000 1 \le N ≤ 100000 1N100000 0 ≤ A i ≤ 1 0 9 0 \le A_i \le 10^9 0Ai109

分析

首先记录一个变量mid,记录答案(中位数)。建立两个堆,一个大根堆一个小根堆,大根堆≤mid的数,小根堆存 > mid的数。但我们在输出答案前需要对mid进行调整,如果小根堆和大根堆内元素相同,就无需处理,此时mid仍然是当前的中位数。如果两个堆中元素个数不同,那我们就需要进行调整。把元素个数较多的堆的堆顶作为mid,mid加入元素较少的堆。

代码

#include<bits/stdc++.h>
#include<queue>
using namespace std;
//typedef long long ll;
const int N = 1e5+5;
int a[N],n,mid;

priority_queue<int , vector<int> ,less<int> > q1;//大顶堆
priority_queue<int , vector<int> ,greater<int> > q2;//小顶堆

int main()
{
	cin>>n;
	cin >> a[1];
	mid = a[1];//mid初值是a[1]
      cout << mid << endl; 
	for(int i=2;i<=n;i++){
		cin>>a[i];
		if(a[i] > mid ) q2.push(a[i]);
		else q1.push(a[i]);
		if(i%2 == 1){//第奇数次加入
			while(q1.size() != q2.size()){
				if(q1.size() > q2.size()){
					q2.push(mid);
					mid = q1.top();
					q1.pop();
					
				}
				else{
					q1.push(mid);
					mid = q2.top();
					q2.pop();
				}
			}
			cout << mid <<endl;
		}
	} 
	return 0;
}

用两个堆,一个大根堆,一个小根堆,大根堆维护当前输入中较大的数,小根堆维护当前数组中较小的数。这样就保证了大根堆的堆顶是较小数的最大值,小根堆的堆顶是较大数中的最小值,然后当大根堆的元素个数为小根堆的元素个数+1的时候,大根堆的堆顶就是中位数。

# 最小函数值

题目描述

n n n 个函数,分别为 F 1 , F 2 , … , F n F_1,F_2,\dots,F_n F1,F2,,Fn。定义 F i ( x ) = A i x 2 + B i x + C i ( x ∈ N ∗ ) F_i(x)=A_ix^2+B_ix+C_i(x\in\mathbb N*) Fi(x)=Aix2+Bix+Ci(xN)。给定这些 A i A_i Ai B i B_i Bi C i C_i Ci,请求出所有函数的所有函数值中最小的 m m m 个(如有重复的要输出多个)。

输入格式

第一行输入两个正整数 n n n m m m

以下 n n n 行每行三个正整数,其中第 i i i 行的三个数分别为 A i A_i Ai B i B_i Bi C i C_i Ci

输出格式

输出将这 n n n 个函数所有可以生成的函数值排序后的前 m m m 个元素。这 m m m 个数应该输出到一行,用空格隔开。

样例 #1

样例输入 #1

3 10
4 5 3
3 4 5
1 7 1

样例输出 #1

9 12 12 19 25 29 31 44 45 54

提示

数据规模与约定

对于全部的测试点,保证 1 ≤ n , m ≤ 10000 1 \leq n,m\le10000 1n,m10000 1 ≤ A i ≤ 10 , B i ≤ 100 , C i ≤ 1 0 4 1 \leq A_i\le10,B_i\le100,C_i\le10^4 1Ai10,Bi100,Ci104

分析

建一个大根堆,存最小的数到第m小的数,第m小的数就理所当然的是堆顶了。 每次我们只需要比较新加进来的数比堆顶大还是比堆顶小,如果比堆顶小,将原来的堆顶丢掉,将新的数塞进去; 如若比堆顶大,根据该题题意,a>0&&b>0,函数对称轴x=−b/2∗a恒小于0,可以得出,y在x>0时是单调递增的,所以接下来的函数值y只会大不会小,可以直接break掉了

由于我们存储的时候用的是大根堆,所以记得要逆序输出,将m个数从小到大输出

代码

#include<bits/stdc++.h>
#include<queue>
using namespace std;
//typedef long long ll;
const int N = 1e4+5;
int s[N],n,m;

priority_queue<int> q;

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		for(int j=1;j<=m;j++){
			int k = a*j*j+b*j+c;
			if(i == 1) q.push(k);
			else{
				if(k < q.top()){
					q.push(k);
					q.pop();
				}
				else break; //如果k已经大于第m小的数了,接下来k仍旧单调递增
                           //所以可以直接break掉,一个重要的优化
			}
		}
	}
	for(int i=1;i<=m;i++){
		s[i] = q.top();
		q.pop();
		
	}//记得要逆着输出!
	for(int i=m;i>=1;i--){
		cout<<s[i]<<" ";
	}
	return 0;
}

前m小的数,很容易想到 对顶堆的经典问题第k大数 并且这题非常良心,m的值不会改变

那么它的思想和对顶堆就非常类似了

对顶堆:以求第k大数为例,具体操作需要一个大根堆,一个小根堆。大根堆中存第k+1大到最小的数字,小跟堆中存第一大到第k大数字。每次加入新数字,与小跟堆的top比较,如若比top大,将小根堆的根加入大根堆中,再将小根堆的根pop出来,将要加入的新数字放入小跟堆;如若比小根堆top小,直接加入大根堆。

对顶堆中需要大根堆的原因是它的k根据不同题意可能会改变(比如每次k++之类的),而这题的m不会变,所以就不需要存第m+1小到最大的数啦,直接把它们丢掉好了


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值