周赛地址:https://leetcode-cn.com/contest/weekly-contest-227/
第一题:检查数组是否经排序和轮转得到
题目已经告诉了轮转的意思:A[i] == B[(i+x) % A.length],我们就要使用这个条件作为判断的依据。
数据量也不大,直接循环所有的x进行判断即可,在判断过程中,如果有符合的,直接返回true,如果所有的x都跑完了还没有返回,那就是false了。
class Solution {
public boolean check(int[] nums) {
int length = nums.length;
// 复制一个用于排序
int[] copy = Arrays.copyOf(nums, length);
Arrays.sort(copy);
// 遍历所有的轮转情况
for (int i = 0; i < length; i++) {
boolean flag = true;
for (int j = 0; j < length; j++) {
if (nums[j] != copy[(j + i) % length]) {
flag = false;
break;
}
}
// 一次轮转完,满足条件直接返回
if (flag) {
return true;
}
}
return false;
}
}
第二题:移除石子的最大得分
当存在两个或更多空堆的时候,游戏停止,我们需要在游戏停止前,更多的得分,那么可以每次选择当前最多的两堆石子,这样可以保证最迟游戏停止。
class Solution {
public int maximumScore(int a, int b, int c) {
// 构造一个倒序的优先队列
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(3, Collections.reverseOrder());
priorityQueue.offer(a);
priorityQueue.offer(b);
priorityQueue.offer(c);
int score = 0;
while (true) {
// 每次从优先队列中取出两个最大的
a = priorityQueue.poll();
b = priorityQueue.poll();
// a == 0时,b和c一定是0,b == 0时,c一定是0
if (a == 0 || b == 0) {
break;
}
// 将最大和次大的减一后放入优先队列
priorityQueue.offer(--a);
priorityQueue.offer(--b);
score++;
}
return score;
}
}
此题还有一种数学解法,时空复杂度都会降低。
假设a,b,c是按照从小到大的顺序,c最大。
考虑两种情况:
1.当a+b≤c的时候,先在a和c里取石子,经过a次操作后,a变为0,c变为c-a,再从b和c里取石子,经过b次操作,b变为0,c变为c-a-b。此时a和b都是0了,游戏结束,此时分数为a+b。
2.当a+b>c的时候,可以使得最后的结果,每个堆尽可能为0,因为取的时候,每次都会选择两个最大的数字取石子,最后一定会出现两种情况:[1,1,1]和[1,1,0]。第一种情况再取一次就是[1,0,0],第二种情况再取一次就是[0,0,0],此时的分数是最高的。根据游戏规则,每次在两个不同的堆里取两个石子。如果最后石子正好可以取完,那么a+b+c的值就是偶数,否则就是奇数。每次从堆里取两个石子,得1分,最终的得分就是(a+b+c)/2了。
class Solution {
public int maximumScore(int a, int b, int c) {
int[] array = new int[]{a, b, c};
// 从小到大进行排序
Arrays.sort(array);
a = array[0];
b = array[1];
c = array[2];
if (a + b <= c) {
return a + b;
} else {
return (a + b + c) / 2;
}
}
}
第三题:构造字典序最大的合并字符串
最初看到这个题目的时候,以为就是两路归并嘛,觉得很简单,结果直接就做错了。
仔细分析了一下,漏掉了一些情况:当word1和word2在index1和index2位置,要对比的字符相同的时候,要看后面的字符情况,要想构造最大的合并字符串,当碰到相同字符的时候,就要先选择子串更大的那一个,这样更容易把大的字符放在前面。
将问题再做精简,在判断从word1还是word2取字符的时候,直接用子串判断,就不用考虑index1和index2处位置字符是否相同了。
class Solution {
public String largestMerge(String word1, String word2) {
int index1 = 0, index2 = 0, length1 = word1.length(), length2 = word2.length();
StringBuilder stringBuilder = new StringBuilder(length1 + length2);
while (index1 < length1 && index2 < length2) {
// 判断子串谁大,先取子串大的,更容易把更大的字符放在前面
if (word1.substring(index1).compareTo(word2.substring(index2)) < 0) {
stringBuilder.append(word2.charAt(index2++));
} else {
stringBuilder.append(word1.charAt(index1++));
}
}
// 若word1还有字符
while (index1 < length1) {
stringBuilder.append(word1.charAt(index1++));
}
// 若word2还有字符
while (index2 < length2) {
stringBuilder.append(word2.charAt(index2++));
}
return stringBuilder.toString();
}
}
第四题:最接近目标值的子序列和
困难题是真的难想,以至于我看着题解的代码都看不懂,或许我刷题量不够吧,还需努力啊。
看着题解想了许久,好像有点眉目了,赶紧记录下来。
先说最容易想到的方法,把所有的子序列都搞出来,求每个子序列的和,每个子序列的和跟goal求abs(sum - goal),求出最小值。
这里的数据量是1 ≤ nums.length ≤ 40,对于每一位都有0和1两种选择,考虑极端情况,那么就有个序列和,数据量非常庞大。
根据题解的指导,我们将nums分成两部分left和right,这样,极端情况下只有种类情况(=1048576)。
那么,此时就有3种情况:最优解子序列位于left部分;最优解子序列位于right部分;最优解位于left和right两部分。
当最优解位于left或right部分的时候,直接遍历leftSum或rightSum即可,时间复杂度也是可以接受的。
当最优解位于left和right部分的时候,就要用双指针同时遍历有序的leftSum和rightSum。一个指针i从leftSum的左端,另一个指针j从rightSum的右端,求leftSum[i] + rightSum[j]的和s,如果s > goal,那么j--,否则i++,在这个过程中,一直统计abs(s - goal)的值,如果有更小的值,进行记录。
先来看leftSum和rightSum的求法,这里卡了许久,一直不理解,拿具体数字举例子吧,假设nums[]={1,2,3,4,5,6,7},nums.length=7。先把nums分成两部分,left = [1,2,3],right = [4,5,6,7]。以left为例,right同理。left里有3个元素,由 = 8,我们需要申请一个1 << 3大小的数组leftSum[]用来存储所有子序列的和,leftSum下标对应的二进制形式,即为left数组里子序列的选择情况,比如leftSum[6] = 5,下标是6,6的二进制是0b110,也就是left[2]和left[1]选中结果值(left[2] + left[1] = 5),注意,二进制的右侧是低位。
leftSum[0] = 0:left中的1,2,3,4都没有选择,此时sum = 0
leftSum[1] = leftSum[0] + nums[0] = 1:left中nums[0]被选中,此时sum = 1
leftSum[2] = leftSum[0] + nums[1] = 2:left中nums[1]被选中,此时sum = 2
leftSum[3] = leftSum[2] + nums[0] = 3:left中nums[0]和nums[1]被选中,此时sum = 3
leftSum[4] = leftSum[0] + nums[2] = 3:left中nums[2]被选中,此时sum = 3
leftSum[5] = leftSum[4] + nums[0] = 4:left中nums[2]和nums[0]被选中,此时sum = 4
leftSum[6] = leftSum[4] + nums[1] = 5:left中nums[2]和nums[1]被选中,此时sum = 5
leftSum[7] = leftSum[6] + nums[0] = 6:left中nums[2]和nums[1]和nums[0]被选中,此时sum = 6
先上一段代码,仔细读里面的注释,就能看懂位运算的含义了。
public static void main(String[] args) {
int[] nums = new int[]{1, 2, 3};
int length = 1 << 3;
int[] sum = new int[length];
// [0, length - 1]:nums对应的所有组合,遍历每一种组合
for (int i = 1; i < length; i++) {
for (int j = 0; j < 3; j++) {
// 组合i,对应的二进制表示中,j位置是0,也就是没有选择nums[j],不做处理
if ((i & (1 << j)) == 0) {
continue;
}
// 组合i,对应的二进制表示中,j位置不是0,就要把nums[j]加到sum中
// nums[j]不加的时候,是sum[?]呢?
// 是sum[i - (1 << j)]:从i中扣除j位是1其余位是0二进制表示对应的整数
System.out.println("sum[" + i + "] = sum[" + i + " - (1 << " + j + ") = " + (i - (1 << j)) + "] + nums[" + j + "]");
sum[i] = sum[i - (1 << j)] + nums[j];
break;
}
}
for (int i = 1; i < length; i++) {
System.out.print(sum[i] + " ");
}
}
如果能理解上面的代码,那么就可以求得所有组合对应的子序列和了,后面就是遍历子序列和求abs()最小值了。
class Solution {
public int minAbsDifference(int[] nums, int goal) {
int length = nums.length, mid = length / 2, leftSize = 1 << mid, rightSize = 1 << (length - mid);
int[] leftSum = new int[leftSize], rightSum = new int[rightSize];
// 遍历所有的组合[0, leftSize - 1]
for (int i = 1; i < leftSize; i++) {
for (int j = 0; j < mid; j++) {
// 对每个组合i,判断i对应二进制的第j位置有没有选中,如果是1表示选中,要把nums[j]加到sum中
if ((i & (1 << j)) != 0) {
// 不加nums[j]的时候,原来组合的sum是多少呢?
// 从i里减去:j位置是1,其余位置是0对应二进制的整数,即是不选中nums[j]对应的sum值
leftSum[i] = leftSum[i - (1 << j)] + nums[j];
break;// 避免重复计算
}
}
}
for (int i = 1; i < rightSize; i++) {
for (int j = mid; j < length; j++) {
if ((i & (1 << (j - mid))) != 0) {
rightSum[i] = rightSum[i - (1 << (j - mid))] + nums[j];
break;
}
}
}
int min = Integer.MAX_VALUE;
// 最值组合对应的sum位于leftSum[]中
for (int i : leftSum) {
min = Math.min(Math.abs(i - goal), min);
}
// 最值组合对应的sum位于rightSum[]中
for (int i : rightSum) {
min = Math.min(Math.abs(i - goal), min);
}
// 最值组合对应的sum位于leftSum[]和rightSum[]中
Arrays.sort(leftSum);
Arrays.sort(rightSum);
int sum, left = 0, right = rightSize - 1;
while (left < leftSize && right >= 0) {
sum = leftSum[left] + rightSum[right];
min = Math.min(min, Math.abs(sum - goal));
if (sum < goal) {
left++;
} else {
right--;
}
}
return min;
}
}