【题解】—— LeetCode一周小结16

简介: 【题解】—— LeetCode一周小结16

上接:【题解】—— LeetCode一周小结15

15.设计哈希映射

题目链接:706. 设计哈希映射

不使用任何内建的哈希表库设计一个哈希映射(HashMap)。

实现 MyHashMap 类:

MyHashMap() 用空映射初始化对象

void put(int key, int value) 向 HashMap 插入一个键值对 (key, value) 。如果 key 已经存在于映射中,则更新其对应的值 value 。

int get(int key) 返回特定的 key 所映射的 value ;如果映射中不包含 key 的映射,返回 -1 。

void remove(key) 如果映射中存在 key 的映射,则移除 key 和它所对应的 value 。

示例:

输入:

[“MyHashMap”, “put”, “put”, “get”, “get”, “put”, “get”, “remove”,

“get”]

[[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]]

输出:

[null, null, null, 1, -1, null, 1, null, -1]

解释:

MyHashMap myHashMap = new MyHashMap();

myHashMap.put(1, 1); // myHashMap 现在为 [[1,1]]

myHashMap.put(2, 2); // myHashMap 现在为 [[1,1], [2,2]]

myHashMap.get(1); // 返回 1 ,myHashMap 现在为 [[1,1], [2,2]]

myHashMap.get(3); // 返回 -1(未找到),myHashMap 现在为 [[1,1], [2,2]]

myHashMap.put(2, 1); // myHashMap 现在为 [[1,1], [2,1]](更新已有的值)

myHashMap.get(2); // 返回 1 ,myHashMap 现在为 [[1,1], [2,1]]

myHashMap.remove(2); // 删除键为 2 的数据,myHashMap 现在为 [[1,1]]

myHashMap.get(2); // 返回 -1(未找到),myHashMap 现在为 [[1,1]]

提示:

0 <= key, value <= 106

最多调用 104 次 put、get 和 remove 方法

题解:

方法:链表

       

class MyHashMap {
    private class Pair {
        private int key;
        private int value;

        public Pair(int key, int value) {
            this.key = key;
            this.value = value;
        }

        public int getKey() {
            return key;
        }

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }
    }

    private static final int BASE = 769;
    private LinkedList[] data;

    /** Initialize your data structure here. */
    public MyHashMap() {
        data = new LinkedList[BASE];
        for (int i = 0; i < BASE; ++i) {
            data[i] = new LinkedList<Pair>();
        }
    }
    
    /** value will always be non-negative. */
    public void put(int key, int value) {
        int h = hash(key);
        Iterator<Pair> iterator = data[h].iterator();
        while (iterator.hasNext()) {
            Pair pair = iterator.next();
            if (pair.getKey() == key) {
                pair.setValue(value);
                return;
            }
        }
        data[h].offerLast(new Pair(key, value));
    }
    
    /** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
    public int get(int key) {
        int h = hash(key);
        Iterator<Pair> iterator = data[h].iterator();
        while (iterator.hasNext()) {
            Pair pair = iterator.next();
            if (pair.getKey() == key) {
                return pair.value;
            }
        }
        return -1;
    }
    
    /** Removes the mapping of the specified value key if this map contains a mapping for the key */
    public void remove(int key) {
        int h = hash(key);
        Iterator<Pair> iterator = data[h].iterator();
        while (iterator.hasNext()) {
            Pair pair = iterator.next();
            if (pair.key == key) {
                data[h].remove(pair);
                return;
            }
        }
    }

    private static int hash(int key) {
        return key % BASE;
    }
}

16.尽量减少恶意软件的传播

题目链接:924. 尽量减少恶意软件的传播

给出了一个由 n 个节点组成的网络,用 n × n 个邻接矩阵图 graph 表示。在节点网络中,当 graph[i][j] = 1 时,表示节点 i 能够直接连接到另一个节点 j。

一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。

假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。

如果从 initial 中移除某一节点能够最小化 M(initial), 返回该节点。如果有多个节点满足条件,就返回索引最小的节点。

请注意,如果某个节点已从受感染节点的列表 initial 中删除,它以后仍有可能因恶意软件传播而受到感染。

示例 1:

输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]

输出:0

示例 2:

输入:graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2]

输出:0

示例 3:

输入:graph = [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2]

输出:1

提示:

n == graph.length

n == graph[i].length

2 <= n <= 300

graph[i][j] == 0 或 1.

graph[i][j] == graph[j][i]

graph[i][i] == 1

1 <= initial.length <= n

0 <= initial[i] <= n - 1

initial 中所有整数均不重复

题解:

方法:深度优先搜索 图 递归

       

