【问题标题】:Smallest window (substring) that has both uppercase and corresponding lowercase characters具有大写和相应小写字符的最小窗口(子字符串)
【发布时间】:2021-10-14 12:39:37
【问题描述】:

我在一次现场采访中被问到以下问题:

当字符串中的每个字母都以大写和小写形式出现时,该字符串被认为是“平衡的”。例如,CATattac 是平衡的(act 出现在这两种情况下),而Madam 不是(ad 仅以小写形式出现)。编写一个函数,给定一个字符串,返回该字符串的最短平衡子串。例如:
“azABaabza”应该返回“ABaab”
“TacoCat”应该返回 -1(不平衡)
“AcZCbaBz”应该返回整个字符串

使用蛮力方法很简单 - 计算所有子串对,然后检查它们是否平衡,同时跟踪最小的子串的大小和起始索引。

如何优化?我有一种强烈的感觉,它可以通过滑动窗口/双指针方法来完成,但我不确定如何。什么时候更新滑动窗口的指针?

编辑:删除滑动窗口标签,因为这不是滑动窗口问题(如 cmets 中所述)。

【问题讨论】:

  • TacoCat 有 'TC' 和 'tc' 怎么不平衡?
  • @Ezan​​i,因为在两者之间您会遇到没有对应大写字母的ao。请注意,该问题要求提供 substring.
  • 优化的目标是什么?蛮力可以优化到 O(N^2) 就够了吗?
  • 我想你的意思是最长的平衡子串,而不是最短的平衡子串?否则,微不足道的答案总是空字符串。
  • @Someone 我不能 100% 肯定地回答你。最好在另一篇文章中问这个问题。不过,就我所知的两指针方法而言,我个人会说是的。

标签: string algorithm


【解决方案1】:

由于字符串的特殊性。只有 26 个大写字母和 26 个小写字母。
我们可以循环每 26 个字母 j 并表示从位置 i 开始的任何子字符串的最小长度,以查找大写和小写字母 j 的匹配项是 len[i][j]
演示 C++ 代码:

string s = "CATattac";
// if len[i] >= s.size() + 1, it denotes there is no matching
vector<vector<int>> len(s.size(), vector<int>(26, 0));
for (int i = 0; i < 26; ++i) {
    int upperPos = s.size() * 2;
    int lowerPos = s.size() * 2;
    for (int j = s.size() - 1; j >= 0; --j) {
        if (s[j] == 'A' + i) {
            upperPos = j;
        } else if (s[j] == 'a' + i) {
            lowerPos = j;
        }
        len[j][i] = max(lowerPos - j + 1, upperPos - j + 1);
    }
}

我们还记录字符数。

// cnt[i][j] denotes the number of characters j in substring s[0..i-1]
// cnt[0][j] is always 0
vector<vector<int>> cnt(s.size() + 1, vector<int>(26, 0));
for (int i = 0; i < s.size(); ++i) {
    for (int j = 0; j < 26; ++j) {
        cnt[i + 1][j] = cnt[i][j];
        if (s[i] == 'A' + j || s[i] == 'a' + j) {
            ++cnt[i + 1][j];
        }
    }
}

那么我们就可以遍历s了。

int m = s.size() + 1;
for (int i = 0; i < s.size(); ++i) {
    bool done = false;
    int minLen = 1;
    while (!done && i + minLen <= s.size()) {
        // execute at most 26 times, a new character must be added to change minLen
        int prevMinLen = minLen;
        done = true;
        for (int j = 0; j < 26 && i + minLen <= s.size(); ++j) {
            if (cnt[i + minLen][j] - cnt[i][j] > 0) {
                // character j exists in the substring, have to find pair of it
                minLen = max(minLen, len[i][j]);
            }
        }
        if (prevMinLen != minLen) done = false;
    }
    // find overall minLen
    if (i + minLen <= s.size())
        m = min(m, minLen);
    cout << minLen << '\n';
}

输出:(如果i + minLen &lt;= s.size(),则有效。否则,如果从该位置开始,则子字符串不存在)
无效的输出差异是由于数组len的生成方式造成的。

8
4
15
14
13
12
11
10

我不确定是否有更简单的解决方案,但这是我现在能想到的最好的解决方案。 时间复杂度:O(N),常数为 26 * 26

