2713.矩阵中严格递增的单元格数

目标

给你一个下标从 1 开始、大小为 m x n 的整数矩阵 mat,你可以选择任一单元格作为 起始单元格 。

从起始单元格出发,你可以移动到 同一行或同一列 中的任何其他单元格,但前提是目标单元格的值 严格大于 当前单元格的值。

你可以多次重复这一过程,从一个单元格移动到另一个单元格,直到无法再进行任何移动。

请你找出从某个单元开始访问矩阵所能访问的 单元格的最大数量 。

返回一个表示可访问单元格最大数量的整数。

示例 1:

输入:mat = [[3,1],[3,4]]
输出:2
解释:上图展示了从第 1 行、第 2 列的单元格开始,可以访问 2 个单元格。可以证明,无论从哪个单元格开始,最多只能访问 2 个单元格,因此答案是 2 。

示例 2:

输入:mat = [[1,1],[1,1]]
输出:1
解释:由于目标单元格必须严格大于当前单元格,在本示例中只能访问 1 个单元格。 

示例 3:

输入:mat = [[3,1,6],[-9,5,7]]
输出:4
解释:上图展示了从第 2 行、第 1 列的单元格开始,可以访问 4 个单元格。可以证明,无论从哪个单元格开始,最多只能访问 4 个单元格,因此答案是 4 。  

说明:

  • m == mat.length
  • n == mat[i].length
  • 1 <= m, n <= 10^5
  • 1 <= m * n <= 10^5
  • -10^5 <= mat[i][j] <= 10^5

思路

有一个二维矩阵,我们可以从任意元素出发到达同行或同列的任意严格大于该元素值的位置,问我们最多能访问到多少单元格。

最直接的想法就是建立一个有向无环图,然后求最大路径长度。但是建图的过程需要循环mn(m+n)次,针对每个元素判断其同行同列上严格大于的元素。显然会超时。

于是考虑使用记忆化搜索,结果测试用例 558/566 超时,这个二维数组只有一行,有 100000列,从 1~100000,我在本地测试的时候报栈溢出。

我想要将其转为迭代的形式,但是时间紧迫,简单起见对一行或一列的情况做了特殊处理,排序后去重,最后勉强通过了。

官网题解使用的是动态规划,有时间详细看一下。//todo

代码

/**
 * @date 2024-06-19 16:28
 */
public class MaxIncreasingCells2713 {

    public int maxIncreasingCells(int[][] mat) {
        int res = 0;
        int m = mat.length;
        int n = mat[0].length;
        if (m == 1) {
            res = n;
            Arrays.sort(mat[0]);
            for (int i = 1; i < n; i++) {
                if (mat[0][i] == mat[0][i - 1]) {
                    res--;
                }
            }
            return res;
        } else if (n == 1) {
            res = m;
            Arrays.sort(mat, (a, b) -> a[0] - b[0]);
            for (int i = 1; i < m; i++) {
                if (mat[i][0] == mat[i - 1][0]) {
                    res--;
                }
            }
            return res;
        }

        int l = m * n;
        // 将二维坐标映射到一维,dp记录的是从该点为起点的能移动的最大次数
        int[] dp = new int[l];
        Arrays.fill(dp, -1);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                res = Math.max(res, move(mat, mat[i][j], i * n + j, i, j, dp));
            }
        }
        return res;
    }

    public int move(int[][] mat, int curVal, int next, int i, int j, int[] dp) {
        int m = mat.length;
        int n = mat[0].length;
        if (dp[next] > -1) {
            return dp[next];
        } else if (dp[next] == -2) {
            return 1;
        }
        boolean noNext = true;
        for (int k = 0; k < n; k++) {
            if (mat[i][k] > curVal) {
                noNext = false;
                dp[next] = Math.max(dp[next], move(mat, mat[i][k], i * n + k, i, k, dp) + 1);
            }
        }
        for (int k = 0; k < m; k++) {
            if (mat[k][j] > curVal) {
                noNext = false;
                dp[next] = Math.max(dp[next], move(mat, mat[k][j], k * n + j, k, j, dp) + 1);
            }
        }
        if (noNext) {
            dp[next] = -2;
            return 1;
        }

        return dp[next];
    }

}

性能

2813.子序列最大优雅度

目标

