周赛地址:​​https://leetcode-cn.com/contest/weekly-contest-238​

第一题:K 进制表示下的各位数字总和

一道进制转换的题目,对于Java语言有两种做法:调用

LeetCode第238场周赛_Math

函数;取余求和。

class Solution {
public int sumBase(int n, int k) {
int sum = 0;
// toString(n, k);可以将n转换成k进制的字符串形式
for (char c : Integer.toString(n, k).toCharArray()) {
sum += c - '0';
}
return sum;
}
}

class Solution {
public int sumBase(int n, int k) {
int result = 0;
while (n != 0) {
// 对k取余求和
result += n % k;
n /= k;
}
return result;
}
}

第二题:最高频元素的频数

思考朴素做法,可知时间复杂度是

LeetCode第238场周赛_Math_02

的,所以需要优化。

显然,频次最高的数字一定是原数组中的数,否则,就有更优解。如果存在一个解

LeetCode第238场周赛_Math_03

不属于原数组,那么,原数组中,比

LeetCode第238场周赛_Math_03

小的数字一定也满足最高频元素,此时需要的代价少于将解化为

LeetCode第238场周赛_Math_03

的代价,所以,解一定来自原数组中的数。

假设答案是

LeetCode第238场周赛_Math_03

,优先操作距离

LeetCode第238场周赛_Math_03

最近的数字可以获得最优解。

既然最终解位于原数组,所以遍历原数组,对于每个数字

LeetCode第238场周赛_Math_03

,把总的代价

LeetCode第238场周赛_数组_09

,按照由远及近的方向,分配到前面的数字里,使前面的数字变为

LeetCode第238场周赛_Math_03

,求得最远可以触及到的下标。

一种是前缀和+二分,一种是滑动窗口。

class Solution {
public int maxFrequency(int[] nums, int k) {
Arrays.sort(nums);
int length = nums.length, answer = 0;
long[] sum = new long[length + 1];// 前缀和
for (int i = 0; i < length; i++) {
// sum[i]:前i个数的和
sum[i + 1] = sum[i] + nums[i];
}
// 假设频次最高的数字是nums[i],使用代价k最多可以让多少个数字变成nums[i]
for (int i = 0; i < length; i++) {
int left = binarySearch(sum, k, nums, i);
answer = Math.max(answer, i - left + 1);
}
return answer;
}

/**
* @param sum 前缀和
* @param k 操作次数的上限
* @param nums 原数组
* @param aimIndex 频次最高数字的下标
* @return 二分查找满足条件的左边界
*/
private int binarySearch(long[] sum, int k, int[] nums, int aimIndex) {
int l = 0, r = aimIndex;
while (l <= r) {
int mid = (l + r) / 2;
// 把nums数组中[mid, aimIndex]区间的值都变成nums[aimIndex]所需要的代价,利用前缀和求解
long cost = (long) (aimIndex - mid + 1) * nums[aimIndex] - (sum[aimIndex + 1] - sum[mid]);
if (cost < k) {
r = mid - 1;
} else if (cost > k) {
l = mid + 1;
} else {
return mid;
}
}
return l;
}
}
class Solution {
public int maxFrequency(int[] nums, int k) {
Arrays.sort(nums);
int length = nums.length, answer = 0, l = 0, r = 0;
long sum = 0;
while (r < length) {
sum += nums[r];
// 把nums的[l,r]区间都变成nums[r]所需要的代价超过k,收缩左边界
while ((long) (r - l + 1) * nums[r] - sum > k) {
sum -= nums[l++];
}
answer = Math.max(answer, r - l + 1);
r++;
}
return answer;
}
}

第三题:所有元音按顺序排布的最长子字符串

遍历字符串,同时对比当前字符和前一个字符,考虑这几种情形:

如果当前字符等于前一个字符,下标后移;

如果当前字符大于前一个字符,kind++,kind的含义是当前遇到了几种字符;

如果当前字符小于前一个字符,重新开始计算kind,left归为为当前位置。

在遍历的过程中,如果出现

LeetCode第238场周赛_数组_11

,那么,就记录一次

LeetCode第238场周赛_前缀和_12

,并维护这个值的最大值。

