第一节课:


题目一

在这里插入图片描述
舍弃可能性的技巧
非基于比较的排序都是根据具体的数据状况来进行的,显然这题不适合;基于比较的排序的最好时间复杂度为:O(N*logN),不满足要求。
假设数组中的元素个数为N,准备N+1个容器;先遍历一遍数组,找出最小min最大max值;如果最小最大值相等,直接返回0;否则将max-min等分成N+1份:间隔为d=(max-min)/(N+1),可以是浮点数;第一个容器存放0~d-1之间的数、…;如果要对数组进行排序,那么可能相邻的两个数存在两种可能:桶内的容器相邻和当前桶的最大值和下一个非空桶的最小值相邻;由于桶的个数比数组元素个数大1,且最小值一定放第一个桶,最大值一定放最后一个桶,那么中间一定存在空桶;该空桶的左侧和右侧都存在不空的桶,左边桶的最大值一定和右边桶的最小值相邻,这两个相邻的数的差值一定大于桶内相邻元素的差值!–>只用找桶间相邻的差值最大值!由于不关心桶内相邻元素的情况,所以可以优化桶中存储的元素,只需要存进入该桶的最大值和最小值。
在这里插入图片描述
在这里插入图片描述

最终的答案是否一定来自空桶两侧非空桶的相邻元素?不一定,例子如下:

在这里插入图片描述

为什么要设置空桶?设置空桶是为了找一个平凡解,宣告一个桶内部相邻元素的差一定不是答案,可以只关注桶间的情况,从而使问题的复杂度为O(N)。这题属于顶级难题,一般做贪心分析可能性时,只会分析哪些显而易见的可能是不必要的,从而优化流程。而这题是人为构造桶的个数,排除掉很多不可能的结果。
代码实现:

int getBucket(int num, int minv, int maxv, int len) {
	//这么理解下面的返回值?
	//想象成一个线段,起点为minv,终点为maxv
	//在这个线段上标刻度。从minv:0到maxv:len
	//当前点num,应该标len * (num - minv) / (maxv - minv)
	//下面的len不能变成len+1,否则最大值求出的下标会超出数组的下标范围
	return len * (num - minv) / (maxv - minv);
}

int maxGap(vector<int>& arr) {
	if (arr.size() < 2) {
		return 0;
	}
	int len = arr.size();
	int minv = arr[0];
	int maxv = arr[0];
	for (int i = 1; i < len; i++) {
		minv = min(minv, arr[i]);
		maxv = max(maxv, arr[i]);
	}
	if (maxv == minv) {
		return 0;
	}
	vector<bool>hasNum(len + 1, false);
	vector<int>mins(len + 1);
	vector<int>maxs(len + 1);

	int bid = 0;
	for (int i = 0; i < len; i++) {
		bid = getBucket(arr[i], minv, maxv, len);
		if (hasNum[bid] == false) {
			hasNum[bid] = true;
			mins[bid] = arr[i];
			maxs[bid] = arr[i];
		}
		else {
			mins[bid] = min(mins[bid], arr[i]);
			maxs[bid] = max(maxs[bid], arr[i]);
		}
	}
	/*也对,没左神的好
	int res = 0;
	for (int i = 0; i < len; i++) {
		if (!hasNum[i]) {
			continue;
		}
		for (int j = i + 1; j < len + 1; j++) {
			if (hasNum[j]) {
				res = max(res, mins[j] - maxs[i]);
				break;
			}
		}
		
	}*/
	int res = 0;
	int lastMax = maxs[0];
	for (int i = 1; i < len + 1; i++) {
		if (hasNum[i]) {
			res = max(res, mins[i] - lastMax);
			lastMax = maxs[i];
		}
	}
	return res;
}


题目二

在这里插入图片描述
给定数组将其分割成几个部分,每个部分的数字求异或(这里的异或理解成不进位相加,从而使得单个数字也能异或,单个数字只有0的异或等于0,其他都非0)。问最多能分割成多少个异或为0的部分(其他的部分可以异或不为0)?