给你一个长度为 n 的二维整数数组 items 和一个整数 k 。

items[i] = [profiti, categoryi],其中 profiti 和 categoryi 分别表示第 i 个项目的利润和类别。

现定义 items 的 子序列 的 优雅度 可以用 total_profit + distinct_categories^2 计算,其中 total_profit 是子序列中所有项目的利润总和,distinct_categories 是所选子序列所含的所有类别中不同类别的数量。

你的任务是从 items 所有长度为 k 的子序列中,找出 最大优雅度 。

用整数形式表示并返回 items 中所有长度恰好为 k 的子序列的最大优雅度。

注意:数组的子序列是经由原数组删除一些元素(可能不删除)而产生的新数组,且删除不改变其余元素相对顺序。

示例 1:

输入:items = [[3,2],[5,1],[10,1]], k = 2
输出:17
解释:
在这个例子中,我们需要选出长度为 2 的子序列。
其中一种方案是 items[0] = [3,2] 和 items[2] = [10,1] 。
子序列的总利润为 3 + 10 = 13 ,子序列包含 2 种不同类别 [2,1] 。
因此,优雅度为 13 + 22 = 17 ,可以证明 17 是可以获得的最大优雅度。 

示例 2:

输入:items = [[3,1],[3,1],[2,2],[5,3]], k = 3
输出:19
解释:
在这个例子中,我们需要选出长度为 3 的子序列。 
其中一种方案是 items[0] = [3,1] ,items[2] = [2,2] 和 items[3] = [5,3] 。
子序列的总利润为 3 + 2 + 5 = 10 ,子序列包含 3 种不同类别 [1, 2, 3] 。 
因此,优雅度为 10 + 32 = 19 ,可以证明 19 是可以获得的最大优雅度。

示例 3:

输入:items = [[1,1],[2,1],[3,1]], k = 3
输出:7
解释:
在这个例子中,我们需要选出长度为 3 的子序列。
我们需要选中所有项目。
子序列的总利润为 1 + 2 + 3 = 6,子序列包含 1 种不同类别 [1] 。
因此,最大优雅度为 6 + 12 = 7 。

说明:

  • 1 <= items.length == n <= 10^5
  • items[i].length == 2
  • items[i][0] == profiti
  • items[i][1] == categoryi
  • 1 <= profiti <= 10^9
  • 1 <= categoryi <= n
  • 1 <= k <= n

思路

已知一个二维数组,元素为[利润, 种类],数组子序列的优雅值定义为利润和 + 不同种类数量^2,让我们求子序列最大的优雅值是多少。

这道题没有做出来,思考方向错了。刚开始想的是使用记忆化搜索,但是后来发现问题的解不一定能够从子问题得出。即 k-1 子序列的优雅值不一定能够得到 k 子序列的优雅值。

例如:{10, 5} -> {10, 2}, {6, 1}, {9, 5},k = 3,我们先固定第一项,然后从后面取 k2 的优雅值最大子序列 {10, 2}, {9, 5}。但是与第一项结合之后,发现类别有重复的,优雅值为 29 + 4 = 33,小于取 {10, 2}, {6, 1} 得到的优雅值 26 + 9 = 35

因此使用递归或者动态规划都是不可解的,不能转换成规模更小的子问题。 //2024.06.14 也有可能是可以解的,只不过没有找到正确的切入点。参考访问数组中的位置使分数最大

官网题解使用的是贪心算法,由于后面会对之前的贪心选择做出调整,有网友称为反悔贪心算法。

由于我们求的是优雅值,相对顺序没有影响,因此可以排序。

然后先取最大的k个值,如果其中类别有重复的,尝试用后面的不同类别替换类别重复但利润较小的,直到没有重复的即可。

这是357场周赛的最后一题,2500多分。

代码

//todo

1542.找出最长的超赞子字符串

目标

给你一个字符串 s 。请返回 s 中最长的 超赞子字符串 的长度。

「超赞子字符串」需满足满足下述两个条件:

  • 该字符串是 s 的一个非空子字符串
  • 进行任意次数的字符交换后,该字符串可以变成一个回文字符串

示例 1:

输入:s = "3242415"
输出:5
解释:"24241" 是最长的超赞子字符串,交换其中的字符后,可以得到回文 "24142"

示例 2:

输入:s = "12345678"
输出:1

示例 3:

输入:s = "213123"
输出:6
解释:"213123" 是最长的超赞子字符串,交换其中的字符后,可以得到回文 "231132"

示例 4:

输入:s = "00"
输出:2

说明:

  • 1 <= s.length <= 10^5
  • s 仅由数字组成

思路

卡在152/153超出时间限制,使用的是滑动窗口。

152测试用例是9999个1,9999个2,......,9999个9。

看了题解使用0-9十个数字保存奇偶性想到了,子串最多只包含一个奇数字符也想到了,使用前缀和也想到了,但是没想到将前缀和的状态压缩保存并通过哈希表记录。

代码

// todo

性能

1553.吃掉N个橘子的最少天数

有思路但是今天只剩下几十分钟了,有机会再做吧。

下面的代码超时了。// 5.13更新:这种贪心策略是不对的,不能优先选第三种策略。同时,最后一行的返回值minDays(n - 1)等于从0~n每一个值都要递归一遍,不可取。最后还要使用记忆化搜索。

/**
 * @date 2024-05-12 23:11
 */

public class MinDays1553 {

    @Deprecated
    public int minDays(int n) {
        int res = 0;
        if (n <= 0) {
            return 0;
        }
        if (n % 3 == 0) {
            res = minDays(n - 2 * (n / 3)) + 1;
        } else if (n % 2 == 0) {
            res = minDays(n - n / 2) + 1;
        } else {
            return minDays(n - 1) + 1;
        }
        return Math.min(res, minDays(n - 1) + 1);
    }
}

857.雇佣K名工人的最低成本

目标

有 n 名工人。 给定两个数组 quality 和 wage ,其中,quality[i] 表示第 i 名工人的工作质量,其最低期望工资为 wage[i] 。

现在我们想雇佣 k 名工人组成一个工资组。在雇佣 一组 k 名工人时,我们必须按照下述规则向他们支付工资:

  1. 对工资组中的每名工人,应当按其工作质量与同组其他工人的工作质量的比例来支付工资。
  2. 工资组中的每名工人至少应当得到他们的最低期望工资。

给定整数 k ,返回 组成满足上述条件的付费群体所需的最小金额 。在实际答案的 10^-5 以内的答案将被接受。

示例 1:

输入: quality = [10,20,5], wage = [70,50,30], k = 2
输出: 105.00000
解释: 我们向 0 号工人支付 70,向 2 号工人支付 35。

示例 2:

输入: quality = [3,1,10,10,1], wage = [4,8,2,2,7], k = 3
输出: 30.66667
解释: 我们向 0 号工人支付 4,向 2 号和 3 号分别支付 13.33333。

说明:

  • n == quality.length == wage.length
  • 1 <= k <= n <= 10^4
  • 1 <= quality[i], wage[i] <= 10^4

思路

从quality中选k个,按照工作质量比例支付工资,并且每人的工资不能低于wage中的最低期望工资,问雇佣这k个人的最低成本是多少。

// todo

代码

/**
 * @date 2024-05-02 20:44
 */
public class MincostToHireWorkers857 {
    public double mincostToHireWorkers(int[] quality, int[] wage, int k) {
        int n = wage.length;
        PriorityQueue<double[]> wqq = new PriorityQueue<>(Comparator.comparingDouble(x -> x[0]));
        double[][] wq = new double[n][2];
        for (int i = 0; i < wage.length; i++) {
            wq[i][0] = (double) wage[i] / quality[i];
            wq[i][1] = i;
            wqq.offer(wq[i]);
        }
        PriorityQueue<Integer> q = new PriorityQueue<>((x, y) -> y - x);
        int sum = 0;
        int ri = 0;
        for (int i = 0; i < k; i++) {
            double[] choose = wqq.poll();
            int index = (int) choose[1];
            sum += quality[index];
            q.offer(quality[index]);
            ri = (int) choose[1];
        }
        double res = (double) sum * wage[ri] / quality[ri];
        while (!wqq.isEmpty()) {
            double[] choose = wqq.poll();
            int index = (int) choose[1];
            if (q.peek() > quality[index]) {
                sum = sum - q.peek() + quality[index];
                double resTmp = (double)sum * wage[index] / quality[index];
                q.poll();
                q.offer(quality[index]);
                if (res > resTmp) {
                    res = resTmp;
                }
            }
        }
        return res;
    }
}

