494.目标和

目标

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

说明:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

思路

有一个数组,可以在数组元素前加上正负号来组成表达式,问表达式等于target的数目。

如果当前元素为正则累加,否则相减,递归直到所有元素都已列入表达式,如果累加结果等于target则返回1,否则返回0。

//todo 改为递推,或记忆化搜索

代码

/**
 * @date 2024-06-30 20:07
 */
public class FindTargetSumWays494 {
    public int findTargetSumWays(int[] nums, int target) {
        return dfs(nums, 1, nums[0], target) + dfs(nums, 1, -nums[0], target);
    }

    public int dfs(int[] nums, int i, int res, int target) {
        if (i == nums.length) {
            return res - target == 0 ? 1 : 0;
        }
        return dfs(nums, i + 1, res + nums[i], target) + dfs(nums, i + 1, res - nums[i], target);
    }

}

性能

2734.执行子串操作后的字典序最小字符串

目标

给你一个仅由小写英文字母组成的字符串 s 。在一步操作中,你可以完成以下行为:

  • 选择 s 的任一非空子字符串,可能是整个字符串,接着将字符串中的每一个字符替换为英文字母表中的前一个字符。例如,'b' 用 'a' 替换,'a' 用 'z' 替换。

返回执行上述操作 恰好一次 后可以获得的 字典序最小 的字符串。

子字符串 是字符串中的一个连续字符序列。

现有长度相同的两个字符串 x 和 字符串 y ,在满足 x[i] != y[i] 的第一个位置 i 上,如果 x[i] 在字母表中先于 y[i] 出现,则认为字符串 x 比字符串 y 字典序更小 。

示例 1:

输入:s = "cbabc"
输出:"baabc"
解释:我们选择从下标 0 开始、到下标 1 结束的子字符串执行操作。 
可以证明最终得到的字符串是字典序最小的。

示例 2:

输入:s = "acbbc"
输出:"abaab"
解释:我们选择从下标 1 开始、到下标 4 结束的子字符串执行操作。
可以证明最终得到的字符串是字典序最小的。

示例 3:

输入:s = "leetcode"
输出:"kddsbncd"
解释:我们选择整个字符串执行操作。
可以证明最终得到的字符串是字典序最小的。

说明:

  • 1 <= s.length <= 3 * 10^5
  • s 仅由小写英文字母组成

思路

求对一个字符串的 非空子串 进行操作后字典序最小的字符串,两个字符串 第一个不同的字母 在字母表 越先出现 其字典序就越小。字符串仅由小写字母组成,可进行的操作指将子串的每一个字母替换为其在字母表中的前一个字母,a 前面的字母定义为 z

关键在于非空子串如何选,根据题意可知,如果子串不含字母 a 操作总能使字典序变小。字符串前面字符的字典序越小整个字符串的字典序就越小,因此可以从前向后遍历直到遇到字母 a 作为子串。如果字符串字母均为 a,操作总会使字典序变大,显然将最后一个 a 改为 z 可以使操作后的字符串字典序最小。

代码

/**
 * @date 2024-06-27 0:05
 */
public class SmallestString2734 {

    public String smallestString_v1(String s) {
        int i = 0;
        int n = s.length();
        char[] chars = s.toCharArray();
        while (i < n && chars[i] == 'a') {
            i++;
        }
        while (i < n && chars[i] != 'a') {
            chars[i++]--;
        }
        if (i == n && s.charAt(n - 1) == 'a') {
            chars[n - 1] = 'z';
        }
        return new String(chars);
    }

    public String smallestString_v2(String s) {
        int i = 0;
        int n = s.length();
        StringBuilder sb = new StringBuilder(s);
        while (i < n && s.charAt(i) == 'a') {
            i++;
        }
        if (i == n) {
            sb.setCharAt(n - 1, 'z');
            return sb.toString();
        }
        while (i < n && s.charAt(i) != 'a') {
            sb.setCharAt(i, (char) (s.charAt(i++) - 1));
        }
        return sb.toString();
    }
}

性能

使用 StringBuilder 显示用时更少,我试了一下与if判断的位置没关系,按道理来说直接数组访问比方法调用开销更小,new StringBuilder(s)s.toCharArray 都进行了数组拷贝。我能想到的解释就是大家都用的StringBuilder,然后这段代码被JIT编译器优化。

2741.特别的排列

目标

