文章目录
- 1、合并区间
- 2、无重叠区间
- 3、用最少的箭引爆气球
- 4、整数替换
- 5、俄罗斯套娃信封
- 6、可被3整除的最大和
- 7、距离相等的条形码
- 8、重构字符串
1、合并区间
56. 合并区间
在合并区间时,得先排序一下,方便判断。方便可以按照左或者右端点排序。很多问题左右端点都可以排序,这里就用左端点排序。
用左端点排序,排好后的所有区间,可以发现能够合并的区间都会是连续的。合并本质上就是在求并集,有可能连续几个区间左端点数字都相同,但右端点不同,这部分就得用最大的右端点做新的右端点;左端点的不同,有3种情况:右端点小于,大于等于上一个右端点,以及左端点大于上一个右端点。
假设第一个区间左右端点是left和right,第二个区间左右端点是ab。如果两个区间有重叠部分(即使只有一个端点也是重叠),那么a <= right,新区间就是[left, max(right, b)];如果a > right,那就不重叠,不合并,更新left和right。
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end());
int left = intervals[0][0], right = intervals[0][1];
vector<vector<int>> res;
for(int i = 1; i < intervals.size(); ++i)
{
int a = intervals[i][0], b = intervals[i][1];
if(a <= right) right = max(right, b);
else
{
res.push_back({left, right});
left = a;
right = b;
}
}
//最后一个区间其实没加入
res.push_back({left, right});
return res;
}
2、无重叠区间
435. 无重叠区间
先看上一题。和上一题不同的是,这道题需要保留尽可能多的不重叠的区间,也就是移除最少的区间。
关于区间重叠的所有情况,画图就能很好地展现出来,这个也简单,就不放图了。对于左端点相同,右端点不同的,应当留小的那个区间,因为更长的区间就更有可能和其它区间相交,这种情况就是right > b,留b所在的区间。其它情况也是如此,我们要留下更小的区间。
a < right,留下min(right, b),等于的情况不需要考虑;a >= right,也就是出现了不相交的情况,那就用下一个区间的右端点继续去判断,也就是更新right为b。
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end());
int left = intervals[0][0], right = intervals[0][1];
int res = 0;
for(int i = 1; i < intervals.size(); ++i)
{
int a = intervals[i][0], b = intervals[i][1];
if(a < right)
{
res++;
right = min(right, b);
}
else right = b;
}
return res;
}
3、用最少的箭引爆气球
452. 用最少数量的箭引爆气球
先看合并区间和重叠区间这两道题。一支箭希望尽量引爆更多气球。根据之前两个题目,按照左端点排序后,如果求交集,会发现互相重叠的区间是连续的,如果求并集,会发现能够合并的区间是连续的。交集出现的这个性质其实是比较强的,三个区间可以合并,但有可能第三个区间和第一个区间无法合并,它们需要通过第二个区间才能三个全部合并,而重叠就无法这样。
对于这道题来说,我们要找的应当是重叠的区间,也就是右端点小于下一个左端点。我们可以求出两个区间的交集,让这个交集去和第三个区间比较,只要都能重叠,那就可以继续求交集,比较下去,交集会逐渐变小。
当a <= right时,交集区间是[max(left, a), min(right, b)];当遇到不重叠的时候,就得另发一支箭了。
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end());
int right = points[0][1];
int res = 1;
for(int i = 1; i < points.size(); ++i)
{
int a = points[i][0], b = points[i][1];
if(a <= right) right = min(right, b);
else
{
res++;
right = b;
}
}
return res;
}
4、整数替换
397. 整数替换
先来看一些二进制知识。
如果是偶整数,二进制表示中最后一位是0;如果是奇整数,二进制表示中最后一位是1;除2操作就是二进制表示中统一右移一位。
在这个题目中,如果是偶数,那就只能除2;如果是奇数,那就有两个选择,+1或-1,我们得判断走哪步更好。
二进制位的最后两位,可能是01,可能是11。对于01,+1变成10,-1变成00,这时候都可以除2,假设原二进制位前面几个是0,那么这时候10和00都除2的话,就变成了01和00,这时候明显00更好,它可以除2,而01只能继续做+1或者-1,所以对于01这个的二进制末二位,-1再除2更好。
对于…11,+1变成…00,-1变成10,这时候也都可以除2,但实际上00除2更好,这个情况可以自己列举。而如果数字是3,二进制表示就是00000011,那么-1再除2就好了。
对于01或11的判断,用数字 % 4就好。
int integerReplacement(int n) {
int res = 0;
while(n > 1)
{
if(n % 2 == 0)
{
n /= 2;
res++;
}
else
{
if(n == 3)
{
res += 2;
n = 1;
}
else if(n % 4 == 1)
{
res+= 2;
n /= 2;
}
else
{
n = n / 2 + 1;
res += 2;
}
}
}
return res;
}
5、俄罗斯套娃信封
354. 俄罗斯套娃信封问题
这道题最终会变成最长递增子序列的问题,用到这个问题的动规、贪心 + 二分思路,所以事先知道会更好,这道题也会在此基础上来写。我的博客动规5,贪心2中有最长递增子序列。
常规解法,虽然这道题会超时,但思路有用。和上面几个题一样,先按照左端点排序就不用去比较右边的区间了,只需要去左边比较右端点了。当排完序后,这其实就像一个最长递增子序列,只要后面的能套前面的,就可以连在一起,就是一个序列的。用动规思路。
状态表示:dp[i]是以i位置的信封为结尾的所有的套娃序列中,最长的序列的长度。
确定状态转移方程:0~i - 1的所有位置中,找到某个区间j,只要i能套j,那么就是dp[j] + 1。所以方程就是max(dp[j] + 1),需要的最大长度,条件则是0 <= j < i,e[i][0] > e[j][0],e[i][1] > e[j][1]。
初始化:dp表全初始化为1,因为1个区间可以套娃自己。
返回值:dp表里最大值。
int maxEnvelopes(vector<vector<int>>& e) {
//动规
sort(e.begin(), e.end());
int n = e.size();
vector<int> dp(n, 1);
int res = 1;
for(int i = 1; i < n; ++i)
{
for(int j = 0; j < i; ++j)
{
if(e[i][0] > e[j][0] && e[i][1] > e[j][1])
dp[i] = max(dp[j] + 1, dp[i]);
}
res = max(res, dp[i]);
}
return res;
}
适用的解法是重写排序 + 贪心。因为只是左端点排序后就用贪心,会需要考虑很多细节。
左端点排完序后,可能会出现几个区间左端点都相同,而右端点不同,这时候要考虑的就是右端点的顺序,如果右端点此时从大到小排序,就可以尽量地套娃下去;当左端点不同的时候,左端点从小到大排序,就不需要再排右端点。
int maxEnvelopes(vector<vector<int>>& e)
{
//贪心
sort(e.begin(), e.end(), [&](const vector<int>& v1, const vector<int>& v2)
{
return v1[0] != v2[0] ? v1[0] < v2[0] : v1[1] > v2[1];
});
vector<int> res;
res.push_back(e[0][1]);
for(int i = 1; i < e.size(); ++i)
{
int b = e[i][1];
if(b > res.back())
{
res.push_back(b);
}
else
{
int left = 0, right = res.size() - 1;
while(left < right)
{
int mid = (left + right) / 2;
if(res[mid] >= b) right = mid;
else left = mid + 1;
}
res[left] = b;
}
}
return res.size();
}
6、可被3整除的最大和
1262. 可被三整除的最大和
既然要得到最大的数,不妨在最一开始把所有数都加起来,模3如果等于0的话就直接返回这个数。
模3的结果还有2种情况,也就是余数1和2的情况,假设x是模3余数为1的数,y是余数为2的数。如果出现了% 3 == 1的情况,那么去掉这个x就没问题了,此时要么剩余的数的和是3的倍数,要么中间有几个有余数的数,但它们加起来就是3的倍数了,最终抵消了。所有整体可以看作去掉余数为1的这个x,其余数的和就是3的倍数。这种情况,要删的就是余数为1的这个数。
除此之外,余数为1的情况,不只是有一个x,可能有两个y,也就是两个余数为2的,加起来就是余数为4,那么% 3的话也是1,也还有更多的y,但和上面一样,最终都是2个y的情况,那么为了消除这个余数为1的情况,把最小的和次小的y去除就好。
余数为1的情况,综上所述,取两个情况的最大值,max(sum - x1, sum - y1 - y2)。
余数为2的情况。要么是一个y,要么是2个x。根据上面的思路,要最小的y,或最小和次小的两个x,max(sum - y1, sum - x1 - x2)。
求一堆数的最小值和次小值,可以用sort,这样最快是nlogn的时间复杂度,可以O(n)来解决,用分类讨论的方法。先选定两个数,每当新来一个数,就去和这两个数比较,一次次更新,最终得到的就是最小和次小的。如果x < x1,则x2 = x1, x1 = x;如果x1 <= x <= x2,则x2 = x。
int maxSumDivThree(vector<int>& nums) {
const int INF = 0x3f3f3f3f;
int sum = 0, x1 = INF, x2 = INF, y1 = INF, y2 = INF;
for(auto x : nums)
{
sum += x;
if(x % 3 == 1)
{
if(x < x1)
{
x2 = x1;
x1 = x;
}
else if(x < x2) x2 = x;
}
else if(x % 3 == 2)
{
if(x < y1)
{
y2 = y1;
y1 = x;
}
else if(x < y2) y2 = x;
}
}
if(sum % 3 == 0) return sum;
else if(sum % 3 == 1) return max(sum - x1, sum - y1 - y2);
else return max(sum - y1, sum - x1 - x2);
}
7、距离相等的条形码
1054. 距离相等的条形码
把所有相同的数看成一个块,一个数组有好几个块,每个块的数字都隔一个位置来放置,这样就可以有效防止出现相邻数字相同。但这样还有问题,如果是122这样的数字,1放在第一个位置,后面没有1,再看2,两个2会放在相邻位置,所以要先处理出现次数最多的数,这样结果就是212,2先隔一个位置放置,再放置1。剩下的数的处理顺序无所谓,都按照隔一个位置放置的规则就行,每次只处理一批相同的数字。
vector<int> rearrangeBarcodes(vector<int>& b) {
unordered_map<int, int> hash;//统计每个数出现的频次
int maxVal = 0, maxCount = 0;//出现次数最多的数和最多的次数
for(auto x : b)
{
if(maxCount < ++hash[x])//统计一下频次再和maxCount比较
{
maxCount = hash[x];
maxVal = x;
}
}
int n = b.size();
vector<int> res(n);
int index = 0;
for(int i = 0; i < maxCount; i++)
{
res[index] = maxVal;
index += 2;
}
hash.erase(maxVal);
for(auto& [x, y] : hash)//这样是把x作为哈希表的key,y作为哈希表的value
{
for(int i = 0; i < y; i++)
{
if(index >= n) index = 1;
res[index] = x;
index += 2;
}
}
return res;
}
8、重构字符串
767. 重构字符串
和条形码基本一样的思路。在处理之前,如果次数最多的那个 > (n + 1) / 2,那么一定无法重排,就像例2。
string reorganizeString(string s)
{
int hash[26] = {0};
char maxChar = ' ';
int maxCount = 0;
for(auto ch : s)
{
if(maxCount < ++hash[ch - 'a'])
{
maxChar = ch;
maxCount = hash[ch - 'a'];
}
}
int n = s.size();
if(maxCount > (n + 1) / 2) return "";
string res(n, ' ');
int index = 0;
for(int i = 0; i < maxCount; i++)
{
res[index] = maxChar;
index += 2;
}
hash[maxChar - 'a'] = 0;
for(int i = 0; i < 26; i++)
{
for(int j = 0; j < hash[i]; j++)
{
if(index >= n) index = 1;
res[index] = 'a' + i;
index += 2;
}
}
return res;
}
结束。