第七节课:

本文介绍了贪心算法在解决字符串问题中的应用,通过确保每个位置不受之前灯的影响来计算最少的路灯数量。接着,详细阐述了先序遍历和后序遍历的关系,用于重构二叉树。同时,探讨了如何将数字转换为中文数字的算法,以及在完全二叉树中寻找节点数量的高效方法。最后,讨论了寻找最长递增子序列的两种动态规划策略,并展示了如何判断一个数是否能被3整除。这些算法和问题涵盖了数据结构和算法的基础知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


题目一

在这里插入图片描述
使用贪心算法:当前来到i位置,讨论i位置是否点灯,之前的选择要确保不会对i位置产生影响,即之前的选择点灯的位置不会照亮i位置。1) i位置为x–>i++;2) (a) i位置为.,如果i+1位置也是.将灯放在i+1位置,i+2位置不管是.还是x都可以直接跳过–>i+3;(b) i位置为x,如果i+1位置是x将灯放在i位置,i+1位置直接跳过–>i+2
代码实现:

//s中只有.和x两种字符
//路灯可以影响左中右三个位置
//至少需要多少灯可以把所有的.都点亮
int lightNums(string& s) {
	int i = 0;
	int res = 0;
	//当前来到i位置,一定要保证之前的灯不会影响到i位置
	//也就是确保之前点的灯一定不会照亮i位置
	while (i < s.length()) {
		if (s[i] == 'x') {//当前为x直接跳过
			i++;
		}
		else {//当前为.,一定要点灯
			res++;
			if (i + 1 < s.length() && s[i + 1] == '.') {
				//如果i+1位置也是.,将灯放在i+1位置,i+2位置不管是.还是x都可以直接跳过
				i = i + 3;
			}
			else if (i + 1 < s.length() && s[i + 1] == 'x') {
				//如果i+1位置是x,将灯放在i位置,i+1位置直接跳过
				i = i + 2;
			}
			else {
				//当前是最后一个位置,跳出循环
				break;
			}
		}
	}
	return res;
}

题目二

在这里插入图片描述
根据先序遍历的第一个值(头节点)将中序遍历划分成左右两个部分(左右子树);先序遍历的第一个值是后序遍历的最后一个值;递归求解左右子树。

在这里插入图片描述

void f(vector<int>& pre, int prei, int prej, int ini, int inj, vector<int>& pos, int posi, int posj, unordered_map<int, int>& mp) {
	if (prei > prej) {
		return;
	}
	pos[posj] = pre[prei];//先序遍历的第一个值是后序遍历的最后一个值
	if (prei == prej) {
		return;
	}
	
	int inlj = mp[pre[prei]] - 1;//根据头节点分成两部分,左部分的j
	int inri = mp[pre[prei]] + 1;//右部分的i
	int inli = ini;
	int inrj = inj;

	int preli = prei + 1;//左部分的i
	int prelj = inlj - inli + preli;//左部分的j
	int preri = prelj + 1;//右部分的i
	int prerj = prej;//右部分的j

	int posli = posi;//左部分的i
	int poslj = inlj - inli + posli;//左部分的j
	int posri = poslj + 1;//右部分的i
	int posrj = posj - 1;//右部分的j

	f(pre, preli, prelj, inli, inlj, pos, posli, poslj, mp);
	f(pre, preri, prerj, inri, inrj, pos, posri, posrj, mp);
}

vector<int>getPost(vector<int>& pre, vector<int>& in) {
	int len = pre.size();
	vector<int>res(len);
	unordered_map<int, int>mp;//便于查找头节点所在位置
	for (int i = 0; i < len; i++) {
		mp.insert({ in[i],i });
	}
	f(pre, 0, len - 1, 0, len - 1, res, 0, len - 1, mp);
	return res;
}


题目三

在这里插入图片描述
从小范围往大范围发散。
代码实现:

string num1To9(int num) {
	if (num < 1 || num>9) {
		return "";
	}
	vector<string>nums({ {"一"}, {"二"}, {"三"}, {"四"}, {"五"}, {"六"}, {"七"}, {"八"}, {"九"} });
	return nums[num - 1];
}

string num1To99(int num, int hasBai) {
	if (num < 1 || num>99) {
		return "";
	}
	if (num < 10) {
		return num1To9(num);
	}
	//有十位
	int shi = num / 10;
	//有无百位,对十位位1的数读法不一样,比如,17:十七;217:二百一十七
	if (shi == 1 && !hasBai) {
		return "十" + num1To9(num % 10);
	}
	return num1To9(shi) + "十" + num1To9(num % 10);
}

string num1To999(int num) {
	if (num < 1 || num>999) {
		return "";
	}
	if (num < 100) {
		return num1To99(num,false);
	}
	string res = num1To9(num / 100) + "百";
	int rest = num % 100;
	if (rest == 0) {
		return res;
	}
	if (rest >= 10) {
		res += num1To99(rest, true);
	}
	else {
		res += "零" + num1To99(rest, true);
	}
	return res;
}

string num1To9999(int num) {
	if (num < 1 || num>9999) {
		return "";
	}
	if (num < 1000) {
		return num1To999(num);
	}
	string res = num1To9(num / 1000) + "千";
	int rest = num % 1000;
	if (rest == 0) {
		return res;
	}
	if (rest >= 100) {
		res += num1To999(rest);
	}
	else {
		res += "零" + num1To99(rest, false);
	}
	return res;
}

string num1To99999999(int num) {
	if (num < 1 && num>99999999) {
		return "";
	}
	if (num < 10000) {
		return num1To9999(num);
	}
	int wan = num / 10000;
	int rest = num % 10000;
	string res = num1To9999(wan) + "万";
	if (rest == 0) {
		return res;
	}
	if (rest < 1000) {
		res += "零" + num1To999(rest);
	}
	else {
		res += num1To9999(rest);
	}
	return res;
}

