【问题标题】:Number of substrings with count of each character as k每个字符的计数为 k 的子串数
【发布时间】:2020-10-02 14:24:08
【问题描述】:

来源:https://www.geeksforgeeks.org/number-substrings-count-character-k/

给定一个字符串和一个整数 k,找出所有不同字符恰好出现 k 次的子字符串的数量。

在 O(n) 中寻找解决方案,使用两个指针/滑动窗口方法。我只能找到满足此条件的最长子字符串,但不能找到该长子字符串中的子字符串。

例如:阿巴巴巴,k = 2
我的解决方案找到了 abab、ababba 等,但没有找到 abba 中的 bb。

有人可以帮我解释一下逻辑吗?

【问题讨论】:

  • 另外,想知道这是否可以在 O(n) 中完成或需要 O(n2) 算法

标签: string data-structures substring


【解决方案1】:

如果您可以编辑您的问题以包含您的解决方案代码,我很乐意为您提供帮助。

现在我正在分享我的解决方案代码(在 java 中),它在 O(n2) 中运行。我已经添加了足够多的 cmets 以使代码自我解释。尽管如此,解决方案的逻辑如下:

正如您正确指出的那样,可以使用滑动窗口方法(具有可变窗口大小)来解决该问题。下面的解决方案考虑了所有可能的子字符串,使用嵌套的 for 循环来设置开始和结束索引。对于每个子字符串,我们检查子字符串中的每个元素是否恰好出现 k 次。

为了避免重新计算每个子字符串的计数,我们在映射中维护计数,并在增加结束索引(滑动窗口)时不断将新元素放入映射中。这确保了我们的解决方案在 O(n2) 而不是 O(n3) 中运行。

为了进一步提高效率,如果子字符串的大小符合我们的要求,我们只检查单个元素的计数。例如对于 n 个唯一元素(映射中的键),所需子字符串的大小为 n*k。如果子字符串的大小与此值不匹配,则无需检查单个字符出现的次数。

import java.util.*;

/**
 * Java program to count the number of perfect substrings in a given string. A
 * substring is considered perfect if all the elements within the substring
 * occur exactly k number of times.
 * 
 * @author Codextor
 */

public class PerfectSubstring {

    public static void main(String[] args) {
        String s = "aabbcc";
        int k = 2;
        System.out.println(perfectSubstring(s, k));

        s = "aabccc";
        k = 2;
        System.out.println(perfectSubstring(s, k));
    }

    /**
     * Returns the number of perfect substrings in the given string for the
     * specified value of k
     * 
     * @param s The string to check for perfect substrings
     * @param k The number of times every element should occur within the substring
     * @return int The number of perfect substrings
     */
    public static int perfectSubstring(String s, int k) {

        int finalCount = 0;

        /*
         * Set the initial starting index for the subarray as 0, and increment it with
         * every iteration, till the last index of the string is reached.
         */
        for (int start = 0; start < s.length(); start++) {

            /*
             * Use a HashMap to store the count of every character in the subarray. We'll
             * start with an empty map everytime we update the starting index
             */
            Map<Character, Integer> frequencyMap = new HashMap<>();

            /*
             * Set the initial ending index for the subarray equal to the starting index and
             * increment it with every iteration, till the last index of the string is
             * reached.
             */
            for (int end = start; end < s.length(); end++) {
                /*
                 * Get the count of the character at end index and increase it by 1. If the
                 * character is not present in the map, use 0 as the default count
                 */
                char c = s.charAt(end);
                int count = frequencyMap.getOrDefault(c, 0);
                frequencyMap.put(c, count + 1);

                /*
                 * Check if the length of the subarray equals the desired length. The desired
                 * length is the number of unique characters we've seen so far (size of the map)
                 * multilied by k (the number of times each character should occur). If the
                 * length is as per requiremets, check if each element occurs exactly k times
                 */
                if (frequencyMap.size() * k == (end - start + 1)) {
                    if (check(frequencyMap, k)) {
                        finalCount++;
                    }
                }
            }
        }
        return finalCount;
    }

    /**
     * Returns true if every value in the map is equal to k
     * 
     * @param map The map whose values are to be checked
     * @param k   The required value for keys in the map
     * @return true if every value in the map is equal to k
     */
    public static boolean check(Map<Character, Integer> map, int k) {
        /*
         * Iterate through all the values (frequency of each character), comparing them
         * with k
         */
        for (Integer i : map.values()) {
            if (i != k) {
                return false;
            }
        }
        return true;
    }
}

