阅读视图

发现新文章,点击刷新页面。

每日一题-可以被 K 整除连通块的最大数目🔴

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

同时给你一个下标从 0 开始长度为 n 的整数数组 values ,其中 values[i] 是第 i 个节点的  。再给你一个整数 k 。

你可以从树中删除一些边,也可以一条边也不删,得到若干连通块。一个 连通块的值 定义为连通块中所有节点值之和。如果所有连通块的值都可以被 k 整除,那么我们说这是一个 合法分割 。

请你返回所有合法分割中,连通块数目的最大值 。

 

示例 1:

输入:n = 5, edges = [[0,2],[1,2],[1,3],[2,4]], values = [1,8,1,4,4], k = 6
输出:2
解释:我们删除节点 1 和 2 之间的边。这是一个合法分割,因为:
- 节点 1 和 3 所在连通块的值为 values[1] + values[3] = 12 。
- 节点 0 ,2 和 4 所在连通块的值为 values[0] + values[2] + values[4] = 6 。
最多可以得到 2 个连通块的合法分割。

示例 2:

输入:n = 7, edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]], values = [3,0,6,1,5,2,1], k = 3
输出:3
解释:我们删除节点 0 和 2 ,以及节点 0 和 1 之间的边。这是一个合法分割,因为:
- 节点 0 的连通块的值为 values[0] = 3 。
- 节点 2 ,5 和 6 所在连通块的值为 values[2] + values[5] + values[6] = 9 。
- 节点 1 ,3 和 4 的连通块的值为 values[1] + values[3] + values[4] = 6 。
最多可以得到 3 个连通块的合法分割。

 

提示:

  • 1 <= n <= 3 * 104
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • values.length == n
  • 0 <= values[i] <= 109
  • 1 <= k <= 109
  • values 之和可以被 k 整除。
  • 输入保证 edges 是一棵无向树。

判断子树点权和是否为 k 的倍数(Python/Java/C++/Go/JS/Rust)

最大化连通块的数目,等价于最大化删除的边数加一。

什么样的边可以删除?

如果 $x$ 和 $y$ 都是 $k$ 的倍数,那么 $x+y$ 也是 $k$ 的倍数。比如 $3$ 和 $6$ 都是 $3$ 的倍数,那么 $3+6=9$ 也是 $3$ 的倍数。

反过来说(逆否命题),如果 $x+y$ 不是 $k$ 的倍数,那么 $x$ 和 $y$ 不全是 $k$ 的倍数。不是 $k$ 的倍数的数,继续拆分,始终存在一个不是 $k$ 的倍数的数。

对应到删边上,删除一条边后,我们把一个连通块分成了两个连通块。如果其中一个连通块的点权和不是 $k$ 的倍数,那么这个连通块无论如何分割,始终存在一个点权和不是 $k$ 的倍数的连通块。所以当且仅当这两个连通块的点权和都是 $k$ 的倍数,这条边才能删除。

删除后,由于分割出的连通块点权和仍然是 $k$ 的倍数,所以可以继续分割,直到无法分割为止。换句话说,只要有能删除的边,就删除。

如何找到可以删除的边?

删除一条边后,我们把一个连通块分成了两个连通块。由于题目保证整棵树的点权和是 $k$ 的倍数,所以只需看其中一个连通块的点权和是否为 $k$ 的倍数。

从任意点出发 DFS 这棵树。计算子树 $x$ 的点权和 $s$,如果 $s$ 是 $k$ 的倍数,那么可以删除 $x$ 到其父节点这条边。注意根节点没有父节点。

连通块的数目等于删除的边数加一。可以把根节点到其父节点这条边(虽然不存在)也算上,这样答案就是删除的边数。

class Solution:
    def maxKDivisibleComponents(self, n: int, edges: List[List[int]], values: List[int], k: int) -> int:
        g = [[] for _ in range(n)]
        for x, y in edges:
            g[x].append(y)
            g[y].append(x)

        # 返回子树 x 的点权和
        def dfs(x: int, fa: int) -> int:
            s = values[x]
            for y in g[x]:
                if y != fa:  # 避免访问父节点
                    # 加上子树 y 的点权和,得到子树 x 的点权和
                    s += dfs(y, x)  
            nonlocal ans
            ans += s % k == 0
            return s

        ans = 0
        dfs(0, -1)
        return ans
