一、题目
多次求和构造目标数组
二、代码
没做出来呜呜😢
// 1
class Solution {
public boolean isPossible(int[] target) {
// 将target排序,从头至尾依次判断是否可由A数组元素相加后组成
// 对于A的处理则是从1开始
// 对于示例3:选择哪一个元素交换
// 不能选择前面已经配对的元素-做标记?-配对了的往后放?"选择满足 0 <= i < target.size 的任意下标 i ,并让 A 数组里下标为 i 处的值为 x"
// 初始化A数组
int len = target.length;
int []A = new int[len];
for(int i=0;i<len;i++) {
A[i] = 1;
}
// 对target排序-升序
Arrays.sort(target);
// A数组所有元素之和
int sum = len;
// 新元素存放位置
for(int i=0;i<len;i++) {
// 对A进行排序
Arrays.sort(A); // *
// 先找-由于target和a均为升序,故此层循环停止条件分为3种情况
// 1.找到则继续构造下一个目标;
// 2.已经遍历完A内数均小于目标,则进行组合;
// 3.当前遍历的A[i]>目标则说明一定构造不出,返回false
for(int j=i;j<len;j++) {
if(target[i]==A[j]) {
break;
}else if(target[i]<A[j]) {
return false;
}
}
// 后组合
// 开始进行组合,每次组合后继续判断,若在此过程中最后未能匹配到,即所有匹配情况均大于目标值,则返回false
// 为在判断中容易些,如果匹配上了应该和target位置保持一致
while(true) {
A[i] = sum;
sum = A[i] + (len-i-1);
if(target[i]==A[i]) {
break;
}else if(target[i]<A[i]) {
return false;
}
}
}
return true;
}
}
// 错误:无限循环
// while(true)代码块中sum求和策略有问题,为了保证target和A数组元素匹配后下标也一致,*处不应将A进行排序
// 2
class Solution {
public static boolean isPossible(int[] target) {
// 将target排序,从头至尾依次判断是否可由A数组元素相加后组成
// 对于A的处理则是从1开始
// 对于示例3:选择哪一个元素交换
// 不能选择前面已经配对的元素-做标记?-配对了的往后放?"选择满足 0 <= i < target.size 的任意下标 i ,并让 A 数组里下标为 i 处的值为 x"
// 初始化A数组
int len = target.length;
int []A = new int[len];
for(int i=0;i<len;i++) {
A[i] = 1;
}
// 对target排序-升序
Arrays.sort(target);
// A数组所有元素之和
int sum = len;
// 新元素存放位置
for(int i=0;i<len;i++) {
// 先找-由于target和a均为升序,故此层循环停止条件分为3种情况
// 1.找到则继续构造下一个目标;
// 2.已经遍历完A内数均小于目标,则进行组合;
// 3.当前遍历的A[i]>目标则说明一定构造不出,返回false
for(int j=i;j<len;j++) {
if(target[i]==A[j]) {
break;
}else if(target[i]<A[j]) {
return false;
}
}
// 后组合
// 开始进行组合,每次组合后继续判断,若在此过程中最后未能匹配到,即所有匹配情况均大于目标值,则返回false
// 为在判断中容易些,如果匹配上了应该和target位置保持一致
while(true) {
A[i] = sum;
sum = 2*sum-1;
if(target[i]==A[i]) {
// System.out.println(A[i]);
break;
}else if(target[i]<A[i]) {
return false;
}
}
}
return true;
}
}
// 测试用例错误
// 错误原因:"sum"加和策略以及逻辑顺序有些问题(尝试先看sum是否合适,然后挑选位置放入)
// 3
class Solution {
public boolean isPossible(int[] target) {
// 初始化A数组
int len = target.length;
int []A = new int[len];
for(int i=0;i<len;i++) {
A[i] = 1;
}
// 对target排序-升序
Arrays.sort(target);
// 重新整理逻辑
// 从头到尾判断A是否有与当前目标相同的值,有则继续判断下一个目标;
// 设置一个判断记号flag,若在遍历过程中存在比当前目标值大的元素,则flag设为true,则无论如何操作都不可能构造出当前目标值
// 若不是以上两种,则先算和,后放入数组中
for(int i=0;i<len;i++) {
// 记号
boolean flag = false;
// j一定要从i同一位置向后判断,实现一一配对儿
for(int j=i;j<len;j++) {
if(target[i] == A[j]) {
if(i!=j) {
int temp = A[j];
A[j] = A[i];
A[i] = temp;
}
break;
}else if(target[i] < A[j]) {
flag = true;
}else {
// A数组所有元素之和
int sum = 0;
for(int s=0;s<len;s++) {
sum += A[s];
}
if(sum==target[i]) {
A[i] = sum;
break;
}else if(sum>target[i]) {
return false;
}else {
// ××××××××××××××怎么组合?××××××××××××××
// 感觉无法通过特定挑选组合,而是用某种算法将可能性都列举出来再选择
}
}
}
}
return true;
}
}
// 46 / 71 个通过的测试用例
// 整体思路错了
三、题解
解法
思路和算法
这道题最容易想到的思路是从初始数组开始模拟所有的可能性,但是可能性太多,会超出时间限制,因此需要考虑其他思路。
由于初始数组的元素都是 1,都是正整数,每次操作都是将数组中的一个元素替换成数组中的所有元素之和,因此每次操作的结果都是将数组中的一个元素值增加,且变化后的元素一定是数组中的最大元素。只要找到数组中的最大元素,即可知道在最后一次操作之前的数组中的所有元素之和,从而将数组恢复到最后一次操作之前的状态。
由此可以反向思考,即从目标数组开始,每次计算上一个状态,判断是否可以得到初始数组。
为了能得到数组中的最大元素,可以使用基于大根堆的优先队列存储数组中的所有元素,优先队列的队首元素即为数组中的最大元素。遍历数组 target,将所有元素加入优先队列,并计算所有元素之和,记为 sum(最后目标数组target形成的前一步需要利用target数组所有元素的和sum前推得出),然后进行反向操作。
将优先队列的队首元素取出,记为 curr,数组中的其他元素之和为 remainSum=sum−curr,则在最后一次操作之前,数组中的所有元素之和为 curr,因此 curr 元素的上一个值为 prev=curr−remainSum。将 sum 的值减去 curr−prev(curr-pre是当前状态与元素为pre状态时相比多了多少,则sum也就多了多少;由sum-(curr-pre)可算出pre状态时的sum值),将 prev 加入优先队列,则 sum 为最后一次操作之前的数组中的所有元素之和,优先队列中的元素为最后一次操作之前的数组中的所有元素。重复上述反向操作,如果能到达初始数组,即数组中的所有元素都是 1,则返回 true,如果出现数组中的元素小于 1 的情况,则返回 false。
当数组中的最大元素远大于数组中的其他元素之和时,上述反向操作的做法仍然会超时(如target为[10000000000000000000001, 1],若按常规反向操作进行,则需要10000000000000000000000的操作才能最终得到[1,1];而改进后由于
// sum=10000000000000000000002;
// curr=10000000000000000000001;
// reaminSum=sum-curr=1;
// curr>remainSum->curr mod reaminSum=0->prev=remainSum=1->[1, 1]->done->return true;)。
注意到当 curr>remainSum 时,数组中的最大元素一定是 curr,且 remainSum 的值不会变化,因此每次反向操作都会使 curr 的值减少 remainSum,直到 curr≤remainSum 时数组中的最大元素才可能不是 curr。因此可以一步计算 prev,令 prev 为 currr 减去若干个 remainSum 之后的值且满足 1≤prev≤remainSum,具体计算方法如下:
如果 curr mod remainSum=0,则 prev=remainSum;
如果 curr mod remainSum≠0,则 prev=curr mod remainSum。
得到 prev 之后,将 sum 的值减去 curr−prev,将 prev 加入优先队列,即达到用 prev 更新 curr 的效果。
上述反向操作的过程必须保证数组中的全部元素都大于 0。如果在某一步反向操作之后,数组中的全部元素之和等于 n,则恢复到初始数组,返回 true。
如果在反向操作的过程中出现 remainSum=0 或者 curr−remainSum<1 的情况,则说明反向操作会导致数组中出现小于 1 的元素,返回 false。
class Solution {
public boolean isPossible(int[] target) {
// 1 元素值大的优先
PriorityQueue<Long> pq = new PriorityQueue<Long>(new Comparator<Long>() {
public int compare(Long num1, Long num2) {
return num2.compareTo(num1);
// 因为num1和num2都是long类型,因此使用compareTo方法
// 若num1和num2都是int类型,则使用运算式子即可,如升序,则:return num2-num1;
}
});
long sum = 0;
for (int num : target) {
sum += num;
// offer() : 添加元素,如果添加成功则返回true,如果队列是满的,则返回false
pq.offer((long) num);
// 由于target为int数组,故变量num也需为int型,且再添加到队列时,转换为long型
}
int n = target.length;
// 当返回到数组全为1时,即sum==n时,则跳出循环
while (sum > n) {
// poll() : 移除队列头的元素并且返回,如果队列为空则返回null
long curr = pq.poll();
long remainSum = sum - curr;
if (remainSum == 0 || curr - remainSum < 1) {
return false;
}
// 如果 curr mod remainSum=0,则 prev=remainSum;如果 curr mod remainSum≠0,则 prev=curr mod remainSum。
long prev = curr % remainSum == 0 ? remainSum : curr % remainSum;
// curr-pre是当前状态与元素为pre状态时相比多了多少,因此当前sum减去这个值得到的就是pre对应数组元素之和
sum -= curr - prev;
// offer() : 添加元素,如果添加成功则返回true,如果队列是满的,则返回false
pq.offer(prev);
}
return sum == n;
// 或直接return true;因为从while条件内跳出条件为sum>=n,当两者相等时则说明为真;若小于则说明一定有元素为小于等于0,在while循环内已筛出这种情况,直接返回false。故,这里也可以直接返回true,提交后答案正确
}
}
来源:力扣(LeetCode)
四、总结
1.优先队列
与1353题解中构造优先队列相比较:
(1)(2)
① (1)处可以能正常使用lambda表达式"(a,b)->b-a"而(2)无法使用的原因是?
(2)也可以使用lambda表达式,只不过需要进行一次类型转换:PriorityQueue pq = new PriorityQueue((a, b) -> (int)(b - a));需要注意的是:当使用lambda表达式时,返回值要为整型。如本例中,由于lambda有类型推断机制,故当定义一个存有Long类型的优先队列时,lambda表达式缺省的参数也为Long型,因此需要在最后返回结果时转换为int型
② 而(2)则是使用常规的方法向优先队列中传递一个自定义的Comparator对象来指定元素的比较规则。例如:
2.Java compareTo() 方法
本例中需要实现升序,故num1大时 优先级大 需要返回-1,故表达式为num2.compareTo(num1)
3.其他
1. 传入PriorityQueue的比较器Comparator后优先级定义规则:定义PriorityQueue时需要传入一个比较器Comparator,这个比较器将决定元素的优先级,决定方式类似于List的sort()方法,也就是当传入a,b时,如果a优先度更高,就会返回负数,如果b优先度高就返回正数,相等就返回0。
2. 无论是compare还是compareTo都需要将最后结果做一次sgn()(符号函数)处理,如下图。所以maybe若比较器传入的是lambda表达式(后来查看官方文档发现是对的)可能就自动重新构造一个Comparator重写了compare方法,即也要对"->"后表达式返回的值进行一次sgn()(符号函数)处理,因此要保证返回的一定是int类型。
3. Java中PriorityQueue的用法以及Comparator接口、Comparable接口、compare方法和compareTo方法
(1)PriorityQueue优先队列
PriorityQueue queue=new PriorityQueue<>(); //默认从小到大
PriorityQueue queue=new PriorityQueue<>( (a,b)->(b-a)); //从大到小
PriorityQueue<int[]> queue=new PriorityQueue<>((a,b)->(a[0]-b[0])); //自定义排序 数组的第一个数字
自定义排序
第一种写法:
类不实现Comparable接口,
lamda表达式定义compator
第二种写法:
类不实现Comparable接口,
自定义新的compator
第三种写法:
类实现Comparable接口
优先队列可以不定义compator
(2) Comparator接口、Comparable接口、compare方法和compareTo方法
Comparable和Comparator都是用来实现集合中元素的比较、排序的。使用Comparable需要修改原先的实体类,而Comparator 不用修改原先的类直接去实现一个新的比较器 ,因此Comparator实际应用更加广泛。
Comparable是在集合内部定义的方法实现的排序。 实现Comparable必然要重写compareTo(T o)方法。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。
Comparator是在集合外部实现的排序。实现了Comparator接口的类,一定要实现compare(T o1,T o2)方法
compareTo(Object o)方法是java.lang.Comparable接口中的方法
compare(Object o1,Object o2)方法是java.util.Comparator接口的方法