给你一个下标从 0 开始的整数数组 nums ,它包含 n 个 互不相同 的正整数。如果 nums 的一个排列满足以下条件,我们称它是一个特别的排列:

  • 对于 0 <= i < n - 1 的下标 i ,要么 nums[i] % nums[i+1] == 0 ,要么 nums[i+1] % nums[i] == 0 。

请你返回特别排列的总数目,由于答案可能很大,请将它对 109 + 7 取余 后返回。

示例 1:

输入:nums = [2,3,6]
输出:2
解释:[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。

示例 2:

输入:nums = [1,4,3]
输出:2
解释:[3,1,4] 和 [4,1,3] 是 nums 两个特别的排列。

说明:

  • 2 <= nums.length <= 14
  • 1 <= nums[i] <= 10^9

思路

有一个互不相同的正整数数组,问使得相邻元素可以被整除(对于相邻元素a % b == 0 || b % a == 0)的排列有多少种。

排列数的计算需要使用dfs,但如果不保存重复子问题的话会超时。

难点在于是否将保存的结果计入,例如 [2,6,3],虽然dfs 2 -> 6 -> 36 -> 2 -> 3有重复的子问题3,但是后者不符合题目条件。

// todo

代码

性能

503.下一个更大元素II – 单调栈

目标

给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。

数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

示例 1:

输入: nums = [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数; 
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

示例 2:

输入: nums = [1,2,3,4,3]
输出: [2,3,4,-1,4]

说明:

  • 1 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9

思路

暴力解法需要针对每一个元素向后搜索,时间复杂度为O(n^2)。这里面有一些重复的操作,例如,数组 [5,4,3,2,1,6] 找下一个更大的元素,从 5 向后搜索 与从 4 向后搜索有重复的部分 3 2 1。单调栈的核是这样处理的,先将第一个元素下标压入栈中,指针移到下一个元素,如果栈顶对应元素大于等于当前元素则将当前元素下标也压入栈中,否则弹栈,记录 res[stack.pop()] = 当前元素,直到栈顶元素大于等于当前元素。

针对这个题目,栈中保存的下标对应的元素值从栈底到栈顶是单调递减的,如果遇到第一个更大元素就弹栈了,因此称为单调栈。

这样做的好处是避免了重复的查找,它将重复搜索的部分使用栈保存了起来,这样就变成了从后向前搜索。如果栈顶元素遇到了第一个更大的元素,那么它也是前面同样小于该值的第一个更大元素,从而避免了重复查找。

这里对循环数组的处理是将原数组拼接到后面,下标进行取模运算。

代码

/**
 * @date 2024-06-24 0:18
 */
public class NextGreaterElements503 {

    public int[] nextGreaterElements_v4(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        Arrays.fill(res, -1);
        int[] stack = new int[n];
        int top = -1;
        for (int i = 0; i < 2 * n - 1; i++) {
            int index = i % n;
            while (top > -1 && nums[stack[top]] < nums[index]) {
                res[stack[top--]] = nums[index];
            }
            if (i < n) {
                stack[++top] = index;
            }
        }
        return res;
    }
}

性能

139.单词拆分

目标

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

说明:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

思路

已知一个字符串列表 wordDict 和一个字符串 s,问能否用列表中的元素拼成该字符串,列表中的元素可以重复使用。

很明显需要使用动态规划来求解,假设当前列表元素 word 的长度为 l,子字符串 sub 的长度为 i,如果 sub.substring(0, i-l) 能由字典中的词拼成并且 word.equals(sub.substring(i-l, l)) 那么 sub 也能由字典中的词拼成。

代码

/**
 * @date 2024-06-23 19:58
 */
public class WordBreak139 {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        for (int i = 1; i <= n; i++) {
            for (String word : wordDict) {
                int length = word.length();
                if (length <= i && dp[i - length] && word.equals(s.substring(i - length, i))) {
                    dp[i] = true;
                }
            }
        }
        return dp[n];
    }

    public boolean wordBreak_v1(String s, List<String> wordDict) {
        int n = s.length();
        char[] mem = new char[n + 1];
        Arrays.fill(mem, '2');
        return dfs(s, 0, wordDict, mem) == '1';
    }

    public char dfs(String s, int i, List<String> wordDict, char[] mem) {
        int n = s.length();
        if (i == n) {
            return '1';
        }
        if (mem[i] != '2') {
            return mem[i];
        }
        for (String word : wordDict) {
            if (s.startsWith(word, i) && '1' == dfs(s, i + word.length(), wordDict, mem)) {
                return mem[i] = '1';
            }
        }
        return mem[i] = '0';
    }
}

性能

最快的解法是使用记忆化搜索,可以剪枝缩小搜索范围。

2288.价格减免

目标

句子 是由若干个单词组成的字符串,单词之间用单个空格分隔,其中每个单词可以包含数字、小写字母、和美元符号 '$' 。如果单词的形式为美元符号后跟着一个非负数值(A word represents a price if it is a sequence of digits preceded by a dollar sign.),那么这个单词就表示一个 价格 。

  • 例如 "$100"、"$23" 和 "$6" 表示价格,而 "100"、"$" 和 "$1e5 不是。

给你一个字符串 sentence 表示一个句子和一个整数 discount 。对于每个表示价格的单词,都在价格的基础上减免 discount% ,并 更新 该单词到句子中。所有更新后的价格应该表示为一个 恰好保留小数点后两位 的数字。

返回表示修改后句子的字符串。

注意:所有价格 最多 为 10 位数字。

示例 1:

输入:sentence = "there are $1 $2 and 5$ candies in the shop", discount = 50
输出:"there are $0.50 $1.00 and 5$ candies in the shop"
解释:
表示价格的单词是 "$1" 和 "$2" 。 
- "$1" 减免 50% 为 "$0.50" ,所以 "$1" 替换为 "$0.50" 。
- "$2" 减免 50% 为 "$1" ,所以 "$1" 替换为 "$1.00" 。

示例 2:

输入:sentence = "1 2 $3 4 $5 $6 7 8$ $9 $10$", discount = 100
输出:"1 2 $0.00 4 $0.00 $0.00 7 8$ $0.00 $10$"
解释:
任何价格减免 100% 都会得到 0 。
表示价格的单词分别是 "$3"、"$5"、"$6" 和 "$9"。
每个单词都替换为 "$0.00"。

说明:

  • 1 <= sentence.length <= 105
  • sentence 由小写英文字母、数字、' ' 和 '$' 组成
  • sentence 不含前导和尾随空格
  • sentence 的所有单词都用单个空格分隔
  • 所有价格都是 正 整数且不含前导零
  • 所有价格 最多 为 10 位数字
  • 0 <= discount <= 100

思路

在许多脚本与命令中经常会使用 $+数字 形式的占位符来替换传入的参数。这个题就是要我们提取这个数字并进行一些操作然后再将处理后的数字替换回去。

知识点:

  • 如何保留两位小数,可以使用String.format("%.2f", 浮点数); 类似C语言的字符串格式化。DecimalFormat df = new DecimalFormat("0.00"); df.format(浮点数);
  • 如何判断是否为数字,Character.isDigit 该API处理了不同code point上的数字,这里我们只需处理ISO-LATIN-1字符集的数字即可。直接比较c >= '0' c <='9'即可。
  • 正则表达式断言,非捕获匹配以及捕获组的替换。匹配到的字符串被保存在group(0)group(1~n)可以获取正则表达式中的捕获组。值得注意的是断言并不会出现在匹配的字符串中,而非捕获匹配不能通过group(1~n) 访问,但还是会作为匹配字符被匹配到。appendReplacement会替换匹配到的字符串即group(0)

需要注意防止数字的溢出,以及边界条件的处理。

官网是根据空格拆分为单词,然后再对单词进行处理,最后使用String.join将单词合并。

有网友使用JS直接用正则表达式匹配数字,并使用匿名函数计算折扣并替换。

leetcode 中Java不能用java.util.regex包,但是可以使用 splitWord.matches("(?<=(^|\\s)\\$)(\\d+)(?=\\s|$)") 判断是否是目标形式,也算是一种判断是否为数字的手段。当然我们也可以不判断数字,直接使用Long.parseLong解析并捕获异常,不处理异常即可。

代码

/**
 * @date 2024-06-18 0:39
 */
public class DiscountPrices2288 {

    /**
     * 遇到$就记录随后的数字,如果遇到非数字就将其append到buffer
     * 如果在遇到分隔符' '之前都是数字,那么计算折扣然后append到buffer
     * 里面需要特别主要在同一个单词内含有多个'$'导致多次开启记录数字
     * 只有开头以及前一个字符是' '的情况下才开启记录数字
     * 另外需要在循环外处理最后一个单词的数字,如果存在的话
     * 还要特别主要数字溢出
     */
    public String discountPrices(String sentence, int discount) {
        // BigDecimal不让用
//        BigDecimal mul = new BigDecimal((100 - discount) / 100.0);
        double mul = (100 - discount) / 100.0;
        DecimalFormat df = new DecimalFormat("0.00");
        int n = sentence.length();
        boolean skip = false;
        StringBuilder sb = new StringBuilder();
        StringBuilder num = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char c = sentence.charAt(i);
            if (c != '$' || num.length() > 0) {
                if (!skip) {
                    sb.append(c);
                } else {
                    if (c >= '0' && c <= '9') {
                        num.append(c);
                    } else if (c == ' ' && num.length() > 0) {
                        // 这里应使用long
                        long origin = Long.parseLong(num.toString());
//                        BigDecimal discnt = new BigDecimal(origin).multiply(mul).setScale(2, BigDecimal.ROUND_HALF_UP);
                        double discnt = origin * mul;
                        sb.append(df.format(discnt)).append(c);
                        num = new StringBuilder();
                        skip = false;
                    } else {
                        sb.append(num).append(c);
                        num = new StringBuilder();
                        skip = false;
                    }
                }
            } else if (i == 0 || sentence.charAt(i - 1) == ' ') {
                // 这里不用else if(!skip),因为num.length >0 说明skip 为true
                skip = true;
                sb.append(c);
            } else {
                // 处理 $$$0 的情况
                if (skip) {
                    skip = false;
                }
                sb.append(c);
            }
        }
        // 处理结尾没有空格的情况
        if (num.length() > 0) {
            long origin = Long.parseLong(num.toString());
            double discnt = origin * mul;
            sb.append(df.format(discnt));
        }
        return sb.toString();
    }

}

