周赛地址:https://leetcode-cn.com/contest/weekly-contest-233/
题目要求连续上升子数组的和最大,进行遍历,对比当前元素和前一个元素的大小关系。
class Solution {
public int maxAscendingSum(int[] nums) {
int max = nums[0];
int current = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] > nums[i - 1]) {
current += nums[i];
max = Math.max(max, current);
} else {
current = nums[i];
}
}
return max;
}
}
首先把题目看懂,题目描述有点长,耐心看完,会发现题目思路并不难。
如果订单是buy,先检查sell的队列里,有没有可以合并的,合并条件是:sell的最低价格≤当前buy的价格。
如果订单是sell,先检查buy的队列里,有没有可以合并的,合并条件是:buy的最高价格≥当前sell的价格。
于是sell用小顶堆,buy用大顶堆,Java里就用优先队列即可,并告知优先队列的排序规则。
订单的数量最大是 1 0 9 10^{9} 109,所以不可能能把每个订单放进去,而是放一个对象进去,所以也就有了Order类,这里用length=2的数组更节省空间。
每次合并的时候,考虑count的比较和处理。
class Solution {
public int getNumberOfBacklogOrders(int[][] orders) {
PriorityQueue<Order> buyPriorityQueue = new PriorityQueue<>(Comparator.comparingInt(order -> -order.price));
PriorityQueue<Order> sellPriorityQueue = new PriorityQueue<>(Comparator.comparingInt(order -> order.price));
for (int[] order : orders) {
if (order[2] == 0) {
// buy
while (!sellPriorityQueue.isEmpty()) {
Order sell = sellPriorityQueue.poll();
if (sell.price <= order[0]) {
if (sell.count > order[1]) {
sell.count -= order[1];
order[1] = 0;
sellPriorityQueue.offer(sell);
break;
} else {
order[1] -= sell.count;
}
} else {
sellPriorityQueue.offer(sell);
break;
}
}
if (order[1] > 0) {
buyPriorityQueue.offer(new Order(order[0], order[1]));
}
} else {
// sell
while (!buyPriorityQueue.isEmpty()) {
Order buy = buyPriorityQueue.poll();
if (buy.price >= order[0]) {
if (buy.count > order[1]) {
buy.count -= order[1];
order[1] = 0;
buyPriorityQueue.offer(buy);
break;
} else {
order[1] -= buy.count;
}
} else {
buyPriorityQueue.offer(buy);
break;
}
}
if (order[1] > 0) {
sellPriorityQueue.offer(new Order(order[0], order[1]));
}
}
}
long result = 0;
while (!buyPriorityQueue.isEmpty()) {
result += buyPriorityQueue.poll().count;
}
while (!sellPriorityQueue.isEmpty()) {
result += sellPriorityQueue.poll().count;
}
return (int) (result % 1000000007);
}
static class Order {
int price, count;
public Order(int price, int count) {
this.price = price;
this.count = count;
}
}
}
先理解题目描述的条件:
观察题目给的例子,将数值想象成高度,那么,整个数组画出来就是一个小山的样子,index位置是小山最高点。两侧依次递减如果递减到1,不继续递减,保持1直到数组的两端,当然,也可以还没减到1就碰到数组的边界了。所以这里计算的时候,要注意边界的取值范围。
有了这个直观的视觉感受,就考虑求和判断了,求和就是等差数列前n项和公式: S = ( a 1 + a n ) × n 2 S=\frac{(a_{1}+a_{n}) \times n}{2} S=2(a1+an)×n,能用公式就用公式,可别一个一个加去,太浪费时间了。
这里再次强调一下爆int的问题,两个int相乘,结果是int,结果可能会爆int,此时就出错了,在用long接收int相乘的结果时,先把int强转成long再进行乘法运算,因为这个wa了好几次。
设nums[index]的值是x,x的最小值是maxSum/n,最大值是maxSum,因为要求nums[index]的最大值,相当于二分法求满足条件的右边界。
class Solution {
public int maxValue(int n, int index, int maxSum) {
int left = maxSum / n, right = maxSum;
while (left <= right) {
int mid = left + (right - left) / 2;
long sum = getSum(index, n, mid);
if (sum > maxSum) {
right = mid - 1;
} else if (sum < maxSum) {
left = mid + 1;
} else {
left = mid + 1;
}
}
// 题目没有提及无解的情况,实际这个if是不会走的
if (right == -1 || getSum(index, n, right) > maxSum) {
return -1;
}
return right;
}
// 计算a[index]=value的时候,整个数组的和
private long getSum(int index, int length, int value) {
long sum = 0;
// a0的意思是a[0]的值,an_1的意思是a[n-1]的值,先按照-1的递减规则,减到数组的边界
long a0 = value - index, an_1 = value - (length - index - 1);
// 判断边界值分类计算和
if (a0 >= 1) {
sum += (a0 + value) * (index + 1) / 2;
} else {
sum += (long) (1 + value) * value / 2 + (index - value + 1);
}
if (an_1 >= 1) {
sum += (an_1 + value) * (length - index) / 2;
} else {
sum += (long) (1 + value) * value / 2 + (length - index - value);
}
return sum - value;// 在计算左右侧的和时,value会被计算两次,所以需要减去一次
}
}
题目的标签是字典树。
先说下朴素做法,分别取出 i i i和 j j j,满足 0 ≤ i < j ≤ n u m s . l e n g t h 0≤i
看下数据范围, 1 ≤ n u m s . l e n g t h ≤ 2 × 1 0 4 1≤nums.length≤2 \times 10^{4} 1≤nums.length≤2×104,朴素做法是 O ( n 2 ) O(n^{2}) O(n2)的复杂度,会超时。
那么,就需要找一种方案,加快统计速度。
考虑一个数 a = 3 , m a x = 5 a=3,max=5 a=3,max=5,试判断另一个数 b b b是否满足 a ⨁ b ≤ 5 a \bigoplus b ≤ 5 a⨁b≤5。将 3 3 3和 5 5 5表示成二进制的形式(这里为了方便表述,只写出来低位的3位): 3 10 = 01 1 2 , 5 10 = 10 1 2 3_{10}=011_{2},5_{10}=101_{2} 310=0112,510=1012。根据再十进制中比较数字大小的原则:两数字位数相同的时候,从高位开始比较,如果高位就能决定出来大小,那么是没有必要比较低位的,同理,在二进制中表示也是如此。
我们考虑 b b b的二进制形式:
这个的比较过程,就是在字典树上进行查询的一个过程。考虑上面的情况1,如果某一位已经足够确定大小关系了,那么低位取什么都无所谓了,也就不用计算了,直接取这个结点的某个属性值即可,这个属性值是:该结点下有几个叶子节点的统计量。
此外,还要介绍一个思路,我们令 q u e r y ( v a l u e , m a x ) query(value, max) query(value,max)表示在字典树上每个元素异或 v a l u e 的 结 果 a n s w e r value的结果answer value的结果answer,满足 0 ≤ a n s w e r ≤ m a x 0≤answer≤max 0≤answer≤max的元素个数。题目求解的 a n s w e r ∈ [ l o w , h i g h ] answer∈[low, high] answer∈[low,high]的数量,我们可以求 q u e r y ( v a l u e , m a x ) query(value, max) query(value,max)和 q u e r y ( v a l u e , m i n − 1 ) query(value, min - 1) query(value,min−1),最终结果就是 q u e r y ( v a l u e , m a x ) − q u e r y ( v a l u e , m i n − 1 ) query(value, max)-query(value, min - 1) query(value,max)−query(value,min−1)。
整个代码流程分为两步:对于数组中的每个数字,在字典树上查询value,插入value。先查询后插入,可以保证查询value的时候,字典树上都是value之前的数字,保证了 i i i和 j j j的位置关系。
查询就相当于拿value和字典树上已经存储过的值按位进行异或运算。
如果某一位足以确定大小关系 v a l u e ⨁ k < m a x value \bigoplus k < max value⨁k<max,就可以累加 v a l u e value value在该二进制位处的叶子数量值,低位的就不用继续算了。
如果某一位足以确定大小关系 v a l u e ⨁ k > m a x value \bigoplus k > max value⨁k>max,就不用继续进行了,此时低位无论取什么值都无法满足题意,累加值为0。
如果存在 v a l u e ⨁ k = m a x value \bigoplus k = max value⨁k=max,这种情况的k也是符合条件的,也要算进去。
class Solution {
public int countPairs(int[] nums, int low, int high) {
Node root = new Node();
int result = 0;
for (int num : nums) {
// 先查询,后插入
result += root.query(root, num, high) - root.query(root, num, low - 1);
root.insert(root, num);
}
return result;
}
}
class Node {
int count;
Node[] child;
public Node() {
this.count = 0;
this.child = new Node[]{
null, null};
}
public void insert(Node root, int value) {
// 假设树的根节点是第0层,只有一个根节点
// 那么,树的第i层,有2^i个结点
// 要想知道一个值是什么,需要从根结点遍历到叶子结点
// 所以,树中的叶子结点数代表树中存储了多少个数字
// 2^14 < 2*10^4 < 2^15
// 所以,用一棵高度为15(从0开始计数)的字典树,可以存储下所有的数据
// 我们规定,左孩子代表二进制是0的分支,右孩子代表二进制是1的分支
root.count++;// 根节点的count++
for (int i = 15; i >= 0; i--) {
int bit = (value >> i) & 1;// 取出value二进制的第i位
if (root.child[bit] == null) {
// bit分支还是空的,就给它创建一个
root.child[bit] = new Node();
}
root = root.child[bit];// 根据bit的值,移动到不同的分支上
root.count++;// 分支结点的count++
}
}
public int query(Node root, int value, int maxValue) {
int result = 0, i;
for (i = 15; i >= 0; i--) {
int valuebit = (value >> i) & 1;
int maxValuebit = (maxValue >> i) & 1;
int answerbit;
// 分别求左右子树对应的位和valuebit异或的结果,对比maxValuebit
Node node = null;// 当出现异或结果相等的时候,临时存储这个相等的结点,用在低位的比较,0和1最多只有一边出现相等的情况
// 遍历root的所有孩子,这里只有0和1两个孩子,也写成循环的形式,更有通用性,如果孩子是a-z,可以改成遍历[a, z]即可
for (int j = 0; j < 2; j++) {
if (root.child[j] != null) {
answerbit = j ^ valuebit;
if (answerbit < maxValuebit) {
// 在当前位已经比较出大小,就不用继续比较低位了
result += root.child[j].count;
}
if (answerbit == maxValuebit) {
// 在当前位无法比较出大小,继续比较低位,将child[j]临时存储下来
node = root.child[j];
}
}
}
if (node != null) {
// 出现了异或结果相等的情况,还需继续比较更低位
root = node;
} else {
// 无需继续比较低位了
break;
}
}
if (i == -1) {
// 最低位都比较过了,还没有比较出来maxVlue和value ^ a的大小关系,也就是value ^ a == maxValue的情况
// 这里需要注意,nums里可能有相同的值,如果有两个a都满足value ^ a == maxValule了,这里就得加2,所以不能用result++;
result += root.count;// 这里的root一定是叶结点了
}
return result;
}
}
还有另一种写法,不需要考虑等于的情况,写起来更容易一些。需要改动的是query的参数和query的方法体。
public int countPairs(int[] nums, int low, int high) {
Node root = new Node();
int result = 0;
for (int num : nums) {
// 先查询,后插入
result += root.query(root, num, high + 1) - root.query(root, num, low);
root.insert(root, num);
}
return result;
}
public int query(Node root, int value, int maxValue) {
int result = 0, i;
for (i = 15; i >= 0; i--) {
int valuebit = (value >> i) & 1;
int maxValuebit = (maxValue >> i) & 1;
if (maxValuebit == 1) {
// maxValuebit == 1,要使得value ^ a的这一位是0,也就是value和a的这一位同1或同0
if (root.child[valuebit] != null) {
// 这里的valuebit == abit,按位异或 == 0,此时可以判断小于maxValue了
result += root.child[valuebit].count;
}
// value ^ a的这一位是1的情况,也就是value和a的这一位不同
if (root.child[1 ^ valuebit] != null) {
root = root.child[1 ^ valuebit];// 转向这一侧继续判断
} else {
break;
}
} else {
// maxValuebit == 0,如果value ^ a的这一位是1,不满足;如果value ^ a的这一位是0,有可能满足,这里就只需要考虑异或结果是0的情况,异或结果是0,那么value和a的这一位同1或同0
if (root.child[valuebit] != null) {
// 这里的valuebit == abit,按位异或 == 0,继续判断
root = root.child[valuebit];
} else {
break;
}
}
}
return result;
}