第五周算法题(堆,二叉树,dp)

第五周:

第一题:

题目来源:912. 排序数组 - 力扣(LeetCode)

题目描述:

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

解题代码:

void down(int* nums, int i, int numsSize) {
    int cur = i;
    int left = cur * 2 + 1;
    int right = cur * 2 + 2;
    if (left < numsSize && nums[left] > nums[cur]) cur = left;
    if (right < numsSize && nums[right] > nums[cur]) cur = right;
    if (i != cur) {
        int k = nums[cur];
        nums[cur] = nums[i];
        nums[i] = k;
        down(nums, cur, numsSize);
    }
}
int* sortArray(int* nums, int numsSize, int* returnSize) {
for (int i = (numsSize - 1) / 2; i >= 0; i--) {
    down(nums, i, numsSize);
}
for (int i = numsSize - 1; i > 0; i--) {
    int k = nums[0];
    nums[0] = nums[i];
    nums[i] = k;
    down(nums, 0, i);
}
*returnSize = numsSize;
return nums;
}

解题思路:

运用到了堆排序的方法。

对于堆排序,我们一般的方法是首先先初始化成一个小根堆(根小于两个孩子)或者大根堆(根大于孩子),需要注意的是,在堆中,左右孩子并无严格的大小关系。一般的堆排序的问题有(此处默认为小根堆):

//1. 插入一个数         heap[ ++ size] = x; up(size);
//2. 求集合中的最小值   heap[1]
//3. 删除最小值         heap[1] = heap[size]; size -- ;down(1);
//4. 删除任意一个元素   heap[k] = heap[size]; size -- ;up(k); down(k);
//5. 修改任意一个元素   heap[k] = x; up(k); down(k);

在此之前,我们要初始化,我们用数组来实现初始化,我们只需要从一半的元素开始遍历,即n/2;在–到0(或1),为什么选择n/2,因为这是最小的父亲节点,我们在初始化时,只需要down,或者up就行,如果是up,就与down相反,下面是对表格的解释:

1.插入为什么是up:插入在末尾,只能向上走。

2.删除是如何实现的:因为数组删除首元素不简单,所以我们将最后一个元素与最小值交换,并down。

3.修改和删除为什么要down,up,这里其实是为了代码的简便,如果不需要up,自然也up不走。

另一种堆排序的方式(思想类似):
#include 
int h[100005];
int size;
void swap(int* x, int* y) {
	int m = *x;
	*x = *y;
	*y = m;
}
void down(int i) {
	int cur = i;
	int left = cur * 2;
	int right = cur * 2 + 1;
	if (left <= size && h[left] < h[cur]) cur = left;
	if (right <= size && h[right] < h[cur]) cur = right;
	if (i != cur) {
		swap(&h[i], &h[cur]);
		down(cur);
	}
}

int main() {
	int n;
	scanf("%d", &n);
	size = n;
	for (int i = 1; i <= n; i++)scanf("%d", &h[i]);
	for (int i = n / 2; i; i--) {
		down(i);
	}
	int m = n;
	while (m--) {
		printf("%d ", h[1]);
		h[1] = h[size];
		size--;
		down(1);
	}
	return 0;
}
这就是运用了上面的2,3;

至于对数组内的整体排序,就用本题的方法:

1.注意与第一种情况相反,如果要升序,则初始化降序,反之升序。

2.他的思路是每次将最大值与最后一个元素交换,这样,就完成了对他的升序排列.

3.交换时将最大的元素换了一个较小的元素,再次调用down时,是否会交换回来:

不会,每次i–,不会遍历到刚交换过的最大数。

第二题:

题目来源:[P1090 NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目描述:

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

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

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

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

输入格式

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

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

输出格式

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

输入输出样例

输入

3 
1 2 9 

**输出 **

15

说明/提示

对于 30%的数据,保证有 n≤1000:

对于 50%的数据,保证有 n≤5000;

对于全部的数据,保证有 n≤10000。

解题代码:

#include 
#include 
#include 
#include 
int main() {
	std::priority_queue<int, std::vector<int>, std::greater<int>>que;
	int n;
	std::cin >> n;
	for (int i = 0; i < n; i++) {
		int m;
		std::cin >> m;
		que.push(m);
	}
	int sum = 0;
	while (que.size() > 1) {
		int cur = que.top();
		que.pop();
		cur += que.top();
		que.pop();
		sum += cur;
		que.push(cur);
	}
	std::cout << sum;
}

解题思路:

ps:(第一次用c++做题,真的是太方便了,泪目…).

本题用了优先队列的思想,也就是堆,c++中,默认优先队列是大根堆,它的定义是:priority_queue q;,同时要应用头文件,即可实现一堆数据的降序排列,但是在本题中,主要思路是确保每次数据的有序性,即每次合并最小的两堆,即为最优,那就是要升序,即小根堆。所以应定义为prioritry_queue,greater>,我们使用了std::greater作为比较函数,这会使得优先队列变成一个小根堆。这样,队列中的元素会按照从小到大的顺序排列,最小的元素总是位于队列的顶部。至于什么是比较函数,我也不知道,不过后面就水到渠成了,即使你想插入一个元素,它也能保持有序性。

在C++中,std::priority_queue提供了以下等操作:

  1. push(const value_type& val):将元素插入优先队列。
  2. pop():删除优先队列顶部的元素。
  3. top():返回优先队列顶部的元素。
  4. empty():检查优先队列是否为空,如果为空返回true,否则返回false
  5. size():返回优先队列中的元素数量,
  6. swap:用来交换两个优先队列的内容。

以下是这些操作的使用示例:

#include 
#include 
int main() {
    std::priority_queue pq1;
    std::priority_queue pq2;
// 使用push插入元素
pq1.push(1);
pq1.push(2);
pq1.push(3);
pq2.push(4);
pq2.push(5);
pq2.push(6);
// 使用top获取顶部元素
std::cout << "pq1的顶部元素: " << pq1.top() << std::endl;  // 输出: pq1的顶部元素: 3
std::cout << "pq2的顶部元素: " << pq2.top() << std::endl;  // 输出: pq2的顶部元素: 6
// 使用pop删除顶部元素
pq1.pop();
pq2.pop();
std::cout << "删除后的pq1的顶部元素: " << pq1.top() << std::endl;  // 输出: 删除后的pq1的顶部元素: 2
std::cout << "删除后的pq2的顶部元素: " << pq2.top() << std::endl;  // 输出: 删除后的pq2的顶部元素: 5
// 使用empty检查队列是否为空
std::cout << "pq1是否为空: " << (pq1.empty() ? "是" : "否") << std::endl;  // 输出: pq1是否为空: 否
std::cout << "pq2是否为空: " << (pq2.empty() ? "是" : "否") << std::endl;  // 输出: pq2是否为空: 否
// 使用size获取队列中的元素数量
std::cout << "pq1中的元素数量: " << pq1.size() << std::endl;  // 输出: pq1中的元素数量: 2
std::cout << "pq2中的元素数量: " << pq2.size() << std::endl;  // 输出: pq2中的元素数量: 2
// 使用swap交换pq1和pq2的内容
pq1.swap(pq2);
// 打印交换后pq1和pq2的顶部元素
std::cout << "交换后的pq1的顶部元素: " << pq1.top() << std::endl;  // 输出: 交换后的pq1的顶部元素: 5
std::cout << "交换后的pq2的顶部元素: " << pq2.top() << std::endl;  // 输出: 交换后的pq2的顶部元素: 2
return 0;
}

第三题:

题目来源:P4913 【深基16.例3】二叉树深度 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目描述

有一个 n*(n≤10e6) 个结点的二叉树。给出每个结点的两个子结点编号(均不超过 n),建立一棵二叉树(根节点的编号为 1),如果是叶子结点,则输入 0 0

建好这棵二叉树之后,请求出它的深度。二叉树的深度是指从根节点到叶子结点时,最多经过了几层。

输入格式

第一行一个整数 n,表示结点数。

之后 n 行,第 i 行两个整数 lr,分别表示结点 i 的左右子结点编号。若 l=0 则表示无左子结点,r=0 同理。

输出格式

一个整数,表示最大结点深度。

输入输出样例

**输入 **
7
2 7
3 6
4 5
0 0
0 0
0 0
0 0
**输出 **
4

解题代码:

#include 
struct tree {
	int l;
	int r;
};
struct tree arr[1000005];
int max_deep(int i) {
	if ((arr[i].l == 0 && arr[i].r == 0)) {
		return 0;
	}
	int maxl = max_deep(arr[i].l);
	int maxr = max_deep(arr[i].r);
	int max = maxl;
	if (maxr > max) max = maxr;
	return max + 1;
}
int main() {
	int n;
	scanf("%d", &n);
	int i;
	for ( i = 1; i <= n; i++) {
		int ldata;
		int rdata;
		scanf("%d %d", &ldata, &rdata);
		arr[i].l = ldata;
		arr[i].r = rdata;
	} 
	int ret = max_deep(1);
	ret++;
	printf("%d",ret);
	return 0;
}

解题思路:

1.注意ret++,因为本身默认有一个根节点。

2.本题数据过大,如果用链表来做,可能会超时。

3.可以抽象的想,假设我们处于某个节点上,其中的int maxl = max_deep(arr[i].l);是遍历他的左子树,而他的左子树又包含众多的右子树,同理,int maxr = max_deep(arr[i].r);是遍历它的右子树,而每次遍历完左右子树,都要比较得到最大值,并且+1返回,+1的意思是计算了当前节点,

它本身也让深度加一。

下面提供第二种做法:

#include 
int main() {
	int dad[100008] = { 0 };
	int leaf[100008] = { 0 };
	int n;
	scanf("%d", &n);
	int idx=0;
	int i;
	for( i = 1; i <= n; i++) {
		int lchild;
		int rchild;
		scanf("%d %d", &lchild, &rchild);
		if (lchild == 0 && rchild == 0) {
			leaf[idx++] = i;
		}
		else
		{
			dad[lchild] = i;
			dad[rchild] = i;
		}
	}
	int max_deep = 0;
	for ( i = 0; i < idx; i++) {
		int cur_deep = 0;
		int j = leaf[i];
		while (1) {
			cur_deep++;
			if (!dad[j]) {				
				if (cur_deep > max_deep) max_deep = cur_deep;
				break;
			}
			j = dad[j];
		}
	}
	printf("%d", max_deep);
}

解题思路:

这种做法相较于第一种做法操作更简便,不过处理更巧,我们很容易知道,一棵二叉树的最大深度,一定是从根节点开始,也一定是到达某一个叶子节点结束。而对于一个叶子节点,它左孩子和右孩子恒不存在,也就是都为0和0,因此,我们可以定义一个叶子节点数组,来记录叶子节点的数目,并且记录下他们父亲是几号节点,同时,我们可以再定义一个非叶子节点数组,该数组用于储存它们的父亲节点是多少,当然也可以二合一,不过,二合一的话会更加的暴力。首先,我们遍历所有叶子节点,然后不断的模拟回溯,一直找到那个储存为零的点,也就是根节点,并每次计算并更新最大值。

第四题:

题目来源:不同的二叉树 - 虚动智能oj (xdzn.club)

描述

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?输出满足题意的二叉搜索树的种数。

输入描述

输入节点数N

输出描述

输出可以组成的二叉搜索树的数量

用例输入 1

3

用例输出 1

5

解题代码:

#include 
int main() {
	int n;
	scanf("%d", &n);
	int dp[100000] = { 0 };
	dp[0] = 1;
	dp[1] = 1;
	for ( int i = 2; i <= n; i++) {
		for ( int j = 1; j <= i; j++) {
			int l = j - 1;
			int r = i - 1 - l;
			dp[i] += dp[l] * dp[r];
		}
	}
	printf("%d", dp[n]);
	return 0;
}

解题思路:

前言:本题主要运用到了动态规划的思想,一开始确实没想到,一直在想记忆化搜索怎么做,直到看了一个视频,才觉得用动态规划确实方便得多。
思路如下:

这是一段用 C 语言编写的代码,用于计算使用 n 个节点可以构成的唯一二叉搜索树的数量。代码使用 动态规划 来解决问题。变量 n 从用户输入中读取。数组 dp 用于存储中间结果。dp 的前两个元素初始化为 1。外部循环从 2 到 n 迭代,内部循环从 1 到 i 迭代。对于每个 i,代码通过将左子树和右子树的唯一二叉搜索树的数量的乘积相加来计算可以使用 i 个节点构成的唯一二叉搜索树的数量。最后,代码打印可以使用 n 个节点构成的唯一二叉搜索树的数量。

对于一个由1到n组成的二叉排序树,那么它总共的种类之和等于由从1到n分别为根节点的二叉树的种类的和。而对于任意节点x,那么,它的左子树的所有节点的种类之和,一定是从1到x-1,而他的右子树,一定是从x+1到数n。例如,输入3,那么,所有二叉搜索数的种类之和,就是以1,2,3分别为根节点的二叉搜索数的种类的和。已知左子数的节点值一定小于根结点,右子树的所有节点值一定大于根节点,以1为根节点,那么他左子树的节点值只可能为零(不存在,视为一种情况),即只有这一种情况,而他的右子树的结点种类为2(数字2和数字3),那么这种情况,就转化为一个节点数为二的二叉树,有几种组成的问题,即有两种组成。那么,以1为根节点的且组成为1到3的这种二叉排序树的这种情况,有1×2=2种。同理,对于2根节点,然么左边只能为1这一种,右边也只能1,那么就是1×1=1种,3和1相似,也是两种,那么总共就是五种,我们可以定义一个数组,储存下每一种它的值是多少,那么下次,就避免了重复计算,比如刚才的思路中我们就是事先储存了2这种情况下的值,那么,对于以1为根节点的1到3这种情况,那么就直接地dp[0]乘上dp[2]就好。

你可能感兴趣的:(算法,数据结构,动态规划)