[LC]好数组

357 阅读1分钟

这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

题目

leetcode-cn.com/problems/th…

提示:

  • 1 <= nums.length <= 10^5^
  • 1 <= nums[i] <= 30

解析

先做一个推论:

  • 如果一系列数的乘积,可以被表示为若干个不同质数的乘积,那么:这一系列数中的每个数分解后得到的都必然是多个不同的质数

听起来像废话,解释一下:

  • 例如给出数组[2~15],那么,如果子数组是一个所谓的好子数组,那么子数组中的数必然满足下列条件:
    • 要么本身是质数,要么是由多个不同质数相乘得到的,即:必然包括K个质数,k>=1。
    • 这K个质数,和子数组中别的K不能指向任何相同的质数。(例如:5,15就不是所需的一个结果)。

注意到题目给出的数,范围在1~30之间,那么:

  • 列出1~30之间,彼此哪些可以组成好数组?
  • 对数据做一下统计,按照上面的规律,就可以计算出最终需要的“好数组”数。

附表:

graph LR
2-->3-->5-->7-->11-->13-->17-->19-->23-->29
2-->6
3-->6
2-->10
5-->10
3-->15
5-->15

根据这个取过了就不能再取的逻辑(若干个不同质数),同时知道30以内的质数只有以下10个:

2,3,5,7,11,13,17,19,23,29

因此,可以用10位来表示取了一些数后的状态,即乘积可以分为哪一些质数。

同时,30以内的所有数,也可以按照上面的,预先做一下分解,方便后续的计算:

static Map<Integer,Integer> dic = new HashMap<>(30);

static{
        int[] prime = new int[]{2,3,5,7,11,13,17,19,23,29};

        for (int i = 2; i <= 30; i++) {
            int t = i;
            int bit = 0;
            boolean det = false;
            for (int i1 = 0; i1 < prime.length&&t!=1&&!det; i1++) {
                int pr = prime[i1];
                if(t%pr==0){
                    if(t%(pr*pr)==0){
                        det = true;
                    }
                    bit+=1<<i1;
                    t = t/pr;
                }
            }
            if(!det) dic.put(i,bit);
        }
    }

那么,计算的步骤该如何求取?

  • 我们把取了哪些质数有多少情况存起来,那么在循环某个数的时候,就可以按照情况(多不算,少的补上计算),对对应的数进行计算。
public int numberOfGoodSubsets(int[] nums) {

    //代表1-30个数
    int[] numCnt = new int[31];
    //这里的意思是:我们会将所有的状态保存下来,而所有状态中最大的是toBinary(1111111111)
    long[] statusCnt = new long[0b1111111111+1];
    //需要初始化statusCnt[0]=1,这样子在做状态计算的时候,数对应的就可以自己计算上去
    statusCnt[0] = 1;
    //结果
    //计算有多少个数
    for (int i : nums) numCnt[i]++;
    //由于只有10个数,使用状态压缩减少存储空间
    for (int curNum = 2; curNum <= 30; curNum++) {
        if(numCnt[curNum]==0) continue;
        int curStatus = dic.getOrDefault(curNum, -1);
        //没有这个数
        if (curStatus == -1) continue;

        //此时,我们对所有状态进行查找,目的是:为每一个缺少当前状态的值加上当前的数
        //根据我们的分析,可以使用状态转移,而这个状态转移,实际上指的是:
        // 缺少X个素数的,在出现这X个素数的数(素数组合或者符合条件的合数)之后,同样地可以组合成好数组
        //这里遍历顺序实际上无所谓,因为这里实际上是从已有状态中取的,如果相同的数取过了,状态中就已经显示出来了
        // 那么无论是对应状态的组合还是当前的数,如果对应位置上有重复,是不可能取的
        for (int historyStatus = 0; historyStatus < statusCnt.length; historyStatus++) {
            //与条件:两位为1才为1,不为0表示有相同的位,这里表示有相同的素数出现了
            if ((curStatus & historyStatus) != 0) continue;
            //status | j 索引位置,表示将j整合进状态为status的任意之后,得到的素数状态
            statusCnt[curStatus | historyStatus] =
                    (statusCnt[historyStatus | curStatus] + statusCnt[historyStatus] * numCnt[curNum]) % mask;
        }
    }

    //结束了状态的计算,最后需要对结果进行一个计算
    long res = 0;
    for (int i = 1; i < statusCnt.length; i++) {
        res = (res + statusCnt[i])%mask;
    }

    //在状态计算结束后,对1进行补充。
    // 对于任意位置的1,根据题意选择选或者不选,对最终的结果数组没有影响,但是都是独立的状态,因此都需要计入
    for (int i = 0; i < numCnt[1]; i++) {
        res = (res * 2) % mask;
    }
    return (int)res;
    }

执行用时:9 ms, 在所有 Java 提交中击败了78.79%的用户

内存消耗:50.5 MB, 在所有 Java 提交中击败了81.82%的用户