性能

522.最长特殊序列ⅠⅠ

目标

给定字符串列表 strs ,返回其中 最长的特殊序列 的长度。如果最长特殊序列不存在,返回 -1 。

特殊序列 定义如下:该序列为某字符串 独有的子序列(即不能是其他字符串的子序列)。

s 的 子序列可以通过删去字符串 s 中的某些字符实现。

  • 例如,"abc" 是 "aebdc" 的子序列,因为您可以删除"aebdc"中的下划线字符来得到 "abc" 。"aebdc"的子序列还包括"aebdc"、 "aeb" 和 "" (空字符串)。

示例 1:

输入: strs = ["aba","cdc","eae"]
输出: 3

示例 2:

输入: strs = ["aaa","aaa","aa"]
输出: -1

说明:

  • 2 <= strs.length <= 50
  • 1 <= strs[i].length <= 10
  • strs[i] 只包含小写英文字母

思路

想要使用动态规划一定要先写弄清递推公式,否则思考的方向错了,编码会更加复杂。

这个题需要将每一个字符串与其它字符串比较,判断是否是其它字符串的子序列。

我们无法利用前面计算过的最长特殊序列来得出新的最长特殊序列,例如:

  • "aabbcc", "aabbcc", "cb", "abc" 前两个字符串没有特殊子序列,前三个字符串最长特殊子序列为 "cb",而单看 "cb" 与 "abc" 的最长特殊序列是 "abc",但 "abc" 是前面两个字符串的子序列。
  • "a", "b", "c", "d", "e", "f", "a", "b", "c", "d", "e", "f" 这组字符串如果不到最后一个,最长特殊子序列长度为1,如果整体来看子序列都不是唯一的。