class Solution {
    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        boolean[] vis = new boolean[n];
        boolean[] isInitial = new boolean[n];
        int mn = Integer.MAX_VALUE;
        for (int x : initial) {
            isInitial[x] = true;
            mn = Math.min(mn, x);
        }

        int ans = -1;
        int maxSize = 0;
        for (int x : initial) {
            if (vis[x]) {
                continue;
            }
            nodeId = -1;
            size = 0;
            dfs(x, graph, vis, isInitial);
            if (nodeId >= 0 && (size > maxSize || size == maxSize && nodeId < ans)) {
                ans = nodeId;
                maxSize = size;
            }
        }
        return ans < 0 ? mn : ans;
    }

    private int nodeId, size;

    private void dfs(int x, int[][] graph, boolean[] vis, boolean[] isInitial) {
        vis[x] = true;
        size++;
        // 按照状态机更新 nodeId
        if (nodeId != -2 && isInitial[x]) {
            nodeId = nodeId == -1 ? x : -2;
        }
        for (int y = 0; y < graph[x].length; y++) {
            if (graph[x][y] == 1 && !vis[y]) {
                dfs(y, graph, vis, isInitial);
            }
        }
    }
}

17.尽量减少恶意软件的传播 II

题目链接:928. 尽量减少恶意软件的传播 II

给定一个由 n 个节点组成的网络,用 n x n 个邻接矩阵 graph 表示。在节点网络中,只有当 graph[i][j] = 1 时,节点 i 能够直接连接到另一个节点 j。

一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。

假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。

我们可以从 initial 中删除一个节点,并完全移除该节点以及从该节点到任何其他节点的任何连接。

请返回移除后能够使 M(initial) 最小化的节点。如果有多个节点满足条件,返回索引 最小的节点 。

示例 1:

输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]

输出:0

示例 2:

输入:graph = [[1,1,0],[1,1,1],[0,1,1]], initial = [0,1]

输出:1

示例 3:

输入:graph = [[1,1,0,0],[1,1,1,0],[0,1,1,1],[0,0,1,1]], initial = [0,1]

输出:1

提示:

n == graph.length

n == graph[i].length

2 <= n <= 300

graph[i][j] 是 0 或 1.

graph[i][j] == graph[j][i]

graph[i][i] == 1

1 <= initial.length < n

0 <= initial[i] <= n - 1

initial 中每个整数都不同

题解:

方法:并查集 图 哈希表

       

class UnionFind {
    private final int[] p;
    private final int[] size;

    public UnionFind(int n) {
        p = new int[n];
        size = new int[n];
        for (int i = 0; i < n; ++i) {
            p[i] = i;
            size[i] = 1;
        }
    }

    public int find(int x) {
        if (p[x] != x) {
            p[x] = find(p[x]);
        }
        return p[x];
    }

    public boolean union(int a, int b) {
        int pa = find(a), pb = find(b);
        if (pa == pb) {
            return false;
        }
        if (size[pa] > size[pb]) {
            p[pb] = pa;
            size[pa] += size[pb];
        } else {
            p[pa] = pb;
            size[pb] += size[pa];
        }
        return true;
    }

    public int size(int root) {
        return size[root];
    }
}

class Solution {
    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        boolean[] s = new boolean[n];
        for (int i : initial) {
            s[i] = true;
        }
        UnionFind uf = new UnionFind(n);
        for (int i = 0; i < n; ++i) {
            if (!s[i]) {
                for (int j = i + 1; j < n; ++j) {
                    if (graph[i][j] == 1 && !s[j]) {
                        uf.union(i, j);
                    }
                }
            }
        }
        Set<Integer>[] g = new Set[n];
        Arrays.setAll(g, k -> new HashSet<>());
        int[] cnt = new int[n];
        for (int i : initial) {
            for (int j = 0; j < n; ++j) {
                if (!s[j] && graph[i][j] == 1) {
                    g[i].add(uf.find(j));
                }
            }
            for (int root : g[i]) {
                ++cnt[root];
            }
        }
        int ans = 0, mx = -1;
        for (int i : initial) {
            int t = 0;
            for (int root : g[i]) {
                if (cnt[root] == 1) {
                    t += uf.size(root);
                }
            }
            if (t > mx || (t == mx && i < ans)) {
                ans = i;
                mx = t;
            }
        }
        return ans;
    }
}

18.从双倍数组中还原原数组

题目链接:2007. 从双倍数组中还原原数组

一个整数数组 original 可以转变成一个 双倍 数组 changed ,转变方式为将 original 中每个元素 值乘以 2 加入数组中,然后将所有元素 随机打乱 。

给你一个数组 changed ,如果 change 是 双倍 数组,那么请你返回 original数组,否则请返回空数组。original 的元素可以以 任意 顺序返回。