string getNumChiExp(int num) {
	if (num == 0) {
		return "零";
	}
	string res = num < 0 ? "负" : "";
	num = abs(num);
	int yi = num / 100000000;
	int rest = num % 100000000;
	if (yi == 0) {
		return res + num1To99999999(rest);
	}
	res += num1To9999(yi) + "亿";
	if (rest == 0) {
		return res;
	}
	if (rest < 10000000) {
		res += "零" + num1To99999999(rest);
	}
	else {
		res += num1To99999999(rest);
	}
	return res;
}


题目四

在这里插入图片描述
可以遍历二叉树,但是题目给的是完全二叉树,应该有更快的算法。首先求完全二叉树的深度d(从头结点一直向左走);然后求头节点的右子树的深度,如果为d-1,说明头节点的左子树一定是满二叉树,如果为d-2,说明头节点的左子树不是满二叉树,但头节点的右子树一定是满二叉树;对不满的那个子树递归求节点树

在这里插入图片描述
时间复杂度:O((logN)^2)

在这里插入图片描述
代码实现:

class Node {
public:
	int val;
	Node* left;
	Node* right;
	Node(int val) {
		this->val = val;
		this->left = nullptr;
		this->right = nullptr;
	}
};

int getLevel(Node* head) {
	if (head == nullptr) {
		return 0;
	}
	int level = 0;
	while (head != nullptr) {
		level++;
		head = head->left;
	}
	return level;
}
int NodeNum(Node* head) {
	if (head == nullptr) {
		return 0;
	}
	int res = 1;//头节点
	int level = getLevel(head);
	int leftLevel = getLevel(head->left);
	int rightLevel = getLevel(head->right);
	
	if (leftLevel == rightLevel) {
		res += (1 << leftLevel) - 1;
		res += NodeNum(head->right);
	}
	else {
		res += (1 << rightLevel) - 1;
		res += NodeNum(head->left);
	}
	return res;
}

题目五

在这里插入图片描述
子序列可以不连续,在不改变相对位置的情况下只要满足递增即可。
方法一:动态规划。dp[i]表示必须以arr[i]结尾的最长递增子序列;每遍历到一个位置查找前面比arr[i]小的元素的最长子序列,选择其中最长的那个作为arr[i]的前一个元素,则dp[i]的值就是在它的最长子序列长度再加一。
在这里插入图片描述
方法二:在方法一的基础上申请一个等长的ends数组,开始时ends全部为无效区;如果一个位置i在有效区,则ends[i]的含义是:所有长度为i+1的递增子序列中最小结尾是谁;arr每遍历到一个位置i在ends数组的有效区中二分的查找大于arr[i]最左的位置j,用arr[i]更新该位置的值ends[j],dp[i]=j+1;如果在ends的有效区中没找到大于arr[i]的位置,则扩充有效区j,ends[j]=arr[i],dp[i]=j+1。

在这里插入图片描述
为什么上述流程是对的?ends有效区一定是递增的。假设arr[i]=92,dp[i]=?arr[i]可以更新ends[20],长度为21的子序列的最小结尾是100,长度为20的子序列的最小结尾是90,那么92可以补在长度为20的子序列的后面,形成长度为21的子序列。

在这里插入图片描述
动态规划的时间复杂度为O(N^2),每个位置求动态规划值的时候存在枚举行为;方法二加速的点是构建了单调性,当求解存在枚举行为的动态规划时能够构建单调性,至少可以降阶,或者直接省掉。
代码实现:

int maxSubLen01(vector<int>& arr) {
	if (arr.size() == 0) {
		return 0;
	}
	vector<int>dp(arr.size());//以i结尾的最长子序列长度
	dp[0] = 1;
	int res = 1;
	for (int i = 1; i < arr.size(); i++) {
		for (int j = 0; j < i; j++) {
			dp[i] = arr[i] > arr[j] ? max(dp[i], dp[j]) : dp[i];
		}
		dp[i] += 1;
		res = max(res, dp[i]);
	}
	return res;
}

int maxSubLen02(vector<int>& arr) {
	if (arr.size() == 0) {
		return 0;
	}
	int res = 1;
	vector<int>eds(arr.size());
	eds[0] = arr[0];
	int e = 0;//有效区的右边界
	for (int i = 1; i < arr.size(); i++) {
		int l = 0;
		int r = e;
		int index = -1;
		while (l <= r) {
			int mid = l + ((r - l) >> 1);
			if (eds[mid] < arr[i]) {
				l = mid + 1;
			}
			else {
				r = mid - 1;
				index = mid;
			}
		}
		if (index != -1) {
			eds[index] = arr[i];
		}
		else {
			e++;//扩展有效区
			eds[e] = arr[i];
		}
	}
	res = e + 1;
	return res;
}

题目六

在这里插入图片描述
判断一个数能否被3整数等价于这个数的每一位相加之和能否被3整数,注意是等价,因此可以反过来,将这个数分成各个部分,各部分求和判断能否被3整除。举个栗子:判断1234能否被3整除,可以将个十百千位上的数字相加得到10,判断10能否被3整除;也可以直接判断1234能否被3整除;也可以判断12+34能否被3整除,因为他们都等价于判断1+2+3+4能否被3整除。
代码实现:

int f(int l, int r) {
	int res = 0;
	for (int i = 0; i < r - l + 1; i++) {
		if (((long)(l + i + 1) * (l + i) >> 1) % 3 == 0) {
			res++;
		}
	}
	return res;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值