class Solution {
public int longestBeautifulSubstring(String word) {
int length = word.length(), left = 0, maxLength = 0, kind = 1;
for (int i = 1; i < length; i++) {
char previous = word.charAt(i - 1), current = word.charAt(i);
if (current > previous) {
kind++;
} else if (current < previous) {
kind = 1;
left = i;
}
if (kind == 5) {
maxLength = Math.max(maxLength, i - left + 1);
}
}
return maxLength;
}
}

第四题:最高建筑高度

题目有两种做法:约束传递;差分约束。

已知

LeetCode第238场周赛_前缀和_13


LeetCode第238场周赛_前缀和_14

数量级的,即使是

LeetCode第238场周赛_Math_15

也会超时。观察到restrictions是

LeetCode第238场周赛_前缀和_16

数量级,而且某个建筑的高度可能根本达不到

LeetCode第238场周赛_Math_17

,可以从

LeetCode第238场周赛_前缀和_18

考虑,对于每两个相邻的

LeetCode第238场周赛_前缀和_18

之间的建筑,我们只关心这两个相邻的

LeetCode第238场周赛_前缀和_18

之间的最大值,无需把这两个相邻的

LeetCode第238场周赛_前缀和_18

之间的所有建筑高度都求解出来。

只观察这些

LeetCode第238场周赛_前缀和_18

,假设

LeetCode第238场周赛_数组_23


LeetCode第238场周赛_前缀和_24

是相邻的两个

LeetCode第238场周赛_前缀和_18

的下标,且

LeetCode第238场周赛_Math_26

,根据题意,需要满足高度差≤距离差,即:

LeetCode第238场周赛_Math_27


在从左向右看的时候,右侧的建筑高度受到左侧相邻建筑高度的限制;

在从右向左看的时候,左侧的建筑高度受到右侧相邻建筑高度的限制。

因此,需要跑两遍,将当前可以确定的约束关系传递给下一个建筑,用以确定下一个建筑的实际限制高度,通过左右限制,确定建筑物实际的限制高度。当相邻的两个实际限制高度已经确定了之后,就可以利用相邻限高建筑高度和距离求得两相邻限制之间的最高值了,再遍历这些最高值,选择最大值即可。

class Solution {
public static int maxBuilding(int n, int[][] restrictions) {
int length = restrictions.length, newLength;
if (length == 0) {// 所有建筑的高度都没有限制
return n - 1;
}
// 按照id升序排序
Arrays.sort(restrictions, Comparator.comparingInt(r -> r[0]));
if (restrictions[length - 1][0] != n) {// 需要加上{n, n - 1}限制和{1, 0}限制
newLength = length + 2;
} else {// 需要加上{1, 0}限制
newLength = length + 1;
}
int[][] r = new int[newLength][2];
// 将{1, 0}加到约束数组中
r[0][0] = 1;
r[0][1] = 0;
// 将约束转移到新数组中
for (int i = 0; i < length; i++) {
r[i + 1][0] = restrictions[i][0];
r[i + 1][1] = restrictions[i][1];
}
// 如果最后一个约束建筑的编号不是n,将{n, n - 1}加到约束数组中
if (restrictions[length - 1][0] != n) {
r[newLength - 1][0] = n;
r[newLength - 1][1] = n - 1;
}
// 从左到右传递约束,修改真实约束高度
for (int i = 1; i < newLength; i++) {
r[i][1] = Math.min(r[i][1], r[i - 1][1] + r[i][0] - r[i - 1][0]);
}
// 从右向左传递约束,修改真实约束高度
for (int i = newLength - 2; i >= 0; i--) {
r[i][1] = Math.min(r[i][1], r[i + 1][1] + r[i + 1][0] - r[i][0]);
}
int answer = 0;
// 这maxLength个高度约束,将n分隔成maxLength - 1个区间,分别求出每个区间内的最大值
for (int i = 1; i < newLength; i++) {
answer = Math.max(answer, (r[i][0] - r[i - 1][0] + r[i][1] + r[i - 1][1]) / 2);
}
return answer;
}
}

根据题意可知,相邻的两个

LeetCode第238场周赛_前缀和_18