示例 1:

输入:changed = [1,3,4,2,6,8]

输出:[1,3,4]

解释:一个可能的 original 数组为 [1,3,4] :

  • 将 1 乘以 2 ,得到 1 * 2 = 2 。
  • 将 3 乘以 2 ,得到 3 * 2 = 6 。
  • 将 4 乘以 2 ,得到 4 * 2 = 8 。

其他可能的原数组方案为 [4,3,1] 或者 [3,1,4] 。

示例 2:

输入:changed = [6,3,0,1]

输出:[]

解释:changed 不是一个双倍数组。

示例 3:

输入:changed = [1]

输出:[]

解释:changed 不是一个双倍数组。

提示:

1 <= changed.length <= 105

0 <= changed[i] <= 105

题解:

方法:排序

       

class Solution {
    public int[] findOriginalArray(int[] changed) {
        int n = changed.length;
        Arrays.sort(changed);
        int[] cnt = new int[changed[n - 1] + 1];
        for (int x : changed) {
            ++cnt[x];
        }
        int[] ans = new int[n >> 1];
        int i = 0;
        for (int x : changed) {
            if (cnt[x] == 0) {
                continue;
            }
            --cnt[x];
            int y = x << 1;
            if (y >= cnt.length || cnt[y] <= 0) {
                return new int[0];
            }
            --cnt[y];
            ans[i++] = x;
        }
        return ans;
    }
}

19.准时抵达会议现场的最小跳过休息次数

题目链接:1883. 准时抵达会议现场的最小跳过休息次数

给你一个整数 hoursBefore ,表示你要前往会议所剩下的可用小时数。要想成功抵达会议现场,你必须途经 n 条道路。道路的长度用一个长度为 n 的整数数组 dist 表示,其中 dist[i] 表示第 i 条道路的长度(单位:千米)。另给你一个整数 speed ,表示你在道路上前进的速度(单位:千米每小时)。

当你通过第 i 条路之后,就必须休息并等待,直到 下一个整数小时 才能开始继续通过下一条道路。注意:你不需要在通过最后一条道路后休息,因为那时你已经抵达会议现场。

例如,如果你通过一条道路用去 1.4 小时,那你必须停下来等待,到 2 小时才可以继续通过下一条道路。如果通过一条道路恰好用去 2 小时,就无需等待,可以直接继续。

然而,为了能准时到达,你可以选择 跳过 一些路的休息时间,这意味着你不必等待下一个整数小时。注意,这意味着与不跳过任何休息时间相比,你可能在不同时刻到达接下来的道路。

例如,假设通过第 1 条道路用去 1.4 小时,且通过第 2 条道路用去 0.6 小时。跳过第 1 条道路的休息时间意味着你将会在恰好 2 小时完成通过第 2 条道路,且你能够立即开始通过第 3 条道路。

返回准时抵达会议现场所需要的 最小跳过次数 ,如果 无法准时参会 ,返回 -1 。

示例 1:

输入:dist = [1,3,2], speed = 4, hoursBefore = 2

输出:1

解释:

不跳过任何休息时间,你将用 (1/4 + 3/4) + (3/4 + 1/4) + (2/4) = 2.5 小时才能抵达会议现场。

可以跳过第 1 次休息时间,共用 ((1/4 + 0) + (3/4 + 0)) + (2/4) = 1.5 小时抵达会议现场。

注意,第 2 次休息时间缩短为 0 ,由于跳过第 1 次休息时间,你是在整数小时处完成通过第 2 条道路。

示例 2:

输入:dist = [7,3,5,5], speed = 2, hoursBefore = 10

输出:2

解释:

不跳过任何休息时间,你将用 (7/2 + 1/2) + (3/2 + 1/2) + (5/2 + 1/2) + (5/2) = 11.5

小时才能抵达会议现场。

可以跳过第 1 次和第 3 次休息时间,共用 ((7/2 + 0) + (3/2 + 0)) + ((5/2 + 0) + (5/2)) =

10 小时抵达会议现场。

示例 3:

输入:dist = [7,3,5,5], speed = 1, hoursBefore = 10

输出:-1

解释:即使跳过所有的休息时间,也无法准时参加会议。

提示:

n == dist.length

1 <= n <= 1000

1 <= dist[i] <= 105

1 <= speed <= 106

1 <= hoursBefore <= 107

题解:

方法:动态规划

       