在这里插入图片描述
当面对子数组的问题,就像以每个位置为结尾的情况答案是什么?如果有答案的话,答案一定在其中。
dp[i]:arr[0…i]最优划分下,有多少部分异或和为0,dp[N-1]就是最后的答案;dp[0]=arr[0]==0?1:0;假设0…i上存在最优划分,那么i一定是最后一个部分的最后一个数,一般位置分两种可能性:1) i所在部分是异或和不是0,dp[i]=dp[i-1]、2) i所在部分的异或和是0,看以下分析:
可能性的舍弃是一个大思路,所用的技巧是假设答案法。上一题是构造一个答案,这一题是假设一个答案看它有什么性质。i所在部分的异或和是0,那么该部分的开头在哪呢?假设在k位置,即arr[k…i]异或和为0,那么k一定是离i最近的能做到从k到i异或和为0的位置,证明:假设中间存在l也能做到arr[l…i]异或和为,那么arr[k…l]的异或和一定也为0,这样将arr[k…i]作为最后一个部分就不是最优划分了,与假设的最优划分矛盾,因此k一定是离i最近的能做到从k到i异或和为0的位置。
在这里插入图片描述
怎么找到k呢?求a[0…i]的异或和eor,只要找到上一个从0位置开始异或和为eor的位置,他的下一个位置就是k的位置(任何数异或上0都是其本身);用一个map,key是某一前缀的异或和;value是该异或和上次出现的位置,当需要找最近出现相同异或和的位置的时候,直接查找。第二种情况的dp[i]=max(dp[i-1],dp[k]+1),这里的k是异或和相等的位置,也就是arr[k+1…i]的异或和为0。

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

//dp[i]:arr[0..i]最优划分异或和为0,有多少部分
int mostEor(vector<int>& arr) {
	if (arr.size() == 0) {
		return 0;
	}
	int len = arr.size();
	vector<int>dp(len);
	dp[0] = arr[0] == 0 ? 1 : 0;
	unordered_map<int, int>mp;//key:前缀异或和,value:下标
	mp.insert({ 0,-1 });
	int eor = arr[0];

	for (int i = 1; i < len; i++) {
		eor = (eor ^ arr[i]);
		if (mp.find(eor) != mp.end()) {
			dp[i] = mp[eor] == -1 ? 1 : dp[mp[eor]] + 1;
		}
		dp[i] = max(dp[i], dp[i - 1]);
		//mp.insert({ eor,i });//用insert方式插入具有相同key的pair会插入失败
		//数组的方式会覆盖之前的value值,所以这里只能用数组的方式,即之前不存在则插入,否则修改value
		mp[eor] = i;
	}
	return dp[len - 1];
}

题目三

在这里插入图片描述
相当于是两个动态规划。1 用普通币拼n,方法数为a,用纪念币拼m-n,方法数为b,则这种面值划分的方式下平成m的面值的方法数就是a*b;n的范围是0…m;问题转化为,用n1种面值的硬币,每种可以使用任意枚,拼成面值为x方法数是多少、用n2种面值的硬币,每种最多只能使用一枚,拼成面值为y的方法数是多少。

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

//dp[i][j]:使用arr[0..i]硬币拼成面值为j的方法数
//m:题目要求拼出的总的面额
vector<vector<int>>dp1(vector<int>& arr, int m) {
	int len = arr.size();
	vector<vector<int>>dp(len , vector<int>(m + 1));
	for (int i = 0; i < len; i++) {
		dp[i][0] = 1;
	}
	for (int j = 1; j < m + 1; j++) {
		//第0行,如果所要拼的面额是arr[0]的整数倍,存在一种方式
		dp[0][j] = (j % arr[0] == 0 ? 1 : 0);
	}
	for (int i = 1; i < len; i++) {
		for (int j = 1; j < m + 1; j++) {
			//斜率优化
			dp[i][j] = dp[i - 1][j] + (j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0);
		}
	}
	return dp;
}