,需要满足:高度差≤距离差,可以列出如下差分约束方程:

LeetCode第238场周赛_前缀和_29


根据差分约束系统的规则,如果存在

LeetCode第238场周赛_前缀和_30

,移项得

LeetCode第238场周赛_前缀和_31

,那么,就建立一条

LeetCode第238场周赛_前缀和_24


LeetCode第238场周赛_数组_23

权值为

LeetCode第238场周赛_数组_34

的有向边。可知,当跑完最短路算法后,图中的元素满足

LeetCode第238场周赛_前缀和_31

,因为

LeetCode第238场周赛_前缀和_36

的边都被松弛了。因此利用差分约束,跑最短路求解出建筑物实际的限制高度,后面的结果计算就一样了。

class Solution {
int[][] r;
int[] distance;
boolean[] visit;
PriorityQueue<int[]> priorityQueue;
Map<Integer, List<int[]>> graph;

public int maxBuilding(int n, int[][] restrictions) {
int length = restrictions.length, newLength, answer = 0;
if (length == 0) {
return n - 1;
}
Arrays.sort(restrictions, Comparator.comparingInt(r -> r[0]));
if (restrictions[length - 1][0] == n) {
newLength = length + 1;
} else {
newLength = length + 2;
}
init(newLength);
r[0] = new int[]{1, 0};
for (int i = 0; i < length; i++) {
r[i + 1][0] = restrictions[i][0];
r[i + 1][1] = restrictions[i][1];
}
if (restrictions[length - 1][0] != n) {
r[newLength - 1] = new int[]{n, n - 1};
}
// 设相邻的两个限高建筑编号为i和j且i < j,限高为r[i]和r[j],应满足r[i] - r[j] ≤ j - i && r[j] - r[i] ≤ j - i
for (int i = 1; i < newLength; i++) {
// 建边的时候,用下标建,不用编号,因为编号可能到1e9,开不了这么大的数组
add(i, i - 1, r[i][0] - r[i - 1][0]);
add(i - 1, i, r[i][0] - r[i - 1][0]);
}
dijkstra(newLength);
for (int i = 1; i < newLength; i++) {
answer = Math.max(answer, (r[i][0] - r[i - 1][0] + distance[i] + distance[i - 1]) / 2);
}
return answer;
}

private void init(int newLength) {
r = new int[newLength][2];
distance = new int[newLength];
visit = new boolean[newLength];
priorityQueue = new PriorityQueue<>(Comparator.comparingInt(r -> r[1]));
graph = new HashMap<>();
}

private void add(int u, int v, int w) {
graph.computeIfAbsent(u, k -> new ArrayList<>()).add(new int[]{v, w});
}

/**
* 写法和常规的dijkstra有不同
* 常规的写法是指定一个起始节点,从这个结点开始做dijkstra
* 这里没有指定起始节点,而是根据distance[i]从小到大的顺序做dijkstra,这样可以将过高的限高条件压低到实际的限高条件
*/
private void dijkstra(int newLength) {
// 将所有限高都放入优先队列
for (int i = 0; i < newLength; i++) {
distance[i] = r[i][1];// distance[i] = r[i][1] - r[0][1];因为r[0][1] == 0,所以写法上忽略了r[0][1]
priorityQueue.offer(new int[]{i, distance[i]});
}
int[] temp;
int v;
// 从限高小的建筑开始求最短路进行松弛,这样可以把过大的限高给降低到实际的限高
// 比如:i = 1,j = 3,r[i] = 2,r[j] = 100,对于r[j]来说,100是个没有约束力的条件,因为r[j]最大可以取到4,始终是小于100的,所以这里要更新r[j]的值为它实际的限高
while (!priorityQueue.isEmpty()) {
temp = priorityQueue.poll();
v = temp[0];
if (!visit[v]) {
visit[v] = true;
for (int[] edge : graph.getOrDefault(v, new ArrayList<>())) {
if (!visit[edge[0]] && distance[v] + edge[1] < distance[edge[0]]) {
distance[edge[0]] = distance[v] + edge[1];
priorityQueue.offer(new int[]{edge[0], distance[edge[0]]});
}
}
}
}
}
}