2867.统计树中的合法路径数目

目标

给你一棵 n 个节点的无向树,节点编号为 1 到 n 。给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ui, vi] 表示节点 ui 和 vi 在树中有一条边。

请你返回树中的 合法路径数目 。

如果在节点 a 到节点 b 之间 恰好有一个 节点的编号是质数,那么我们称路径 (a, b) 是 合法的 。

注意:

  • 路径 (a, b) 指的是一条从节点 a 开始到节点 b 结束的一个节点序列,序列中的节点 互不相同 ,且相邻节点之间在树上有一条边。
  • 路径 (a, b) 和路径 (b, a) 视为 同一条 路径,且只计入答案 一次 。

思路

质数是指在大于1的自然数(非负整数)中,除了1和它本身以外不再有其他因数的自然数。

现在有一颗 n 个节点的无向树,要求任意两个连通节点间恰好有一个质数节点的路径数。树是一种无环连通图,问题可以转化为从树中选取两个节点,节点之间的路径只经过一个质数节点。由于没有环,所以两点之间的路径是唯一的。

  1. 两个都是质数节点要排除掉。
  2. 以质数节点为中心,与它邻接的非质数节点符合条件。即以质数节点为中心加上与之相连的非质数节点任取两个均可。我们可以称直接与质数节点相连的非质数节点为直接节点。
  3. 直接节点连通的非质数节点也可能满足条件,需要要减去直接节点向外连通的路径,即直接节点加上其向外连通的节点之间任取两个的路径数。

按照上面的思路,先要找到所有的质数节点,涉及到质数判断。同时保存与之直接相连的非质数节点。然后保存非质数节点的边,使用Map保存,边的两个端点都保存进去,方便后续向外查找连通的节点。

得到满足条件的节点总数,根据排列组合公式C(n,2) = n!/(2!(n-2)!) = (n-1)n/2 求得路径总数D。

将外围节点k向外连通节点总数记为Ik,无效路径数为(Ik-1)Ik/2

最终的结果就是D - Σ(Ik-1)Ik/2

代码

/**
 * @date 2024-02-27 0:22
 */
public class CountPaths {

    public Map<Integer, Set<Integer>> primeEdges = new HashMap<>();
    public Map<Integer, Set<Integer>> notPrimeEdges = new HashMap<>();
    public Map<Integer, Integer> indirectNodesNumMap = new HashMap<>();
    Set<Integer> counter = new HashSet<>();

    public long countPaths(int n, int[][] edges) {
        for (int i = 0; i < n - 1; i++) {
            int[] edge = edges[i];
            boolean i0 = isPrimeNumber(edge[0]);
            boolean i1 = isPrimeNumber(edge[1]);
            if (i0 && !i1) {
                primeEdges.computeIfAbsent(edge[0], k -> new HashSet<>());
                primeEdges.get(edge[0]).add(edge[1]);
            } else if (!i0 && i1) {
                primeEdges.computeIfAbsent(edge[1], k -> new HashSet<>());
                primeEdges.get(edge[1]).add(edge[0]);
            } else if(!i0){
                notPrimeEdges.computeIfAbsent(edge[0], k -> new HashSet<>());
                notPrimeEdges.computeIfAbsent(edge[1], k -> new HashSet<>());
                notPrimeEdges.get(edge[0]).add(edge[1]);
                notPrimeEdges.get(edge[1]).add(edge[0]);
            }
        }
        long res = 0;
        for (Integer primeNode : primeEdges.keySet()) {
            Set<Integer> nonPrimeNodesOfPrimeEdge = primeEdges.get(primeNode);
            counter.clear();
            int total = 0;
            for (int nonPrimeNode : nonPrimeNodesOfPrimeEdge) {
                counter.add(nonPrimeNode);
                if (indirectNodesNumMap.get(nonPrimeNode) == null) {
                    indirectNodesNumMap.put(nonPrimeNode, 1);
                    countEdges(nonPrimeNode, nonPrimeNode);
                }
                total += indirectNodesNumMap.get(nonPrimeNode);
            }
            total = total + 1;
            res += total * (total - 1L) / 2L;
            for (int nonPrimeNode : nonPrimeNodesOfPrimeEdge) {
                int indirectNodesNum = indirectNodesNumMap.get(nonPrimeNode);
                res -= indirectNodesNum * (indirectNodesNum - 1L) / 2L;
            }
        }
        return res;
    }

    public Set<Integer> countEdges(int key, int nonPrimeNode) {
        if (notPrimeEdges.get(nonPrimeNode) != null) {
            for (Integer node : notPrimeEdges.get(nonPrimeNode)) {
                if (!counter.contains(node)) {
                    indirectNodesNumMap.put(key, indirectNodesNumMap.get(key) + 1);
                    counter.add(node);
                    countEdges(key, node);
                }
            }
        }
        return counter;
    }