//dp[i][j]:使用arr[0..i]硬币拼成面值为j的方法数
//m:题目要求拼出的总的面额
vector<vector<int>>dp2(vector<int>& arr, int m) {
	int len = arr.size();
	vector<vector<int>>dp(len, vector<int>(m + 1));
	for (int i = 0; i < len; i++) {
		dp[i][0] = 1;
	}
	for (int j = 1; j < m + 1; j++) {
		dp[0][j] = (j == arr[0] ? 1 : 0);
	}
	for (int i = 1; i < len; i++) {
		for (int j = 1; j < m + 1; j++) {
			dp[i][j] = dp[i - 1][j] + (j - arr[i] >= 0 ? dp[i - 1][j - arr[i]] : 0);
		}
	}
	return dp;
}
//以上还可以进行空间优化


int numWays(vector<int>& arr1, vector<int>& arr2, int m) {
	int res = 0;
	int len1 = arr1.size();
	int len2 = arr2.size();
	vector<vector<int>>d = dp1(arr1, m);
	vector<vector<int>>p = dp2(arr2, m);
	for (int i = 0; i <= m; i++) {
		res += d[len1-1][i] * p[len2-1][m - i];
	}
	return res;
}


题目四

两个排好序的数组A和B,怎么找到整体第k大的数,或者整体第k小的数?要求使用较少的比较次数。
方法一:外排,谁小谁移动,时间复杂度:O(k)
方法二:首先假设第k小的数在A中,在A中二分找到中间的数,可以知道他在A中是第几小的数,然后在B中二分,可以知道他在B中是第几小的数,两者求和,就是当前数总体是第几小的(假设是第i小的),如果i>k–>第k小的数在左部分,继续二分;如果在A中找不到–>在B中重复上述操作。时间复杂度:O(logN*logM)。

在这里插入图片描述
方法三:
算法原型:两个长度相同的有序数组,怎么找到他们的上中位数(上中位数是偶数长的有序数组中,下标小的那个中位数)?

  1. 两个数组长度都是偶数。如果两数组中点的值相等–>那么这两个相等的值就是他们的上中位数;如果两数组中点的值不相等,假设b>b’–>A数组中b右侧的数不可能是上中位数,B数组中b’及b’左侧的数不可能是上中位数–>出去不可能的区间,剩下部分求上中位数,就是整体的上中位数。总结来说对于两个数组长度都是偶数的情况下求上中位数就是,相等 直接返回,不等 将剩下区间递归求上中位数,子问题的上中位数就是总问题的上中位数

在这里插入图片描述
在这里插入图片描述
2) 两个数组长度都是奇数。如果两数组中点的值相等–>那么这两个相等的值就是他们的上中位数;如果两数组中点的值不相等,假设3>3’–>A数组中3及3右侧的数不可能是上中位数,B数组中3’左侧的数不可能是上中位数,剩下区间的长度不相等,不能直接递归,手动验3’是否为上中位数,如果是 直接返回,不是 去除之后,区间长度相等,递归求–>出去不可能的区间,剩下部分求上中位数,就是整体的上中位数。
上述过程的时间复杂度为:O(logN)
现在正式讨论题目本身:
假设A中有10个数,B中有17个数,要求第k小?

  1. 1<=k<=10(小于短数组的长度)–>A和B分别拿出k个数,然后求上中位数即为答案

在这里插入图片描述
2) 10<k<=17–>不妨设求第15小的数,A数组的全体都可能是,B数组中5’-15’有可能是第15小的数,但这时A和B可能的区间长度不相等,需要手动判断5’是否为第15小的数,剩下的区间再求上中位数
3) 17<k<=27–>不妨设求第23小的数。A数组的前5个元素不可能是第23小的数;B数组中前12个元素不可能是第23小的数;单独验证6和13’是否为第23小的元素;剩下的数求上中位数即为结果

在这里插入图片描述
时间复杂度:O(log(min(N,M)))
代码实现:

//寻找第k小的数
//1.外排:谁小谁移动

