第五节课:


题目一

在这里插入图片描述
题目分析:(1、2)和(2、1)算一种裂开方式,移除可以认为裂开的方式只能是不递减的。
f(pre,rest):还剩rest的数需要裂开,要求裂开的数第一个数不能比pre小,返回裂开的方法数
改动态规划
在这里插入图片描述
存在枚举行为:斜率优化,观察临近几个位置能否替代枚举行为。如果不存在枚举行为,停留在记忆化搜索阶段就可以了,复杂度不会再有优化,但是存在枚举行为,考虑斜率优化。

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

//还剩rest的数需要裂开,要求裂开的数第一个数不能比pre小,返回裂开的方法数
int f(int pre, int rest) {
	if (rest == 0) {
		return 1;//之前的裂开方案构成了一种有效方法
	}
	if (rest < pre) {
		return 0;
	}
	int res = 0;
	for (int i = pre; i <= rest; i++) {
		res += f(i, rest - i);
	}
	return res;
}

int ways(int n) {
	if (n < 1) {
		return 0;
	}
	return f(1, n);
}


//改动态规划
//dp[i][j]:还剩j需要分裂,分裂j的第一个数不能比i小
int ways2(int n) {
	vector<vector<int>>dp(n + 1, vector<int>(n + 1));
	for (int i = 0; i < n + 1; i++) {
		dp[i][0] = 1;
	}
	for (int i = 1; i < n + 1; i++) {
		dp[i][i] = 1;
	}
	dp[1][2] = 2;
	for (int i = 2; i < n; i++) {
		dp[i][i + 1] = 1;
	}
	//从下往上,从左往右
	for (int i = n - 2; i >= 1; i--) {
		for (int j = i + 2; j < n + 1; j++) {
			for (int k = i; k <= j; k++) {
				dp[i][j] += dp[k][j - k];
			}

		}
	}
	return dp[1][n];
}


//存在枚举行为:斜率优化
int ways3(int n) {
	vector<vector<int>>dp(n + 1, vector<int>(n + 1));
	for (int i = 0; i < n + 1; i++) {
		dp[i][0] = 1;
	}
	for (int i = 1; i < n + 1; i++) {
		dp[i][i] = 1;
	}
	dp[1][2] = 2;
	for (int i = 2; i < n; i++) {
		dp[i][i + 1] = 1;
	}
	//从下往上,从左往右
	for (int i = n - 2; i >= 1; i--) {
		for (int j = i + 2; j < n + 1; j++) {
			dp[i][j] = dp[i][j - i] + dp[i + 1][j];
		}
	}
	return dp[1][n];
}

题目二

在这里插入图片描述
拓扑贡献记录:当前选定节点的子节点,对当前选定的节点负责,子节点维护的信息是,以选定节点为头节点的拓扑结构中,子节点为头节点的子树对这个拓扑结构的节点贡献数。
在这里插入图片描述
有没有一种方法,使得原来对左右孩子负责的拓扑贡献记录,变成对当前节点负责的拓扑贡献记录?如果又这样一种方法,且时间复杂度很低,那么就可以很轻易获得每个节点符合搜索二叉树条件的最大拓扑结构的大小。
将原来对10负责的拓扑贡献记录,更新成对18负责:沿着18的左子树的右边界更新;如果来到的节点小于18,那么当前节点的左子树原来的记录不需要修改可以直接用;如果来到的节点大于18,那么以当前节点为头的子树上的所有节点均不可用,沿着这条右边界,沿途修改每个节点的右子树的拓扑贡献记录。如果已知的拓扑贡献记录是对18的右孩子负责的,那就沿着右子树的左边界更新。
在这里插入图片描述
在这里插入图片描述
时间复杂度分析:每个节点都要遍历他的左树的右边界,和右树的左边界(来回个一次),但是各个节点之间的左树的右边界,和右树的左边界是不同的,也就是不存在重复遍历节点的情况,因此时间复杂度为O(N)
在这里插入图片描述
也是属于二叉树的递归套路,之前的用法是左右子树告诉当前节点信息,当前节点生成自己的信息返回,之后左右子树的信息都可以不要了;本题是左右子树的信息生成当前节点的信息,但是左右子树的信息不能丢,只是意义变了

代码实现:

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

class Record {
public:
	int l;
	int r;
	Record(int l,int r) {
		this->l = l;
		this->r = r;
	}
};

//返回被移除的节点数
//s:true表示左子树;false表示右子树
int modifyMap(Node* n, int hVal, unordered_map<Node*, Record*>& mp, bool s) {
	if (n == nullptr || mp.find(n) == mp.end()) {
		return 0;
	}
	Record* r = mp[n];
	if (s) {
		if (n->val > hVal) {
			mp.erase(n);
			return r->l + r->r + 1;
		}
		else {
			int minus = modifyMap(n->right, hVal, mp, s);
			mp[n]->r -= minus;
			return minus;
		}
	}
	else {
		if (n->val < hVal) {
			mp.erase(n);
			return r->l + r->r + 1;
		}
		else {
			int minus = modifyMap(n->left, hVal, mp, s);
			mp[n]->l -= minus;
			return minus;
		}
	}
	return 0;
}

