【问题标题】:Find the number of substrings of a string which can be divisible by a number k找出可以被数字 k 整除的字符串的子字符串数
【发布时间】:2018-10-01 16:56:42
【问题描述】:

给定一个字符串,我想找到可以由原始字符串形成的所有子字符串,这些子字符串可以被整数 k 整除。例如字符串 14917 可以形成 7 个可以被整数 7 整除的子字符串。子字符串是:14、1491、14917、49、91、917 和 7。我想出了一个解决方案,但它没有输入大字符串时有效运行。我的代码是

string = '14917'
divider = 7

count = 0
for i in range(len(string)):
    for j in range(i+1, len(string)+1):
        sub_string = string[i:j]
        if int(sub_string) % divider == 0:
            count += 1

print(count) 

我已经阅读了解决此类问题的快速方法,其中大部分都谈到了计算字符串的滚动余数,但我无法真正正确地实现它。有什么方法可以快速解决这个问题。提前致谢。

【问题讨论】:

  • 我怀疑你在概念上会比这更有效率。例如,对于除数 7,十进制系统没有任何可用于增量计算的好结构。我看到的唯一可能改进的是数字解析的东西。
  • 您要查找字符串还是数数? (提示:计数更快。)
  • 我同意尼科的观点。从循环体中删除字符串操作将加快代码速度(但不会改变时间复杂度)。换句话说,将字符串转换为循环之前的数字列表。
  • 因为子集的个数是2^n
  • k 可以有多大?

标签: python string python-3.x algorithm math


【解决方案1】:

如果我们只是想要计数,这里是如何解决这个问题的大纲,我们不介意有多种方法可以提取相同的子字符串,而k相对素数到 107 是)。

首先让我们从数字的最后一位数到第一位,跟踪整数的余数。对于14917,这意味着编译下表:

number  10**digits % 7   digit  remainder
                                          0
     7         1           7     0+1*7 -> 0
    17         3           1     0+3*1 -> 3
   917         2           9     3+2*9 -> 0
  4917         6           4     0+6*4 -> 3
 14917         4           1     3+4*1 -> 0

现在这是诀窍。每当你在两个地方看到相同的余数时,从一个到另一个,你得到的东西可以被 7 整除。例如,在两个 3 之间你得到 49。如果一个特定的值出现 i 次,那么表示可被 7 整除的 i*(i-1)/2(可能相同)子字符串。

如果我们想获得唯一的子字符串,那么我们必须做更多的工作。很多。但是如果我们生成一个后缀树,我们仍然可以是O(length of string),这样我们就可以相对快速地计算重复项。

要实际产生数字,这种方法仍然是O(n^2)。但它会比你现有的大字符串方法更快,因为你只用小整数做数学。将字符串转换为数千位数的数字并不是特别快...


所以这里有更多关于唯一子串计数的后缀树方法的复杂性的详细信息。做对要困难得多。

上面我们从字符串的结尾回到开头,跟踪最后的剩余部分。但这意味着特定数字添加到余数中的内容取决于它在字符串中的位置。然而,在树中,给定节点与字符串末端的高度不同。这使得特定节点的余数更难计算。

我们需要做的是计算某种余数,其中当前数字的贡献取决于其高度,而不是保持当前数字的贡献固定。诀窍是将冒泡的可能余数集乘以10<sup>-1</sup>。然后当且仅当从这里开始的数字可以被k 整除时,我们将得到 0。 10<sup>-1</sup> (mod k) 是什么意思?它表示一个数字m,这样(10*m) % k 就是1。通过检查可以看出5 适用于7,因为50 = 7*7 + 1。我们总是可以通过反复试验找到相反的结果。一般来说,通过Euler's Theorem可以更有效地确定它的存在和价值。无论哪种方式,在我们的例子中都是5

现在,将余数集乘以一个数字而不是当前数字是更多的工作,但这样做的好处是我们可以合并树的分支。例如,考虑5271756 的后缀树。 (请注意,唯一性很重要,因为字符串 7 出现了两次。)

(root):
  a
  b
  c
  d
  e
(a): '17'
  f
(b): '27'
  a
(c): '5'
  b
  e
(d): '7'
  a
  f
(e): '6'(end)
(f): '5'
  e

现在我们可以按照自己的方式备份树来查找余数。 756 的计算说明了这个想法:

digit  prev_remainders remainders
#                 for    6
6      {}              {(6)%7: 1}
#                 for    5         56
5      {6: 1}          {(5)%7: 1, (5+5*6)%7: 1}
                       {    5: 1,         0: 1} = {0:1, 5:1}
#                 for    7         756           75
7      {0: 1, 2:1}     {(7)%7: 1, (7+5*0)%7: 1, (7+5*5): 1}
                       {    0: 1,         0: 1,       4: 1} = {0:2, 4:1}

因此,从那里开始,我们有 2 个可被 0 整除的字符串,即 7756

从根开始填充整棵树,然后以同样的方式冒泡(手工完成,我可能会犯错误 - 第一次就犯了很多错误!):

(root): {0:8, 1:6, 2:3, 4:1, 5:4, 6:4}
  a
  b
  c
  d
  e
(a): '17' {0:1, 1:3}
  f
(b): '27' {2:3, 6:3}
  a
(c): '5' {0:4, 1:3, 5:1}
  b
  e
(d): '7' {0:3, 4:1, 5:3}
  a
  f
(e): '6'(end) {6:1}
(f): '5' {0:1, 5:1}
  e

从中我们得出结论,有8 子串可以被7 整除。事实上它们是:

175 (af)
5271 (cba)
52717 (cbaf)
5271756 (cbafe)
56 (ce)
7 (d)
7175 (daf)
756 (dcf)

剩下的呢?例如,有3 获取2 的方法是什么意思?这意味着有3 子字符串s 这样( (s%7) * (5^(len(s)-1)) ) %7 == 2。所以我们在最终答案中不需要它,但我们在中间计算中肯定需要它!

【讨论】:

  • 感谢您的解释!我只是想问一下后缀树。因为在我的情况下,我需要找到所有唯一的子字符串,所以我可能需要检查重复项。构建后缀树的最佳策略是什么?
  • @SiddhantDube 标准方法称为Ukkonen's algorithm。但计算会变得棘手。我会考虑细节并添加注释。
  • @SiddhantDube 比我想象的要难,不过我还是给出了原理的解释。在编写通用代码之前,您肯定会想出一些玩具示例,因为它有些复杂。
猜你喜欢
  • 1970-01-01
  • 2023-03-14
  • 2018-10-13
  • 1970-01-01
  • 2018-04-02
  • 1970-01-01
  • 2020-05-17
  • 2012-02-21
  • 2010-10-20
相关资源
最近更新 更多