性能

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 中每个整数都不同

思路

这个和昨天的题的区别是移除节点之后原来连通的区域可能就断开了。刚开始想,昨天的需要排除掉同一连通区域存在多个感染节点的情况,今天这个就不能排除了。但是其影响的节点数也不能通过连通区域节点个数来计算。处理起来就比较复杂了,不能简单地根据直接相连的节点数来判断。当连通区域中含有多个感染节点时,需要区分边缘感染节点与中间感染节点,边缘感染节点又与孤立的感染节点相同,都是减少1。然后还要考虑连通区域仅有一个感染节点的情况。

区分 单一边缘感染节点 与 孤立感染节点

2、3 感染,返回3

0 - 1
|
3

2

无需区分 多个边缘感染节点 与 孤立感染节点

1、2、3 感染,返回1

0 - 1
|
3

2

区分 中间感染节点 与 孤立感染节点,并且不能仅根据直接相连的非感染节点来判断

0 、2、4、8 感染,返回8

     7
     | \
0    4 - 6
     |
1 -  8 - 3
     |
2    5

错了好多次,终于调试过了。正面确实不太好求解。总结一下就是:

  1. 连通区域存在多个感染节点
    • 去掉边缘的感染节点,感染节点总数减少1,全是边缘感染节点与包含中间感染节点是一样的
    • 去掉非边缘感染节点,需要dfs获取不含感染节点路径的节点总数
  2. 连通区域仅有1个感染节点(可以是孤立感染节点、边缘节点、中间节点)
    • 感染节点总数减少连通区域节点个数

最终答案需要获取以上减少最多的节点,如果存在多个,返回下标最小的。