int minNum01(vector<int>& arr1, vector<int>& arr2,int k) {
	int m = arr1.size();
	int n = arr2.size();
	
	if (m + n < k) {
		return INT_MAX;//无效值
	}
	int res = 0;
	int p1 = 0;
	int p2 = 0;
	int i = 0;
	for (; i < k && p1 < m && p2 < n; i++) {
		if (arr1[p1] < arr2[p2]) {
			res = arr1[p1];
			p1++;
		}
		else {
			res = arr2[p2];
			p2++;
		}
	}
	if (p1 == m) {
		for (; i < k; i++) {
			res = arr2[p2++];
		}
	}
	if (p2 == n) {
		for (; i < k; i++) {
			res = arr1[p1++];
		}
	}
	return res;
}

//2.左神方法
//算法原型:求两个等长数组的上中位数
//l、r为arr1所求区间的左右边界
//x、y为arr2所求区间的左右边界
int getMedian(vector<int>& arr1, int l, int r, vector<int>& arr2, int x, int y) {
	int m1 = l + ((r - l) >> 1);
	int m2 = x + ((y - x) >> 1);
	if (arr1[m1] == arr2[m2]) {//中间值相等,直接返回,奇偶情况相同
		return arr1[m1];
	}
	if ((r - l + 1) % 2 == 0) {
		//长度为偶数时:
		//中间值小的上半区间中的元素可能是上中位数(不包含中间值)
		//中间值大的下半区间中的元素可能是上中位数(包含中间值)
		if (arr1[m1] > arr2[m2]) {
			return getMedian(arr1, l, m1, arr2, m2 + 1, y);
		}
		else {
			return getMedian(arr1, m1 + 1, r, arr2, x, m2);
		}
	}
	else {
		//长度为奇数时:
		//中间值小的上半区间中的元素可能是上中位数(包含中间值)
		//中间值大的下半区间中的元素可能是上中位数(不包含中间值)
		//所确定的区间长度不等,因此需要单独判断所包含的中间值是否为上中位数
		//如果是,直接返回;否则舍弃掉,区间长度相等,调用递归
		if (arr1[m1] > arr2[m2]) {
			if (arr2[m2] >= arr1[m1 - 1]) {
				return arr2[m2];
			}
			else {
				return getMedian(arr1, l, m1 - 1, arr2, m2 + 1, y);
			}
		}
		else {
			if (arr1[m1] >= arr2[m2 - 1]) {
				return arr1[m1];
			}
			else {
				return getMedian(arr1, m1 + 1, r, arr2, x, m2 - 1);
			}
		}
	}
}


int minNum02(vector<int>& arr1, vector<int>& arr2, int k) {
	
	//始终让arr1为长数组
	if (arr1.size() < arr2.size()) {
		arr1.swap(arr2);
	}
	int m = arr1.size();
	int n = arr2.size();

	if (m + n < k || k < 1) {
		return INT_MAX;//无效输入,返回系统最大值
	}

	if (k <= n) {
		//k小于等于短数组长度,直接求两数组前k元素的上中位数
		return getMedian(arr1, 0, k - 1, arr2, 0, k - 1);
	}
	else if (k > m) {
		//k大于长数组的长度:
		//arr1,k-n-1之前的位置均不可能是第k小,之后可能
		//arr2,k-m-1之前的位置均不可能是第k小,之后可能
		//这种情况需要先判断arr1[k-n-1]和arr2[k-m-1]是否为第k小
		//如果不判断,最后求得的是第k-1小,具体举例分析
		if (arr1[k - n - 1] >= arr2[n - 1]) {
			return arr1[k - n - 1];
		}
		if (arr2[k - m - 1] >= arr1[m - 1]) {
			return arr2[k - m - 1];
		}
		return getMedian(arr1, k - n, m - 1, arr2, k - m, n - 1);
	}
	else {
		//k>短数组长度,小于等于长数组长度
		//短数组的全体都有可能是第k小
		//长数组的k-n-1位置之前不可能是第k小,k-1之后也不能是第k小
		//长数组的区间比短数组的区间长1,因此先单独判定arr1的k-n-1位置是否为第k小
		//判断条件是,比短数组的最大值还要大
		//如果不是,舍弃之后调递归
		if (arr1[k - n - 1] >= arr2[n - 1]) {
			return arr1[k - n - 1];
		}
		return getMedian(arr1, k - n, k - 1, arr2, 0, n - 1);
	}

	return INT_MAX;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值