LeetCode 2904. Shortest and Lexicographically Smallest Beautiful String

You are given a binary string s and a positive integer k.

A substring of s is beautiful if the number of 1's in it is exactly k.

Let len be the length of the shortest beautiful substring.

Return the lexicographically smallest beautiful substring of string swith length equal to len. If s doesn't contain a beautiful substring, return an empty string.

A string a is lexicographically larger than a string b (of the same length) if in the first position where a and b differ, a has a character strictly larger than the corresponding character in b.

  • For example, "abcd" is lexicographically larger than "abcc" because the first position they differ is at the fourth character, and d is greater than c.

Example 1:

复制代码
Input: s = "100011001", k = 3
Output: "11001"
Explanation: There are 7 beautiful substrings in this example:
1. The substring "100011001".
2. The substring "100011001".
3. The substring "100011001".
4. The substring "100011001".
5. The substring "100011001".
6. The substring "100011001".
7. The substring "100011001".
The length of the shortest beautiful substring is 5.
The lexicographically smallest beautiful substring with length 5 is the substring "11001".

Example 2:

复制代码
Input: s = "1011", k = 2
Output: "11"
Explanation: There are 3 beautiful substrings in this example:
1. The substring "1011".
2. The substring "1011".
3. The substring "1011".
The length of the shortest beautiful substring is 2.
The lexicographically smallest beautiful substring with length 2 is the substring "11".

Example 3:

复制代码
Input: s = "000", k = 1
Output: ""
Explanation: There are no beautiful substrings in this example.

Constraints:

  • 1 <= s.length <= 100
  • 1 <= k <= s.length

大意就是在一个全是0和1的string里找substring,要满足substring里1的数量为k的条件。返回长度最短且字典序最小的那个结果。

最简单的办法就是brute force强行找到所有substring然后做比较。刚开始想了个用TreeSet存所有满足条件的substring然后返回第一个,还得写个comparator先比较length再比较字典序,就还挺麻烦的。但是正好复习了一下TreeSet。

取最小:first() - O(1)

取最大:last() - O(logn)

如果按string长度比较,需要同时按长度和字典序排序:

TreeSet<String> set = new TreeSet<>( Comparator.comparingInt(String::length) .thenComparing(Comparator.naturalOrder()) );

如果仅写 (a, b) -> a.length() - b.length(),当插入两个长度相同的不同字符串(例如 "cat" 和 "dog")时,第二个字符串会被丢弃!

我非常naive的写法,甚至最后也忘了加set为空的情况了。

复制代码
class Solution {
    public String shortestBeautifulSubstring(String s, int k) {
        TreeSet<String> results = new TreeSet<>(Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder()));
        for (int i = 0; i < s.length(); i++) {
            for (int j = i; j < s.length(); j++) {
                String substr = s.substring(i, j + 1);
                if (countOnes(substr) == k) {
                    results.add(substr);
                }
            }
        }
        if (results.size() == 0) {
            return "";
        }
        return results.first();
    }

    private int countOnes(String s) {
        int result = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '1') {
                result++;
            }
        }
        return result;
    }
}

然后看了解答,嗯,虽然人家也是brute force但是写的比我优雅多了。我这都N^3logN了。

主要思想就是for loop是按长度从小到大开始循环,也就是说直接上来就找长度为k的,然后逐渐增加,这样找到的第一个长度的答案就是我们要的了。不需要TreeSet。时间复杂度n^3。

复制代码
class Solution {
    public String shortestBeautifulSubstring(String s, int k) {
        // iterate from shortest to longest
        for (int len = k; len < s.length() + 1; len++) {
            String result = "";
            // iterate the substring of length len with starting index i
            for (int i = 0; i + len < s.length() + 1; i++) {
                String substr = s.substring(i, i + len);
                if (countOnes(substr) == k) {
                    // replace result with lexicographically smaller string
                    if (result == "" || substr.compareTo(result) < 0) {
                        result = substr;
                    }
                }
            }
            if (result != "") {
                return result;
            }
        }
        return "";
    }

    private int countOnes(String s) {
        int result = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '1') {
                result++;
            }
        }
        return result;
    }
}

然后是最优解sliding window只需要O(n^2)。刚开始看了答案也没理解,借助AI终于理解了且总结了一下sliding window。

首先前面可以short circuit去掉invalid cases这个没啥好说的。

具体的算法就是维持left和right,我们遍历right,left先保持不变。

最开始的blocker在没有理解第一波iteration,其实第一波就是相当于找到一个前面已经有k个1的right的位置,然后开始看能不能移动left来缩小范围。所以对于right就是遇到1就++。

用count记录有多少个1,如果right移动了发现是1那就count++。

然后移动left有两个情况:如果移完right发现1多了,那就要丢掉left;如果left是0,也要丢掉left,因为我们要最短的长度,leading 0肯定短不了。如果丢掉的left是1,那count也要--。

最后就是比较看看现在count如果==k了的话看看现在这个substring和原来我们记录的result谁更优秀。这个判断条件要仔细写。

复制代码
class Solution {
    public String shortestBeautifulSubstring(String s, int k) {
        // short circuit invalid cases
        int total = 0;
        for (int i = 0; i < s.length(); i++) {
            total += s.charAt(i) - '0';
        }
        if (total < k) {
            return "";
        }

        // sliding window
        int left = 0;
        int count = 0;
        String result = "";
        for (int right = 0; right < s.length(); right++) {
            // count the number of '1's
            count += s.charAt(right) - '0';
            
            // remove redundant chars - extra '1' or '0'
            while (count > k || s.charAt(left) == '0') {
                count -= s.charAt(left) - '0';  // if it's '1', decrease count
                left++;
            }

            // calculate
            if (count == k) {
                String substring = s.substring(left, right + 1);
                // careful about the criteria
                if (result == "" || substring.length() < result.length() ||
                    (substring.length() == result.length() && substring.compareTo(result) < 0)) {
                        result = substring;
                }
            }
        }
        return result;
    }
}

Sliding window总结:

总结:滑动窗口的"标准流程"

while 放在 if 前面,遵循的是滑动窗口算法最核心的逻辑顺序:

  1. 吸纳(right++:右指针向右扩展,把新元素拉进窗口。

  2. 调整(while :左指针向右收缩,消除所有不合规状态 (超标的 '1' 或冗余的 '0'),使窗口达到"合法且极致紧凑"的状态。

  3. 结算(if:此时的窗口才是最纯净的,再去检查它是否符合条件、记录答案。

相关推荐
维克兜率天28 分钟前
【维克】技术指标是什么:从均线到MACD的数学原理
人工智能·笔记·python·算法·量化
Messy create33 分钟前
【储能系统三大核心】
网络·stm32·单片机·算法·能源
sel_939 分钟前
【vLLM】vLLM 推理框架详解:从 PagedAttention 到生产级部署实战
人工智能·深度学习·算法·语言模型·框架·vllm
XR1234567881 小时前
办公大楼组网选型决策树:TCO 与三大品牌优劣对比总结
算法·决策树·机器学习
阳明山水1 小时前
Mamba路径如何建模长程时序依赖
人工智能·深度学习·算法·机器学习·架构
_Narcissus_3 小时前
分治&递归
数据结构·c++·笔记·算法·leetcode·递归·分治
明志数科3 小时前
具身智能数据工程全链路解析:从真实产线采集到LeRobot适配
网络·人工智能·算法
小O的算法实验室3 小时前
AAAI-26,Lehmer编码搜索排列空间的理论与实证分析
算法
lch2011_yb3 小时前
CSP-S 2023 密码锁 题解
算法