//二叉树的递归套路
//返回以当前节点为头节点符合二叉搜索树的最大拓扑结构中节点数
int maxBSTnodes(Node* head, unordered_map<Node*, Record*>& mp) {
	if (head==nullptr) {
		return 0;
	}

	//左右子树的信息
	int lnodes = maxBSTnodes(head->left, mp);
	int rnodes = maxBSTnodes(head->right, mp);
	
	//修改拓扑贡献记录对当前头节点负责
	modifyMap(head->left, head->val, mp, true);
	modifyMap(head->right, head->val, mp, false);
	
	//修改后的拓扑贡献记录,左右子树分别能给当前节点贡献多少节点
	int lNum = ((mp.find(head->left) != mp.end()) ? (mp[head->left]->l + mp[head->left]->r + 1) : 0);
	int rNum = ((mp.find(head->right) != mp.end()) ? (mp[head->right]->l + mp[head->right]->r + 1) : 0);

	//将当前节点的拓扑贡献记录插入到map中
	mp.insert({ head,new Record(lNum,rNum) });

	//返回最多的节点数
	return max(lNum + rNum + 1, max(lnodes, rnodes));
}

int mainF(Node* head) {
	unordered_map<Node*, Record*>mp;
	return maxBSTnodes(head, mp);
}

题目三

在这里插入图片描述
本题属于完美洗牌问题。当然可以使用一个额外的空间进行外排,但是这样空间复杂度就是O(N)了。能不能做到时间复杂O(N),空间复杂度O(1),实际上完美洗牌问题的时间复杂度只能做到。
给出一个数的下标i(假设从1开始),可以通过简单的变换得到,数组调整之后他的下标i’

在这里插入图片描述

能否像多米诺骨牌那样,一直推下去,即下标循环怼。

在这里插入图片描述
但是!从一个位置出发,不一定能将所有的位置怼出来,也就是一个数组的下标可能构成几个独立的小环,从一个环不能怼到另一个环。
在这里插入图片描述
是否有个机制能告诉我们,下一个环的起始位置呢?当N(偶数)满足时,不同环的触发点为1、3、9…(结论)
在这里插入图片描述
当N不满足上式怎么办呢?算法原型:如何将分为两个部分的数组交换位置,要求额外空间复杂度O(1)–>左部分逆序;右部分逆序;整体再逆序;over

在这里插入图片描述
将一个长度不满足的数组,分成两部分,一部分是满足的,先将满足的部分按要求调好,在对不满足的部分重复上述操作。最接近假设N=14:

在这里插入图片描述
完美洗牌问题的应用:给定一个无序数组(长度奇偶不定),调整数组,使调完之后的数组元素之间满足<=、>=、<=、>=…,要求额外空间复杂度O(1)
在这里插入图片描述
首先对数组排序(堆排);如果长度为偶数,进行完美洗牌,然后两两分组,每组进行交换;如果长度是奇数,除了0位置,其他位置进行完美洗牌,over!
在这里插入图片描述
在这里插入图片描述
代码实现:

//逆序
void reverse(vector<int>& arr, int l, int r) {
	while (r > l) {
		swap(arr[l], arr[r]);
		r--;
		l++;
	}
}
//交换两部分
void rotate(vector<int>& arr, int l, int m, int r) {
	reverse(arr, l, m);
	reverse(arr, m + 1, r);
	reverse(arr, l, r);
}



//下标连续怼
int modifyIndex(int l, int r, int i) {
	//传入真实的坐标,返回从1开始的下标,表示该真实坐标对应的值变换后的位置
	int len = r - l + 1;
	i = i - l + 1;
	if (i <= len / 2) {
		return 2 * i;
	}
	return 2 * i - len - 1;
}
void cycle(vector<int>& arr, int l, int r, int start) {
	int trigger = start;//下标从1开始
	int cur = modifyIndex(l, r, start + l - 1);
	int pre = arr[start + l - 1];
	while (cur != trigger) {
		int t = arr[l + cur - 1];
		arr[l + cur - 1] = pre;
		pre = t;
		cur = modifyIndex(l, r, l + cur - 1);
	}
	arr[start + l - 1] = pre;
}
void cycles(vector<int>& arr, int l, int r, int k) {
	vector<int>start(k);//给个循环的开始位置,从1开始
	int base = 1;
	for (int i = 0; i < k; i++) {
		start[i] = base;
		base *= 3;
	}
	for (int i = 0; i < k; i++) {
		cycle(arr, l, r, start[i]);
	}
}


//在arr[l..r]上做完美洗牌调整
void shuffle(vector<int>& arr, int l, int r) {
	while (r - l + 1 > 0) {
		int len = r - l + 1;//总长度
		int base = 3;
		int k = 1;
		while (len + 1 >= 3 * base) {
			base *= 3;
			k++;
		}
		base -= 1;//当前处理部分的长度
		//交换两部分
		rotate(arr, l + base / 2, len / 2 + l - 1, len / 2 + base / 2 + l - 1);

		//下标连续怼
		cycles(arr, l, l + base - 1, k);

		//解决剩余部分
		l = l + base;
	}
}


//主函数
void shuffle(vector<int>& arr) {
	if (arr.size() != 0 && arr.size() % 2 == 0) {
		shuffle(arr, 0, arr.size() - 1);
	}
}


//完美洗牌问题的应用
void f(vector<int>& arr) {
	int len = arr.size();
	if (len == 0) {
		return;
	}
	if (len % 2 == 0) {
		shuffle(arr);
		for (int i = 0; i < len; i += 2) {
			swap(arr[i], arr[i + 1]);
		}
	}
	else {
		shuffle(arr, 1, len - 1);
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值