LeetCode 321. 拼接最大数 (错误的解法的进一步思考)

前言

7.30 日晚上做这道题,花了3小时多,最后没做出来,想了一晚上为什么我的解法有错误,最终发现根本问题在于本题在逻辑上无解,只能进行枚举求解。

以下我将展示我在逻辑上是如何思考这一题的,又是怎么钻进死胡同里,直到最后才明白这是一个死循环。

这题同一批次的学习笔记在:
算法学习 (门徒计划)4-3 专项面试题解析 学习笔记
学习笔记里记录了成功的解法,而本文讨论这次失败的解法的问题所在。

LeetCode 321. 拼接最大数

链接:https://leetcode-cn.com/problems/create-maximum-number

给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。

求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。

说明: 请尽可能地优化你算法的时间和空间复杂度。

示例 1:

输入:
nums1 = [3, 4, 6, 5]
nums2 = [9, 1, 2, 5, 8, 3]
k = 5
输出:
[9, 8, 6, 5, 3]

解题思路

阶段一:思考可用知识点

本题显而易见就是在获取数有限的情况下,拼一个尽可能大的数字,那么就相当于期望高位尽可能的大,对于这么一个期望值尽可能的大的需求则通常是用单调栈或者单调队列来做的,由于不存在滑动窗口,那么就用单调栈来做。

另外根据期望找到最大值这个需求,那么就要用单调递减栈。

扫描二维码关注公众号,回复: 14886792 查看本文章

因此: 用一个数组作为单调栈递减栈,同时也作为最终结果进行返回
对对应代码中的:
int [] stack = new int [k];

阶段二:讨论问题

简单讨论

相比起普通的单调递减栈,这次的入栈选择就变得不一样了,要求尽可能的从两个数列中进行依次取大的数字,但是当剩余的数字和栈内数字数量总和为k时,入栈操作应该取消旧数据出栈需求,最终将这个结果返回。

但是这种思考是有问题的,通常情况下,单调栈是对于一个震荡序列进行入栈来进行单调性维持的,但是如果入栈选择是两个序列,则在合并成一个序列时有多种情况,因此如果不能讨论清楚这多种情况,则不能这么入栈

显然是讨论不清楚的,因此一个单调栈的方案就被否决了。

进一步讨论

那么合并成一个序列再进行单调性维护不可行,如果先对两个各自的序列进行单调性维护,再将结果进行合并,可行吗?

可行,但不完全可行,当两个子序列长度之和能够大于需求的结果序列长度时,此时可以从这两个单调性序列中进行取尽可能的大的数字来合并成结果。
但是当两个子序列长度之和不足时情况就不同了

问题转换为:有没有办法进行子序列长度的补充,但是让补充的结果对于数值的影响最小呢。

新问题的讨论 (我走入死胡同的开始)

我认为有:

  • 首先这个用于补足长度的字符一定得尽可能的补充在低位,因此我找到合并序列的最后一位试图在前进行数值的插入。

于是我遇到了一个新的问题,这些插入的数字从何而来,又能插入几个?

  • 这些数字从被出栈的元素中来
  • 出栈的元素区间有多大就能提供多少个

由此对应这一段代码:

					stack[endIndex--]= nums1[q1[q1top]];
                    System.out.println(endIndex+"-e1--"+stack[endIndex+1]);
                    int size =0;
                    if(q1top>0)
                        size = q1[q1top]-q1[q1top-1]-1;
                    else    
                        size = q1[q1top];
                    
                    while(size>0&&needLen>0) {
    
    
                        stack[endIndex--] =  nums1[s1.pop()];
                        size --;
                        needLen --;
                    }
                    q1top--;

完善设计(无法补完的设计)

既然已经知道了如何插入数字,并找到了数字的来源(当单调栈进行单调维护时,将出栈的元素放入一个备用栈中,从这个栈中出的元素在一个区间内都是符合单调性的)唯一的问题就是如何选择用来插入数值的区间。

这个需求咋一看可以实现,但是逻辑上不可能

这个区间的选择有4个优先级,优先级数字越大,优先级越低
(优先级是逐层比较的,先进行级别外的比较,再进行级别内的比较)

  1. 区间有或者无。当需要一个区间进行数值的插入时,优先选择区间长度大于0的区间(这很好理解,如果长度为0,那就不可用)
  2. 区间结尾小或者大。两个备选区间结尾如果一大一小,那么这两个区间一定是结尾大的排在前面(这很好理解,因为前面的数字可以获得更高的权重)
  3. 区间本次贡献长度长或者短。两个备选区间如果都不能满足或者其中有一个不能满足本次长度的需求,那么优先选择更能满足长度需求的区间(这稍微难理解了一点,实际上,使用的区间越长就意味着那些数值小的数,更多的处于了低位,让高位尽可能的放大的数字)
  4. 区间总整体贡献数值的大或者小(这是本质上无法解决的问题,接下来我会重点说。)

核心漏洞

对于第四种优先级,当情况需要进行这种优先级讨论时,必然:

  • 若干连续的单调子序列结尾相等,
  • 都有可用空间
  • 可用空间长度相同

我举个例子,以下两个序列需要合并一个长度为5的结果:

[7,2,7]–>[7,7]
[7,6,7]–>[7,7]

此时怎么写逻辑判断可以实现77767?

一般人都会想,选择子区间值更大的。的确如此,但是如果备选的子区间不是2选择1而是2选择2、3选2或者4选2,应该怎么做。

我举个例子,以下两个序列需要合并一个长度为8的结果:

[9,2,9,8,9]–>[9,9,9]
[9,6,9,4,9]–>[9,9,9]

此时总序列长度为6,需要找补长度为2,最终结果应该为:
9,9,9,9,8,9,4,9