//todo

代码

性能

2786.访问数组中的位置使分数最大

目标

给你一个下标从 0 开始的整数数组 nums 和一个正整数 x 。

你 一开始 在数组的位置 0 处,你可以按照下述规则访问数组中的其他位置:

  • 如果你当前在位置 i ,那么你可以移动到满足 i < j 的 任意 位置 j 。
  • 对于你访问的位置 i ,你可以获得分数 nums[i] 。
  • 如果你从位置 i 移动到位置 j 且 nums[i] 和 nums[j] 的 奇偶性 不同,那么你将失去分数 x 。

请你返回你能得到的 最大 得分之和。

注意 ,你一开始的分数为 nums[0] 。

示例 1:

输入:nums = [2,3,6,1,9,2], x = 5
输出:13
解释:我们可以按顺序访问数组中的位置:0 -> 2 -> 3 -> 4 。
对应位置的值为 2 ,6 ,1 和 9 。因为 6 和 1 的奇偶性不同,所以下标从 2 -> 3 让你失去 x = 5 分。
总得分为:2 + 6 + 1 + 9 - 5 = 13 。

示例 2:

输入:nums = [2,4,6,8], x = 3
输出:20
解释:数组中的所有元素奇偶性都一样,所以我们可以将每个元素都访问一次,而且不会失去任何分数。
总得分为:2 + 4 + 6 + 8 = 20 。