class Solution {
    private int ans;

    public int maxKDivisibleComponents(int n, int[][] edges, int[] values, int k) {
        List<Integer>[] g = new ArrayList[n];
        Arrays.setAll(g, _ -> new ArrayList<>());
        for (int[] e : edges) {
            int x = e[0];
            int y = e[1];
            g[x].add(y);
            g[y].add(x);
        }

        dfs(0, -1, g, values, k);
        return ans;
    }

    // 返回子树 x 的点权和
    private long dfs(int x, int fa, List<Integer>[] g, int[] values, int k) {
        long sum = values[x];
        for (int y : g[x]) {
            if (y != fa) { // 避免访问父节点
                // 加上子树 y 的点权和,得到子树 x 的点权和
                sum += dfs(y, x, g, values, k);
            }
        }
        ans += sum % k == 0 ? 1 : 0;
        return sum;
    }
}
class Solution {
public:
    int maxKDivisibleComponents(int n, vector<vector<int>>& edges, vector<int>& values, int k) {
        vector<vector<int>> g(n);
        for (auto& e : edges) {
            int x = e[0], y = e[1];
            g[x].push_back(y);
            g[y].push_back(x);
        }

        int ans = 0;

        // 返回子树 x 的点权和
        auto dfs = [&](this auto&& dfs, int x, int fa) -> long long {
            long long sum = values[x];
            for (int y : g[x]) {
                if (y != fa) { // 避免访问父节点
                    // 加上子树 y 的点权和,得到子树 x 的点权和
                    sum += dfs(y, x);
                }
            }
            ans += sum % k == 0;
            return sum;
        };

        dfs(0, -1);
        return ans;
    }
};
func maxKDivisibleComponents(n int, edges [][]int, values []int, k int) (ans int) {
g := make([][]int, n)
for _, e := range edges {
x, y := e[0], e[1]
g[x] = append(g[x], y)
g[y] = append(g[y], x)
}

// 返回子树 x 的点权和
var dfs func(int, int) int
dfs = func(x, fa int) int {
s := values[x]
for _, y := range g[x] {
if y != fa { // 避免访问父节点
// 加上子树 y 的点权和,得到子树 x 的点权和
s += dfs(y, x)
}
}
if s%k == 0 {
ans++
}
return s
}

dfs(0, -1)
return
}
var maxKDivisibleComponents = function(n, edges, values, k) {
    const g = Array.from({ length: n }, () => []);
    for (const [x, y] of edges) {
        g[x].push(y);
        g[y].push(x);
    }

    let ans = 0;

    // 返回子树 x 的点权和
    function dfs(x, fa) {
        let sum = values[x];
        for (const y of g[x]) {
            if (y !== fa) { // 避免访问父节点
                // 加上子树 y 的点权和,得到子树 x 的点权和
                sum += dfs(y, x);
            }
        }
        ans += sum % k === 0 ? 1 : 0;
        return sum;
    }

    dfs(0, -1);
    return ans;
};
impl Solution {
    pub fn max_k_divisible_components(n: i32, edges: Vec<Vec<i32>>, values: Vec<i32>, k: i32) -> i32 {
        let n = n as usize;
        let mut g = vec![vec![]; n];
        for e in edges {
            let x = e[0] as usize;
            let y = e[1] as usize;
            g[x].push(y);
            g[y].push(x);
        }

        // 返回子树 x 的点权和
        fn dfs(x: usize, fa: usize, g: &[Vec<usize>], values: &[i32], k: i64, ans: &mut i32) -> i64 {
            let mut sum = values[x] as i64;
            for &y in &g[x] {
                if y != fa { // 避免访问父节点
                    // 加上子树 y 的点权和,得到子树 x 的点权和
                    sum += dfs(y, x, g, values, k, ans);
                }
            }
            if sum % k == 0 {
                *ans += 1;
            }
            sum
        }

        let mut ans = 0;
        dfs(0, 0, &g, &values, k as i64, &mut ans);
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

相似题目

2440. 创建价值相同的连通块

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