代码里是按边缘感染节点与中间感染节点分的:

  1. 边缘感染节点
    • 孤立感染节点,减1
    • 连通区域内有多个边缘感染节点,减1
    • 连通区域内仅有一个边缘感染节点,减连通区域节点个数
  2. 中间感染节点(如果存在中间节点就不考虑边缘节点了,因为题目中限制了1 <= initial.length < n,一定存在可以减少2个的中间节点,分析到这里时我以为我发现了官网的漏洞,错误的实现也能通过,想要贡献测试用例呢,结果提示测试用例非有效值。如果是小于等于n这个解法就得多一个判断条件initial.length == n,直接取最小下标
    • 仅有一个中间感染节点,连通区域节点个数
    • 有多个中间感染节点,dfs搜索不含感染节点路径上的非感染节点个数,如果有感染节点,那么它也是减1,不过这里不再比较了,原因上面也说过了。

代码中d存的是中间节点(包括指向自身的边大于2),如果d为空则表示连通区域全是边缘感染节点(边为2),或孤立感染节点(边为1)。

对于全是边缘感染节点与孤立感染节点的情况,取下标最小即可。而对于中间感染节点,通过dfs来查找连通个数。如果通过dfs查找的个数为1,并且它还是中间感染节点,那么它周围全是感染节点。按道理来说,应该与边缘节点一起取最小下标。但是题目给出了限制,那么一定存在一个可以减少2的中间节点。

通过上面的分析只是说明了该问题正向分析的复杂性,如果不是不断尝试,很难直接把上面的所有情况想清楚。所以,上面的分析也没有太大的用处,过一段时间重做这个题,还是会踩坑。

官网题解使用的是逆向思维,统计的是从每个非感染节点出发不经过感染节点所经历的个数,在dfs过程中使用状态机来标识感染节点的个数。如果只遇到了1个感染节点,那么累加刚才遍历的节点个数,而如果有多个,那么就只能减少它自己。因此,如果存在只遇到一个感染节点的情况,就取个数最大的。否则取下标最小的。

其实,只遇到一个感染节点的情况包括了上面的单一边缘感染节点、中间单一感染节点以及多个中间感染节点(dfs非感染个数不为0的情况,即路径上不含有感染节点)的情况,而遇到多个感染节点,则说明被多个感染节点包围/半包围(对应全是边缘节点、边缘与中间、全中间,后面两种情况上面的算法忽略掉了),并且取最小下标直接包括了孤立感染节点。

可以发现同样是一步处理,我们赋予它不同的内涵,其所应对的场景就大为不同。

代码


/**
 * @date 2024-04-17 8:46
 */
public class MinMalwareSpread928 {
    public int[] u;
    TreeSet<Integer> s;
    HashSet<Integer> d = new HashSet<>();
    List<Integer>[] g;

    public void merge(int x, int y) {
        HashSet<Integer> tmp = new HashSet<>();
        int rx = find(x, tmp);
        int ry = find(y, tmp);
        d.addAll(tmp);
        if (s.contains(rx) && s.contains(ry)) {
            if (rx > ry) {
                u[rx] = ry;
            } else if (rx < ry) {
                u[ry] = rx;
            }
        } else if (s.contains(ry)) {
            u[rx] = ry;
        } else {
            u[ry] = rx;
        }
    }

    public int find(int x, HashSet<Integer> tmp) {
        if (x != u[x]) {
            if (s.contains(x) && s.contains(u[x])) {
                if (g[x].size() > 2) {
                    tmp.add(x);
                }
                if (g[u[x]].size() > 2) {
                    tmp.add(u[x]);
                }
            }
            x = find(u[x], tmp);
        }
        return u[x];
    }

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

    public int count(int x) {
        int cnt = 0;
        int rt = find(x);
        for (int i = 0; i < u.length; i++) {
            if (rt == find(i)) {
                cnt++;
            }
        }
        return cnt;
    }

    public int countMalware(int x) {
        int cnt = 0;
        int rt = find(x);
        for (int i = 0; i < u.length; i++) {
            if (rt == find(i) && s.contains(i)) {
                cnt++;
            }
        }
        return cnt;
    }

    public int adjacencyUninfected(int x, int parent) {
        int cnt = 1;
        boolean[] visited = new boolean[u.length];
        for (Integer node : g[x]) {
            if (parent == node || node == x || visited[node]) {
                continue;
            }
            visited = new boolean[u.length];
            if (!s.contains(node)) {
                int subCnt = dfs(node, x, visited);
                if (subCnt != 0) {
                    cnt += subCnt;
                }
            }
        }
        return cnt;
    }

    public int dfs(int x, int parent, boolean[] visited) {
        if (s.contains(x)) {
            return 0;
        }
        int cnt = 1;
        for (Integer node : g[x]) {
            if (parent == node || node == x || visited[node]) {
                visited[node] = true;
                continue;
            }
            visited[node] = true;
            if (s.contains(node)) {
                return 0;
            }
            int subCnt = dfs(node, x, visited);
            if (subCnt == 0) {
                return 0;
            } else {
                cnt += subCnt;
            }
        }
        return cnt;
    }

    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        g = new ArrayList[n];
        u = new int[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>(n);
            u[i] = i;
        }

        s = new TreeSet<>();
        for (int i : initial) {
            s.add(i);
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (graph[i][j] == 1) {
                    g[i].add(j);
                    merge(i, j);
                }
            }
        }
        int res = Integer.MAX_VALUE;
        int tmp = Integer.MAX_VALUE;
        TreeSet<Integer> ini = new TreeSet<>((x, y) -> count(y) - count(x) == 0 ? (adjacencyUninfected(y, -1) - adjacencyUninfected(x, -1) == 0 ? x - y : adjacencyUninfected(y, -1) - adjacencyUninfected(x, -1)) : count(y) - count(x));
        if (d.isEmpty()) {
            // d为空表示连通区域仅有1个感染节点
            for (int i : initial) {
                if (countMalware(i) == 1 && count(i) > 1) {
                    // 连通区域节点数大于1
                    if (tmp == Integer.MAX_VALUE) {
                        tmp = i;
                    } else {
                        int ci = count(i);
                        int ct = count(tmp);
                        if (ci > ct) {
                            // 取连通区域节点数大的
                            tmp = i;
                        } else if (ci == ct) {
                            // 如果相等取下标小的
                            tmp = Math.min(i, tmp);
                        }
                    }
                } else {
                    // 对于孤立节点,直接取索引最小的即可
                    res = Math.min(i, res);
                }
            }
            // 如果全部是孤立节点,取res,否则取tmp
            return tmp == Integer.MAX_VALUE ? res : tmp;
        } else {
            ini.addAll(d);
        }

        return ini.first();
    }
}

性能