说明:

  • 2 <= nums.length <= 10^5
  • 1 <= nums[i], x <= 10^6

思路

给定一个数组 nums 与 正整数 x,从下标 0 开始,允许从任意位置 i 开始向后访问位置 j,如果nums[i]nums[j] 的奇偶性相同,则可以获得 nums[j] 分,否则获得 nums[j] - x 分。求能够获得的分数总和的最大值。

刚开始就想到要从后向前,自底向上动态规划,如果当前的奇偶性与与后面的奇偶性相同就累加,否则就将后面的值减去x。接着又想到并不是要每一个节点都要访问,如果节点没有访问奇偶性和谁比较呢?并且后面的得分取决于前一个元素的奇偶性,考虑到昨天的题 子序列最大优雅度,觉得可能方向又错了。

于是就尝试贪心算法,从下标0开始,执行while循环,如果后面的元素奇偶性与之相同,直接累加。对于奇偶性不同的,我们可以考虑累加或者跳过。这样问题就变成了从这个新位置开始向后能获取的最大分数。注意新的位置奇偶性发生了变化。

这么一想问题又变成记忆化搜索了,于是就可以转换为递推/动态规划问题。

// todo 转换为动态规划的写法

代码

/**
 * @date 2024-06-14 8:43
 */
public class MaxScore2786 {

    public long maxScore(int[] nums, int x) {
        int n = nums.length;
        long[][] mem = new long[n + 1][2];
        for (int i = 0; i < mem.length; i++) {
            mem[i] = new long[]{Integer.MIN_VALUE, Integer.MIN_VALUE};
        }
        long res = nums[0];
        int flag = nums[0] % 2;
        int i = 1;
        while (i < n && nums[i] % 2 == flag) {
            res += nums[i];
            i++;
        }
        res += Math.max(0, maxScore(nums, x, i, flag, mem));
        return res;
    }

    public long maxScore(int[] nums, int x, int start, int preFlag, long[][] mem) {
        int n = nums.length;
        if (start >= n) {
            return 0;
        }
        // 如果选择该节点
        int flag = nums[start] % 2;
        long select = nums[start];
        if (preFlag != flag) {
            select -= x;
        }
        int i = start + 1;
        while (i < n && nums[i] % 2 == flag) {
            select += nums[i];
            i++;
        }
        if (mem[i][flag] == Integer.MIN_VALUE) {
            mem[i][flag] = maxScore(nums, x, i, flag, mem);
        }
        select += Math.max(0, mem[i][flag]);
        // 如果跳过该节点
        if (mem[start + 1][preFlag] == Integer.MIN_VALUE) {
            mem[start + 1][preFlag] = maxScore(nums, x, start + 1, preFlag, mem);
        }
        return Math.max(select, mem[start + 1][preFlag]);
    }

}

性能

419.甲板上的战舰

目标

给你一个大小为 m x n 的矩阵 board 表示甲板,其中,每个单元格可以是一艘战舰 'X' 或者是一个空位 '.' ,返回在甲板 board 上放置的 战舰 的数量。

战舰 只能水平或者垂直放置在 board 上。换句话说,战舰只能按 1 x k(1 行,k 列)或 k x 1(k 行,1 列)的形状建造,其中 k 可以是任意大小。两艘战舰之间至少有一个水平或垂直的空位分隔 (即没有相邻的战舰)。

示例 1:

输入:board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]]
输出:2

示例 2:

输入:board = [["."]]
输出:0

说明:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 200
  • board[i][j] 是 '.' 或 'X'

进阶:你可以实现一次扫描算法,并只使用 O(1) 额外空间,并且不修改 board 的值来解决这个问题吗?

思路

这道题的描述很容易让人误解成只允许在'X'的位置上建造战舰,并且满足规则战舰不能相邻,问最多能造多少战舰。

但实际上题目的含义是,有一个棋盘,棋盘上放着战舰,每个战舰由k个'X'表示,战舰的形状是 1*k 或者 k*1,战舰的排放规则是不能相邻,让我们统计战舰数量。

刚开始想到的是使用一个二维数组保存是否访问过,从第一行开始从左到右访问,如果为'X'则向下延伸并将visited置为true。