class Solution {
    public int minSkips(int[] dist, int speed, int hoursBefore) {
        int sumDist = 0;
        for (int d : dist) {
            sumDist += d;
        }
        if (sumDist > (long) speed * hoursBefore) {
            return -1;
        }

        int n = dist.length;
        int[][] memo = new int[n][n];
        for (int[] row : memo) {
            Arrays.fill(row, -1); // -1 表示没有计算过
        }
        for (int i = 0; ; i++) {
            if (dfs(i, n - 2, memo, dist, speed) + dist[n - 1] <= (long) speed * hoursBefore) {
                return i;
            }
        }
    }

    private int dfs(int i, int j, int[][] memo, int[] dist, int speed) {
        if (j < 0) { // 递归边界
            return 0;
        }
        if (memo[i][j] != -1) { // 之前计算过
            return memo[i][j];
        }
        int res = (dfs(i, j - 1, memo, dist, speed) + dist[j] + speed - 1) / speed * speed;
        if (i > 0) {
            res = Math.min(res, dfs(i - 1, j - 1, memo, dist, speed) + dist[j]);
        }
        return memo[i][j] = res; // 记忆化
    }
}

20.组合总和

题目链接:39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7

输出:[[2,2,3],[7]]

解释:

2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。

7 也是一个候选, 7 = 7 。

仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8

输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1

输出: []

提示:

1 <= candidates.length <= 30

2 <= candidates[i] <= 40

candidates 的所有元素 互不相同

1 <= target <= 40

题解:

方法:枚举

       

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(0, target, candidates, ans, path);
        return ans;
    }

    private void dfs(int i, int left, int[] candidates, List<List<Integer>> ans, List<Integer> path) {
        if (left == 0) {
            // 找到一个合法组合
            ans.add(new ArrayList<>(path));
            return;
        }

        if (left < candidates[i]) {
            return;
        }

        // 枚举选哪个
        for (int j = i; j < candidates.length; j++) {
            path.add(candidates[j]);
            dfs(j, left - candidates[j], candidates, ans, path);
            path.remove(path.size() - 1); // 恢复现场
        }
    }
}

方法:完全背包

       

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        int n = candidates.length;
        // 完全背包
        boolean[][] f = new boolean[n + 1][target + 1];
        f[0][0] = true;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= target; j++) {
                f[i + 1][j] = f[i][j] || j >= candidates[i] && f[i + 1][j - candidates[i]];
            }
        }

        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        // 倒着递归,这样参数符合 f 数组的定义
        dfs(n - 1, target, candidates, f, ans, path);
        return ans;
    }

    private void dfs(int i, int left, int[] candidates, boolean[][] f, List<List<Integer>> ans, List<Integer> path) {
        if (left == 0) {
            // 找到一个合法组合
            ans.add(new ArrayList<>(path));
            return;
        }

        // 无法用下标在 [0, i] 中的数字组合出 left
        if (left < 0 || !f[i + 1][left]) {
            return;
        }

        // 不选
        dfs(i - 1, left, candidates, f, ans, path);

        // 选
        path.add(candidates[i]);
        dfs(i, left - candidates[i], candidates, f, ans, path);
        path.remove(path.size() - 1);
    }
}


21.组合总和 III

题目链接:216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

只使用数字1到9

每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7

输出: [[1,2,4]]

解释:

1 + 2 + 4 = 7

没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9

输出: [[1,2,6], [1,3,5], [2,3,4]]

解释:

1 + 2 + 6 = 9

1 + 3 + 5 = 9

2 + 3 + 4 = 9

没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1

输出: []

解释: 不存在有效的组合。

在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

提示:

2 <= k <= 9

1 <= n <= 60

题解:

方法:枚举

       

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> path = new ArrayList<>(k);
        dfs(9, n, k, ans, path);
        return ans;
    }

    private void dfs(int i, int t, int k, List<List<Integer>> ans, List<Integer> path) {
        int d = k - path.size(); // 还要选 d 个数
        if (t < 0 || t > (i * 2 - d + 1) * d / 2) { // 剪枝
            return;
        }
        if (d == 0) { // 找到一个合法组合
            ans.add(new ArrayList<>(path));
            return;
        }
        for (int j = i; j >= d; j--) {
            path.add(j);
            dfs(j - 1, t - j, k, ans, path);
            path.remove(path.size() - 1);
        }
    }
}

下接:【题解】—— LeetCode一周小结17



相关文章
|
6天前
|
人工智能 BI
|
1月前
|
人工智能 BI
|
1月前
|
人工智能 BI 测试技术
|
3月前
|
人工智能 BI
|
2月前
|
测试技术 索引
|
4月前
|
存储 测试技术 索引
|
4月前
|
索引
【题解】—— LeetCode一周小结17
【题解】—— LeetCode一周小结17
【题解】—— LeetCode一周小结8
【题解】—— LeetCode一周小结8