题目一
舍弃可能性的技巧
非基于比较的排序都是根据具体的数据状况来进行的,显然这题不适合;基于比较的排序的最好时间复杂度为: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)。
方法三:
算法原型:两个长度相同的有序数组,怎么找到他们的上中位数(上中位数是偶数长的有序数组中,下标小的那个中位数)?
- 两个数组长度都是偶数。如果两数组中点的值相等–>那么这两个相等的值就是他们的上中位数;如果两数组中点的值不相等,假设b>b’–>A数组中b右侧的数不可能是上中位数,B数组中b’及b’左侧的数不可能是上中位数–>出去不可能的区间,剩下部分求上中位数,就是整体的上中位数。总结来说对于两个数组长度都是偶数的情况下求上中位数就是,相等 直接返回,不等 将剩下区间递归求上中位数,子问题的上中位数就是总问题的上中位数
2) 两个数组长度都是奇数。如果两数组中点的值相等–>那么这两个相等的值就是他们的上中位数;如果两数组中点的值不相等,假设3>3’–>A数组中3及3右侧的数不可能是上中位数,B数组中3’左侧的数不可能是上中位数,剩下区间的长度不相等,不能直接递归,手动验3’是否为上中位数,如果是 直接返回,不是 去除之后,区间长度相等,递归求–>出去不可能的区间,剩下部分求上中位数,就是整体的上中位数。
上述过程的时间复杂度为:O(logN)
现在正式讨论题目本身:
假设A中有10个数,B中有17个数,要求第k小?
- 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;
}