【讨论】:

    【解决方案2】:

    我不能给你一个 O(n) 的解决方案,但我可以给你一个 O(k*n) 的解决方案(比 geeksforgeeks 页面中提到的 O(n^2) 更好)。

    这个想法是最大没有。元素是 26。所以,我们不必检查所有子字符串,我们只需要检查长度

    所以,检查所有 26*k*l 可能的子字符串! (假设 k

    【讨论】:

      【解决方案3】:

      对于一个给定的值k和一个长度n的字符串s字母大小D,我们可以在O(n*D).

      我们需要找到每个字符恰好出现 k 次的子字符串

      • 此类子字符串的最小大小 = k(当只有一个字符时)
      • 此类子字符串的最大大小 = k*D(当所有字符都存在时)

      所以我们将检查所有大小在 [k, k*D] 范围内的子字符串

      from collections import defaultdict
      
      ALPHABET_SIZE = 26
      
      def check(count, k):
          for v in count.values():
              if v != k and v != 0:
                  return False
          return True
      
      def countSubstrings(s, k):
          total = 0
          for d in range(1, ALPHABET_SIZE + 1):
              size = d * k
              count = defaultdict(int)
              l = r = 0
              while r < len(s):
                  count[s[r]] += 1
                  # if window size exceed `size`, then fix left pointer and count
                  if r - l + 1 > size:
                      count[s[l]] -= 1
                      l += 1
                  # if window size is adequate then check and update count
                  if r - l + 1 == size:
                      total += check(count, k)
                  r += 1  
          return total
      
      def main():
          string1 = "aabbcc"
          k1 = 2
          print(countSubstrings(string1, k1))        # output: 6
          
          string2 = "bacabcc"
          k2 = 2
          print(countSubstrings(string2, k2))        # output: 2
      
      main()
      

      【讨论】:

      • 考虑到检查方法需要O(D)时间,这不是O(NDD)解决方案吗?
      【解决方案4】:

      很少有观察可以帮助优化解决方案

      1. 请注意,您不需要检查所有可能大小的子字符串,您只需要检查大小为 k、2k、3k 等直到 ALPHABET_SIZE * k 的子字符串(记住 鸽巢原理)

      2. 您可以从任何一端预先计算字母的频率直到某个索引,然后您可以使用它来查找 O(26) 中任意两个索引之间的字母频率

      C++ 在 O(n * ALPHABET_SIZE^2) 中实现您的问题

      我添加了cmets图表来帮助你快速理解代码

      diagram 1

      diagram 2

      #include <bits/stdc++.h>
      #define ll long long
      #define ALPHABET_SIZE 26
      
      using namespace std;
      
      int main()
      {
          ios_base::sync_with_stdio(false);
          cin.tie(NULL);
          cout.tie(NULL);
      
          int n, k;
          string s;
      
          cin >> n >> k;
          cin >> s;
      
          ll cnt = 0;
          /**
           * It will be storing frequency of each alphabets
           **/
          vector<int> f(ALPHABET_SIZE, 0);
          /**
           * It will store alphabets frequency till that index
           **/
          vector<vector<int>> v;
      
          v.push_back(f);
      
          /**
           * Scan array from left to right and calculate the frequency of each alphabets till that index
           * Now push that frequency array in v
           * This loop will run for n times
           **/
          for (int i = 1; i <= n; i++)
          {
              f[s[i - 1] - 'a']++;
              v.push_back(f);
          }
      
          /**
           * This loop will run for k times
           **/
          for (int i = 0; i < k; i++)
          {
              /**
               * start is the lower bound (left end from where window will start sliding)
               **/
              int start = i;
              /**
               * end is the upper bound (right end till where window will be sliding)
               **/
              int end = (n / k) * k + i;
      
              if (end > n)
              {
                  end -= k;
              }
      
              /**
               * This loop will run for n/k times
               **/
              for (int j = start; j <= end; j += k)
              {
                  /**
                   * This is a ALPHABET_SIZE * k size window
                   * It will be sliding between start and end (inclusive)
                   * This loop will run for at most ALPHABET_SIZE times
                   **/
                  for (int d = j + k; d <= min(ALPHABET_SIZE * k + j, end); d += k)
                  {
                      /**
                       * A flag to check weather substring is valid or not
                       **/
                      bool flag = true;
                      /**
                       * Check if frequencies at two different indexes differ only by zero or k (element wise)
                       * Note that frequencies at two different index can't be same
                       * This loop will run for ALPHABET_SIZE times
                       **/
                      for (int idx = 0; idx < ALPHABET_SIZE; idx++)
                      {
                          if (abs(v[j][idx] - v[d][idx]) != k && abs(v[j][idx] - v[d][idx]) != 0)
                          {
                              flag = false;
                          }
                      }
      
                      /**
                       * Increase the total count if flag is true
                       **/
                      if (flag)
                      {
                          cnt++;
                      }
                  }
              }
          }
      
          /**
           * Print the total count
           **/
          cout << cnt;
      
          return 0;
      }
      

      【讨论】:

      • 你能解释一下为什么你一开始给v加了一个空频率吗?然后从 i=t 开始到
      • @NeoRavi,假设你想计算从索引 i 到 j 的字母的频率,你可以像 freq[k] = v[j][k] - v[i-1] 这样计算[k] 其中 0
      【解决方案5】:

      如果您想以简单的方式解决问题,而不用担心时间复杂度。这是解决方案。

      public class PerfecSubstring {
          public static void main(String[] args) {
              String st = "aabbcc";
              int k = 2;
              System.out.println(perfect(st, k));
          }
      
          public static int perfect(String st, int k) {
              int count = 0;
              for (int i = 0; i < st.length(); i++) {
                  for (int j = st.length(); j > i; j--) {
                      String sub = st.substring(i, j);
                      if (sub.length() > k && check(sub, k)) {
                          System.out.println(sub);
                          count++;
                      }
                  }
              }
              return count;
          }
      
          public static boolean check(String st, int k) {
              Map<Character, Integer> map = new HashMap<>();
              for (int i = 0; i < st.length(); i++) {
                  Character c = st.charAt(i);
                  map.put(c, map.getOrDefault(c, 0) + 1);
              }
              return map.values().iterator().next() == k &&  new HashSet<>(map.values()).size() == 1;
          }
      }
      

      【讨论】:

        【解决方案6】:

        这是我在 C# 中做的一个答案,复杂度为 O(n^2)。我可能应该使用辅助方法来避免使用大量代码,但它确实可以完成工作。 :)

        namespace CodingChallenges
        {
            using System;
            using System.Collections.Generic;
        
            class Solution
            {
                // Returns the number of perfect substrings of repeating character value 'num'.
                public static int PerfectSubstring(string str, int num)
                {
                    int count = 0;
                    for (int startOfSliceIndex = 0; startOfSliceIndex < str.Length - 1; startOfSliceIndex++)
                    {
                        for (int endofSliceIndex = startOfSliceIndex + 1; endofSliceIndex < str.Length; endofSliceIndex++)
                        {
                            Dictionary<char, int> dict = new Dictionary<char, int>();
                            string slice = str.Substring(startOfSliceIndex, (endofSliceIndex - startOfSliceIndex) + 1);
                            for (int i = 0; i < slice.Length; i++)
                            {
                                if (dict.ContainsKey(slice[i]))
                                {
                                    dict[slice[i]]++;
                                }
                                else
                                {
                                    dict[slice[i]] = 1;
                                }
                            }
                            bool isPerfect = true;
                            foreach (var entry in dict)
                            {
                                if (entry.Value != num)
                                {
                                    isPerfect = false;
                                }
                            }
                            if (isPerfect)
                            {
                                Console.WriteLine(slice);
                                count++;
                            }
                        }
                    }
                    if (count == 1)
                    {
                        Console.WriteLine(count + " perfect substring.");
                    }
                    else
                    {
                        Console.WriteLine(count + " perfect substrings.");
                    }
                    return count;
                }
        
        
                public static void Main(string[] args)
                {
                    string test = "1102021222";
                    PerfectSubstring(test, 2);
                }
            }
        }
        

        【讨论】:

          【解决方案7】:

          此解决方案适用于 O(n*D)

          我认为可以通过将 hash_map(frozenset(head_sum_mod_k.items())) 替换为更新其哈希而不是重新计算它的地图实现来将其升级为 O(n) - 之所以可以这样做,是因为每次迭代只会更改 head_sum_mod_k 的一个条目。

          from copy import deepcopy
          
          def countKPerfectSequences(string:str, k):
              print(f'Processing \'{string}\', k={k}')
              # init running sum
              head_sum = {char: 0 for char in string}
              tail_sum = deepcopy(head_sum)
              tail_position = 0
              # to match both 0 & k sequence lengths, test for mod k == 0
              head_sum_mod_k = deepcopy(head_sum)
              occurrence_positions = {frozenset(head_sum_mod_k.items()): [0]}
              # iterate over string
              perfect_counter = 0
              for i, val in enumerate(string):
                  head_sum[val] += 1
                  head_sum_mod_k[val] = head_sum[val] % k
                  while head_sum[val] - tail_sum[val] > k:
                      # update tail to avoid longer than k sequnces
                      tail_sum[string[tail_position]] += 1
                      tail_position += 1
                  # print(f'str[{tail_position}..{i}]=\'{string[tail_position:i+1]}\', head_sum_mod_k={head_sum_mod_k} occurrence_positions={occurrence_positions}')
                  # get matching sequences between head and tail
                  indices = list(filter(lambda i: i >= tail_position, occurrence_positions.get(frozenset(head_sum_mod_k.items()), [])))
                  # for start in indices:
                  #     print(f'{string[start:i+1]}')
                  perfect_counter += len(indices)
                  # add head
                  indices.append(i+1)
                  occurrence_positions[frozenset(head_sum_mod_k.items())] = indices
              return perfect_counter
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-04-17
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多