力扣(LeetCode)每日随机一题——1354.多次求和构造目标数组[困难]

文章讲述了如何使用优先队列和compareTo方法解决一个题目,涉及多次求和构造目标数组的问题,通过优化算法和优先级队列的使用避免了时间复杂度过高的问题。
摘要由CSDN通过智能技术生成

一、题目

多次求和构造目标数组

123

二、代码

没做出来呜呜😢

// 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)1(2)
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对象来指定元素的比较规则。例如:3

2.Java compareTo() 方法

12
本例中需要实现升序,故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类型。1
2
1
2

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
1
第二种写法:
类不实现Comparable接口,
自定义新的compator
2
第三种写法:
类实现Comparable接口
优先队列可以不定义compator
3(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接口的方法

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值