1837. K 进制表示下的各位数字总和
知识点:进制转换
将十进制数字转换为K进制数字,只需不断进行两种操作 —— 取余和除K,直至被转换数字变为0。
class Solution {
public:
int sumBase(int n, int k) {
int sum = 0;
while(n > 0) {
sum += n % k;
n /= k;
}
return sum;
}
};
1838. 最高频元素的频数
知识点:双指针
为了方便操作,我们先将 nums 升序排列。
首先明确一个事情,一定存在i使得以nums[i] 为目标值,可以获得最大频次。以下是证明过程:
- 不妨先设
x为可获得最大频次的目标值,且不在nums中。 - 因为每次操作只能加不能减,所以x必然是由小于 x 的某些数字变换得到的。
- 不妨设这些数字为集合
S,其中最大的数字为max。 - 因为每次只能加一,所以
S中除max之外的其他数字,在变为x之前,必然先变为max。 - 所以以
max为目标值也能得到与x相同的频次。
继续观察,当以nums[i]为目标值时,必然是将不超过nums[i]的,位于连续区间nums[j..i]中的数字转换为nums[i]是最优策略。
左指针j,右值针i 分别指向nums的某个位置且 ,其含义为将nums[j..i]这段区间内的元素都变为nums[i]。
假设已知,以nums[i]为目标值时的最高频次 以及 达到该频次的最小操作次数 。依次为基础可较快的计算出 以及 :
- 移动右指针
i:将 个nums[i]全部修改为nums[i+1],则 ,,。 - 移动左指针
j:如果,则抛弃最左边的元素,直到 。 , ,
初始状态,。
因为每个元素只会被左右指针各访问一次,所以整体的时间复杂度为。
class Solution {
public:
int maxFrequency(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int l = 0, r = 0;
int anw = 1;
int64_t sum = 0;
while(r+1 < nums.size()) {
r++;
sum += (nums[r] - nums[r-1])*1L*(r-l);
while(sum > k) {
sum -= nums[r]-nums[l];
l++;
}
anw = max(anw,r-l+1);
}
return anw;
}
};
1839. 所有元音按顺序排布的最长子字符串
知识点:递推
设 dp[i] 代表以 word[i] 结尾的,符合题目要求的前缀的长度。
前缀的定义是:
- 前缀是word的一个子串。
- 该子串是有
a,e,i,o,u中的第一种或者前几种组成的。 - 该子串是以
a开头的。 - 该子串中元素都是升序排列的。
状态转移过程见注释:
class Solution {
public:
bool check(char a, char b) {
if (a == 'a' && b == 'e') return true;
if (a == 'e' && b == 'i') return true;
if (a == 'i' && b == 'o') return true;
if (a == 'o' && b == 'u') return true;
return false;
}
int longestBeautifulSubstring(string word) {
// 因为 dp[i] 的计算仅依赖 dp[i-1]。
// 所以使用 cur 指代 dp[i],pre 指代 dp[i-1]。这样可以节省内存~
// 初始化 pre,即dp[0]
int pre = (word[0] == 'a' ? 1 : 0), cur = 0;
int anw = 0;
for (int i = 1; i < word.size(); i++) {
// 如果 dp[i-1] 是一个前缀,且 word[i] 与 word[i-1]的字典序是连续的
if (pre > 0 && (word[i] == word[i-1] || check(word[i-1], word[i]))) {
cur = pre+1;
} else {
cur = (word[i] == 'a' ? 1 : 0);
}
// 如果word[i] 为 'u',则说明该前缀也是一个合法子串,更新一下anw
if (word[i] == 'u') {
anw = max(anw, cur);
}
pre = cur;
}
return anw;
}
};
1840. 最高建筑高度
知识点:几何
首先,我们将restrictions转换为平面直角坐标系上的点,建筑编号为横坐标,高度限制为纵坐标。然后过每个点做两条斜率分别为 1 和 -1 的直线
特别的,为了便于处理,如果restrictions中不包含n号建筑的高度限制,那么我们将<n,n-1>添加到restrictions中。
如上图所示,相邻的点对把[0,n]这个大区间分割为若干个一段小区间。如果我们能找到每段小区间中最优解,那么就可以从这些局部最优解中找到全局最优解。
接下来,对于每个小区间,通过小数操作找出最优解:
- 找直线:
- 找出过其左端点以及左端点左边那些点的,斜率为 1 的直线中,截距最小的直线,记为 L。
- 同样的,找出过其右端点以及右端点右边那些点的,斜率为 -1 的直线中,截距最小的直线,记为 R。
- 求交点:求出 L 和 R 的交点,记为 P。
- 计算局部最优解:
- 如果 P 的横坐标在该小区间内,则P的纵坐标记为该小区间的最优解。
- 如果 P 在该小区间的左侧,则将左端点横坐标代入L和R,其中较小的纵坐标即为最优解。
- 如果 P 在该小区间的有责,则将右端点横坐标代入L和R,其中较小的纵坐标即为最优解。
在所有的局部最优解中,寻找最大值,即为全局最优解。
为何只需关心左边的斜率为1的直线,以及右边斜率为-1的直线呢?因为这些直线决定了高度的上限,一旦超过该高度,就无法在指定位置下降到指定高度。剩下的一半直线并不没有该意义。
int64_t l[100001], r[100001];
class Solution {
public:
int maxBuilding(int n, vector<vector<int>>& limit) {
if (limit.empty()) {
return n-1;
}
sort(limit.begin(), limit.end(), [](auto &lhs, auto &rhs) { return lhs[0] < rhs[0];});
if (limit.back().at(0) != n) {
limit.emplace_back(vector<int>{n, n-1});
}
int top = -1;
for (int i = 0; i < limit.size(); i++) {
l[i] = top;
top = min(top, limit[i][1]-limit[i][0]);
}
top = numeric_limits<int>::max();
for (int i = limit.size()-1; i >= 0; i--) {
top = min(top, limit[i][1]+limit[i][0]);
r[i] = top;
}
int64_t anw = 0;
for (int i = 0; i < limit.size(); i++) {
int64_t y = (l[i]+r[i])/2;
int64_t x = (r[i]-l[i])/2;
int64_t lx = i == 0 ? 0 : limit[i-1][0];
int64_t rx = limit[i][0];
if (x < lx) {
anw = max(anw, min(-lx + r[i], lx + l[i]));
} else if (x > rx) {
anw = max(anw, min(-rx + r[i], rx + l[i]));
} else {
anw = max(anw, y);
}
}
return anw;
}
};