后来看了题解,可以将原数组的'X'置为其它标记,这样就不用额外的空间了。

进阶的问题是不修改board,根据排列规则,仅统计战舰的起点,即上面与左边不能为'X'。

也有网友使用dfs来处理,子节点是下方与右侧的元素,如果是'X'则改为其它标记,否则返回。其实直接循环遍历更方便一些。

还有网友使用并查集计算连通分量,其实也没有必要。并查集主要用于快速判断元素是否属于同一个集合,统计集合中元素个数,集合元素的合并。这里仅需要连通分量个数,并且连通的关系很简单,纵向或横向,并不像图遍历那样复杂,直接计数即可。

代码

/**
 * @date 2024-06-11 8:40
 */
public class CountBattleships419 {

    /**
     * 网友题解
     * 一次扫描,不使用额外空间,不修改board
     * 统计左上顶点
     * 需满足条件:顶点左侧与上侧不能为'X'
     */
    public int countBattleships_v2(char[][] board) {
        int ans = 0;
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[i].length; j++) {
                if (board[i][j] == 'X' &&
                        (j == 0 || board[i][j - 1] != 'X') &&
                        (i == 0 || board[i - 1][j] != 'X')) {
                    ans++;
                }
            }
        }
        return ans;
    }

    /**
     * 看了题解,可以直接将board中的'X'置为'.',这样就不需要额外的visited数组了
     * 一次扫描,但是修改了board
     */
    public int countBattleships_v1(char[][] board) {
        int res = 0;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            int j = 0;
            while (j < n) {
                if (board[i][j] == 'X') {
                    res++;
                    int k = i + 1;
                    while (k < m && board[k][j] == 'X') {
                        board[k][j] = '.';
                        k++;
                    }
                    while (j < n && board[i][j] == 'X') {
                        board[i][j] = '.';
                        j++;
                    }
                } else {
                    j++;
                }
            }
        }

        return res;
    }

    /**
     * 一次扫描,但是使用了额外的空间
     */
    public int countBattleships(char[][] board) {
        int res = 0;
        int m = board.length;
        int n = board[0].length;
        boolean[][] visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            int j = 0;
            while (j < n) {
                if (visited[i][j]) {
                    j++;
                    continue;
                }
                if (board[i][j] == 'X') {
                    res++;
                    int k = i;
                    while (k < m && board[k][j] == 'X') {
                        visited[k][j] = true;
                        k++;
                    }
                    while (j < n && board[i][j] == 'X') {
                        visited[i][j] = true;
                        j++;
                    }
                } else {
                    j++;
                }
            }
        }

        return res;
    }
}

性能

使用额外空间

不使用额外空间

881.救生艇

目标

给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。

每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。

返回 承载所有人所需的最小船数 。

示例 1:

输入:people = [1,2], limit = 3
输出:1
解释:1 艘船载 (1, 2)

示例 2:

输入:people = [3,2,2,1], limit = 3
输出:3
解释:3 艘船分别载 (1, 2), (2) 和 (3)

示例 3:

输入:people = [3,5,3,4], limit = 5
输出:4
解释:4 艘船分别载 (3), (3), (4), (5)

说明:

  • 1 <= people.length <= 5 * 10^4
  • 1 <= people[i] <= limit <= 3 * 10^4

思路

给定一个数组,数组元素是待救援人员的体重,每条救生艇最多载两人且体重不超过限制,问最少需要多少救生艇。

尽可能多地让两人乘一艇,将体重轻的与重的搭配,如果都是轻的则造成运力浪费。

因此先按体重从小到大排序,然后使用双指针将重的与轻的搭配即可。

代码

/**
 * @date 2024-06-10 21:18
 */
public class NumRescueBoats881 {

    public int numRescueBoats(int[] people, int limit) {
        int n = people.length;
        Arrays.sort(people);
        int end = Arrays.binarySearch(people, limit);
        if (end < 0) {
            end = -end - 1;
        }
        // [end,n) 下标小于end的元素均比limit小,end 为0则可直接返回n
        int res = n - end;
        int start = 0;
        while (start < end) {
            // 如果start == end -1 即仅剩start一个元素 不再重复累加
            // start < end 防止end - 1为负即end==0
            // 由于取开区间,不包括end,因此从end-1开始
            while (start < end - 1 && people[start] + people[end - 1] > limit) {
                end--;
                res++;
            }
            start++;
            end--;
            res++;
        }
        return res;
    }
}

性能