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):此时的窗口才是最纯净的,再去检查它是否符合条件、记录答案。

相关推荐
菜鸟~noob23310 小时前
【电子战】 第22篇:误差椭圆与置信区间【含matlab代码】
开发语言·算法·matlab
weixin_3077791310 小时前
C++代码实现MATLAB中的normalize函数功能
开发语言·c++·算法·matlab
不会就选b10 小时前
算法日常・每日刷题--<动态规划>11
算法
(Charon)10 小时前
【C++面试】手写String类:同时实现拷贝构造与移动构造
c++·算法
迷途之人不知返11 小时前
算法系列6:模拟
算法
旺仔仔仔11 小时前
# 机器人项目实战(1):选相机、试深度,再到手眼标定
算法
CoderYanger11 小时前
A.每日一题:1614. 括号的最大嵌套深度
java·程序人生·算法·leetcode·面试·学习方法
yuki蜜语11 小时前
别只会调包跑模型!一文讲透大模型训练底层的“递推、递归、三明治”三重逻辑
算法
成旭先生11 小时前
银行卡识别 API:一张照片读出卡号、BIN 与发卡行
人工智能·算法·ocr·api接口·金融科技·银行卡识别·支付风控
陌上花开缓缓归以11 小时前
mebdlts3.4.1安全启动全流程
服务器·算法