    public boolean isPrimeNumber(int num) {
        if (num == 1) {
            return false;
        }
        if (num == 2) {
            return true;
        }
        if (num % 2 == 0) {
            return false;
        }
        for (int i = 3;  i * i <= num; i+=2) {
            if (num % i == 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
//        int[][] edges = new int[][]{new int[]{1, 2}, new int[]{1, 3}, new int[]{2, 4}, new int[]{2, 5}};
        int[][] edges = new int[][]{new int[]{1, 2}, new int[]{4, 1}, new int[]{3, 4}};
        CountPaths main = new CountPaths();
//        System.out.println(main.countPaths(5, edges));
        System.out.println(main.countPaths(4, edges));
    }
}

性能

勉强通过。有时间再回来看看题解吧。

LCP24.数字游戏

目标

小扣在秋日市集入口处发现了一个数字游戏。主办方共有 N 个计数器,计数器编号为 0 ~ N-1。每个计数器上分别显示了一个数字,小扣按计数器编号升序将所显示的数字记于数组 nums。每个计数器上有两个按钮,分别可以实现将显示数字加一或减一。小扣每一次操作可以选择一个计数器,按下加一或减一按钮。

主办方请小扣回答出一个长度为 N 的数组,第 i 个元素(0 <= i < N)表示将 0~i 号计数器 初始 所示数字操作成满足所有条件 nums[a]+1 == nums[a+1],(0 <= a < i) 的最小操作数。回答正确方可进入秋日市集。

由于答案可能很大,请将每个最小操作数对 1,000,000,007 取余。

思路

老实说这道题我花了好一会儿才弄清楚题目要求,最后解法还因为超时没有通过。

先解释一下目标吧,实际上需要求解的是每一个位置上的局部最优解。所谓局部指仅考虑当前位置及之前的所有位置的操作次数。所谓最优是指初始序列达到满足条件的状态(公差为1的等差数列)时所做操作总和的最小值。如果有N个计数器,那么就有N个满足条件的序列,有可能前面操作最少的序列并不是后面操作最少的序列,即前面位置的总操作次数可能并不是当前最优解的一部分。可以参考上面图片的示例3。

我的思路就是直接暴力破解,循环遍历输入的数组,在第i次遍历中,分别将(0 ~ i-1)位置上的元素作为基点k,左侧的序列依次减1,右侧的依次加1。在第k个序列中求解(0 ~ i-1)位置上操作次数的累加和。在第j个位置上的操作次数为 nums[k]-(k-j)。

代码

public static int[] numsGame2(int[] nums) {
    int[] res = new int[nums.length];
    // 累加k序列的操作总和
    int[] acc = new int[nums.length];
    for (int i = 1; i < nums.length; i++) {
        for (int k = 0; k <= i; k++) {
            int temp = acc[k];
            // 增加一些判断跳过一些计算
            if(temp >= res[i] && res[i] != 0){
                continue;
            }
            // 这里看似套了3个循环,但其实只有在k序列第一次出现时才会循环i次,那么前面的i-1次只需从acc中累计即可,实际时间复杂度是O(n^2)
            for (int j = temp == 0 ? 0 : i; j <= i; j++) {
                temp += Math.abs(nums[j] - (nums[k] - (k - j)));
            }
            acc[k] = temp;
            if (k == 0) {
                res[i] = temp;
            } else {
                res[i] = Math.min(res[i], temp);
            }
        }
        res[i] = res[i] % 1000000007;
    }
    return res;
}
acc a b c d
acc[0] 0 |b-a-1| |c-a-2| |d-a-3\
acc[1] |a-b+1| 0 |c-b-1| |d-b-2\
acc[2] |a-c+2| |b-c+1| 0 |d-c-1\
acc[3] |a-d+3| |b-d+2| |c-d+1| 0

如上表所示,如果序列是[a,b,c,d],基点k等于c的时候,前2次从acc累加,最后一次需要重新累加。时间复杂度为O(n^2)。这和我们常见的外层循环N次,内层循环N次不同。循环的计算次数序列为1、3、5、7...,根据等差数列求和公式:Sn = n × a1 + (n × (n-1)/2) × d,当 d=2a1=1 时,Sn = n^2

性能

由于题目给出的数组最大长度是10^5,暴力求解是不可行的。也尝试了增加条件判断跳过一些计算但还是无法降低问题的规模。于是就开始怀疑是否前面的计算结果是否与后面的计算有关联,可以简化后面的计算?更优的算法复杂度应为O(nlogn)O(logn)O(n)。我思考了很久,应该是存在知识盲区了。我去看了答案,涉及到求中位数。其实一开始有想过中位数,方差均值这些,但是没想到和这个最优解有什么关系。今天没时间了,抽空再看看吧。