如果依旧是用选择子区间更大的逻辑进行2选1,结果应该是:
9,9,9,9,4,9,8,9
因为第一轮的区间选择是8和4比较,第二轮则是4和2比较

因此相同优先级别间不是2选1而是召集所有当前轮次能获得同优先级区间进行不破坏原始顺序的情况下,进行选择平凑出一个长度满足找补需求的子区间序列,使得满足合并的结果值最大。

换句话说如果对于上方的例子那就是:
从2、8和6、4这两个序列中选出两个不改变原始顺序的子序列,另这两个子序列合并的结果值最大

这个需求等效于什么?等效于这题本身的需求,因此在这一刻,求解这题的办法要求能够先解出这道题,线程锁死了。

示例代码(仅仅实现了优先级1和2的处理)

(不能解决全部的问题,但是能解决只包含优先级1和2的问题)

class Solution {
    
    
    public int[] maxNumber(int[] nums1, int[] nums2, int k) {
    
    
        int [] stack = new int [k];
        int stack_top = -1;
        int last1 = nums1.length;
        int last2 = nums2.length;
        

        Stack <Integer> s1 =new  Stack <Integer>();
        Stack <Integer> s2 =new  Stack <Integer>();

        int [] q1 = new int [nums1.length];
        int [] q2 = new int [nums2.length];   
        int q1top = -1;
        int q2top = -1;
        for(int i=0;i<nums1.length;i++){
    
    
            while(q1top>=0&&nums1[q1[q1top]]<nums1[i]){
    
    
                s1.add(q1[q1top--]);
            }
            q1[++q1top] = i;
        }
        for(int i=0;i<nums2.length;i++){
    
    
            while(q2top>=0&&nums2[q2[q2top]]<nums2[i]){
    
    
                s2.add(q2[q2top--]);
            }
            q2[++q2top] = i;
        }
        //System.out.println(q2top+q1top+2);

        if(q2top+q1top+2>=k){
    
    
            int i = 0,j=0,index=0;
            while(index<k){
    
    
                if(j>q2top||(i<=q1top&&nums2[q2[j]]<nums1[q1[i]])){
    
    
                    stack[index++] = nums1[q1[i++]]; 
                }else
                    stack[index++] = nums2[q2[j++]];
            }
        }else{
    
    
            int needLen = k-q2top-q1top-2;
            int endIndex = stack.length -1;
            while(needLen>0){
    
    
                
                if(q2top<0||(q1top>=0&&nums2[q2[q2top]]>nums1[q1[q1top]])){
    
    
                    stack[endIndex--]= nums1[q1[q1top]];
                    System.out.println(endIndex+"-e1--"+stack[endIndex+1]);
                    int size =0;
                    if(q1top>0)
                        size = q1[q1top]-q1[q1top-1]-1;
                    else    
                        size = q1[q1top];
                    
                    while(size>0&&needLen>0) {
    
    
                        stack[endIndex--] =  nums1[s1.pop()];
                        size --;
                        needLen --;
                    }
                    q1top--;
                    System.out.println(q1top+"-1--"+needLen);
                }else{
    
    
                    stack[endIndex--]= nums2[q2[q2top]];
                    System.out.println(endIndex+"-e2--"+stack[endIndex+1]);
                    int size =0;
                    if(q2top>0)
                        size = q2[q2top]-q2[q2top-1]-1;
                    else    
                        size = q2[q2top];
                    
                    while(size>0&&needLen>0) {
    
    
                        stack[endIndex--] =  nums2[s2.pop()];
                        size --;
                        needLen --;
                    }                    
                    q2top--;
                    System.out.println(q2top+"-2--"+needLen);
                }
            }
            int i = 0,j=0,index=0;
            while(index<=endIndex){
    
    
                if(i<=q1top&&nums2[q2[j]]<nums1[q1[i]]){
    
    
                    stack[index++] = nums1[q1[i++]]; 
                }else
                    stack[index++] = nums2[q2[j++]];
            }
        }     


        return stack;
    }
}

结语

在后续反思的过程中,我在想为什么这种解法会诱导我逐步深入但是最后把我困死在逻辑中,因为这种解法非常有诱惑力,在讨论到第四个优先级别时,这个解法实际上可以以很高的效率去解决绝大部分的题目举出的例子。并且前3个优先级都是在两个备选区间中选择一个这是逻辑能做的事情,而当到达第四个优先级时,突然变成了在N个区间中选择M个的情况,这是逻辑做不到的事情,只能遍历所有情况然后寻找最合适的。

并且逻辑是步步深入的,在本题的思考中,前几步是稳健的道路可行,有效,唯独最后一步,是无法抵达的天堑。

如何让自身在开发过程中不要陷入这种陷阱,我的理解是真正的想清楚再行动。那么有没有可能不行动就没办法想清楚呢,必然是可能的。

这种想不清楚的可能性,就是程序的漏洞。

如何避免这种漏洞

善用计算机的强大计算力,在必要的时候用枚举,代替逻辑,因为逻辑永远是2选一,而枚举则是尝试最优解。

逻辑只能实现二选一,枚举则是N选一,尽管最终选择是用逻辑实现的,但是枚举提供了求解问题的维度提升,因此解决问题时,如果出现了死循环说明维度太低,需要升维。

本题如果在一开始我就想着试一下所有的可能情况,我很快就能和官方解法一样,但是我看着题目上写着尽量提高性能,于是,我先进行了方案的设计试图用逻辑代替枚举,这使得我花了很多时间才想清楚这个方案是不可行的。

因此在开发生涯中,首先要考虑不是性能,而是一个可行的简单的可以理解的方法,然后才是对这个方法的优化,最终实现一个高性能的解决方案

猜你喜欢

转载自blog.csdn.net/ex_xyz/article/details/119273779