【讨论】:

    【解决方案2】:

    编辑:由于不必要的二进制搜索,我以前有 O(nlog(n))。

    我想到了一个解决方案,技术上 O(n),其中n 是字符串的长度,但常数很大。

    为简单起见,让我们考虑一个类似的情况,只有两个字母 AB(以及它们的小写字母),并让 l 成为字母表的大小以供将来参考。我处理了一个示例字符串ABabBaaA

    我们首先计算每个字母出现次数的前缀计数。在这种情况下,我们得到

    i: 0, 1, 2, 3, 4, 5, 6, 7, 8
    ----------------------------
    A: 0, 1, 1, 1, 1, 1, 1, 1, 2
    a: 0, 0, 0, 1, 1, 1, 2, 3, 3
    B: 0, 0, 1, 1, 1, 2, 2, 2, 2
    b: 0, 0, 0, 0, 1, 1, 1, 1, 1
    

    这样,假设我们从1开始索引字符串(为了实现,你可以在开头添加一个额外的字符,比如美元符号$),我们可以得到每个字母的出现次数在恒定时间的任何子字符串上(或者更确切地说 - 在 O(l) 中,但在我的情况下,l 设置为 2,在你的情况下为 l = 26,所以从技术上讲,这是恒定时间)。

    好的,现在我们准备字符索引的数组/向量/队列,因此如果字符A 出现在索引18 上,则结构将由18 组成。我们得到

    A: 1, 8
    a: 3, 6, 7
    B: 2, 5
    b: 4
    

    重要的是,在数组和向量中,我们可以通过逐一丢弃小于每个索引的索引,在摊销常数时间内查找某些“大于”的最小元素。

    现在,算法。从大于0 的每个(左)索引开始,我们将找到与[left_index, right_index] 绑定的子字符串平衡的最早的右索引。我们这样做如下:

    1. left_index = right_index = i 开头为i = 1, ..., n

    2. 读取right_index 的前缀计数数组并减去left_index - 1 的前缀计数,接收子字符串[left_index, right_index] 的计数。找到任何未通过“余额”检查的字母。如果没有,您找到了从left_index 开始的最短平衡子串。

    3. 查找第一次出现的大于left_index 的“缺失”字母。将right_index 设置为该事件的索引。转到第 1 步,保留修改后的right_index

    例如:以left_index = right_index = 1开头我们看到子字符串中每个字母出现的次数是1, 0, 0, 0,所以a检查失败。 a最早出现的是3,所以我们设置right_index = 3。我们回到第 1 步,接收一个新的事件数组:1, 1, 1, 0。现在b检查失败,它最早出现大于1的是4,所以我们将right_index设置为4。我们转到第 1 步,接收到出现的数组 1, 1, 1, 1,它通过了余额检查。

    另一个例子:从left_index = right_index = 2 开始,我们在步骤 1 中得到一个出现的数组0, 0, 1, 0。现在b 未通过检查。 b大于left_index的最早出现是4,所以我们将right_index设置为4。现在我们得到了一个出现的数组0, 1, 1, 1,所以A 没有通过检查。 A 大于 left_index 的最早出现是 8,因此我们将 right_index 设置为该值。现在,出现的数组是2-1, 3-0, 2-0, 1-0,即1, 3, 2, 1,它通过了余额检查。

    最终我们会找到最短的平衡子串是bBleft_index = 4

    该算法的复杂度为 O(nl^2),因为:我们从 n 不同的索引开始,并且我们在 O 中执行最多 l 查找(对于 l 可能无法通过检查的不同字母) (1)。对于每次查找,我们必须计算 l 前缀和的差异。但由于l 是恒定的(尽管它可能很大,比如 26),这简化为 O(n)。

    【讨论】:

    • 很好的解释,但我没有按照你的第二个例子:“left_index = right_index = 2 我们在步骤 1 中得到了一个出现的数组0,0,1,0。现在 b 未能通过检查。”如何?我们不应该得到1,0,1,0 代替(然后检查在a 失败)?
    • 另外,它如何确保我们找到“最短”的平衡字符串?如果我理解正确,我们将不得不遍历字符串的整个长度并将每个索引的算法作为left_index=right_index=i 运行。对吗?
    • 我相信时间复杂度仍然是 O(Nl^2) ,因为它与我的算法基本相同?读取前缀计数数组将花费 O(l) 并且最多会有 O(l) 个读取前缀计数的循环。
    • @Someone 回答您的第一条评论:我们通过从 right_index-th 前缀总和中减去 left_index-1-th 前缀总和得到 0,0,1,0。我们收到1-1,0-0,1-0,0-0,即0,0,1,0。现在b 未通过检查,因为B 的出现次数非零,但b 的出现次数为零。所以b 丢失了,所以b 没有通过检查。 Aa 满足检查,因为两者的出现次数均为零。
    • @Someone 回答您的第二条评论:是的,我们遍历整个字符串,并且对于每个起始位置 (left_index),我们找到了最短的平衡子字符串。因此,在为每个起始位置找到最短的位置之后,我们从中挑选出最短的,这就是整体上最短的。如果你问为什么为每个起始位置找到最短的,那是因为所有字母都必须满足检查,并且对于每个“坏”字母,我们找到最左边的right_index,这使得字母满足检查。
    【解决方案3】:

    我正在使用递归方法;我不确定它的时间复杂度是多少。

    我们的想法是我们检查字符串中的哪些字符以它们的大写格式和大写格式都存在。对于两种形式都没有给出的任何字符,我们用空格''替换它们。然后我们将 ' ' 上的剩余字符串拆分为一个列表。

    在第一种情况下,如果我们只剩下一个字符串,我们返回它的长度。

    在第二种情况下,如果我们没有剩余字符,我们返回 -1。

    在第三种情况下,如果我们还有多个字符串,我们会重新评估每个字符串的子长度,并返回我们随后评估的最长字符串的长度。

        from collections import Counter
        
        def findMutual(s):
            lower = dict(Counter( [x for x in s if x.lower() == x] ))
            upper = dict(Counter( [x for x in s if x.upper() == x] ))
        
            mutual = {}
        
            for charr in lower:
                if charr.upper() in upper:
                    mutual[charr] = upper[charr.upper()] + lower[charr]
        
            matching_charrs = ''.join([x if x.lower() in mutual else ' ' for x in s ]).split()
            print(s)
            print(matching_charrs)
            return matching_charrs
        
        
        
        def smallestSubstring(s):
        
            matching_charrs = findMutual(s)
        
            if len(matching_charrs) == 1:
                return(len(matching_charrs[0]))
            elif len(matching_charrs) == 0:
                return(-1)
            else:
                list_lens = []
                for i in matching_charrs:
                    list_lens.append(smallestSubstring(i))
                return max(list_lens)                
        
        print(smallestSubstring('azABaabza'))
        print(smallestSubstring('dAcZCbaBz'))
        print(smallestSubstring('TacoCat'))
        print(smallestSubstring('Tt'))
        print(smallestSubstring('T'))
        print(smallestSubstring('TaCc'))
    

    【讨论】:

      猜你喜欢
      • 2022-06-12
      • 2020-09-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-22
      • 2020-09-16
      • 2016-01-25
      • 2011-01-16
      相关资源
      最近更新 更多