【问题标题】:Efficiently counting the number of substrings of a digit string that are divisible by k?有效地计算可被 k 整除的数字字符串的子字符串数?
【发布时间】:2016-02-22 14:16:07
【问题描述】:

我们得到一个由数字0-9 组成的字符串。我们必须计算可被数字k 整除的子字符串的数量。一种方法是生成所有子字符串并检查它是否可以被k 整除,但这将花费O(n^2) 时间。我想在O(n*k)时间解决这个问题。

1 <= n <= 100000 and 2 <= k <= 1000.

我看到了一个类似的问题here。但在那个问题中,k 被固定为 4。所以,我使用了能被 4 整除的性质来解决这个问题。 这是我对这个问题的解决方案:

int main()
{
    string s;
    vector<int> v[5];
    int i;
    int x;
    long long int cnt = 0;

    cin>>s;
    x = 0;
    for(i = 0; i < s.size(); i++) {
        if((s[i]-'0') % 4 == 0) {
            cnt++;
        }
    }
    for(i = 1; i < s.size(); i++) {
        int f = s[i-1]-'0';
        int s1 = s[i] - '0';
        if((10*f+s1)%4 == 0) {
            cnt = cnt + (long long)(i);
        }
    }
    cout<<cnt;

}

但我想要一个适用于任何 k 值的通用算法。

【问题讨论】:

  • 那么,您的代码在哪里?您是如何解决这个问题的?
  • 听起来很像一个竞争问题。如果比赛已经结束,请发布链接,以便我们验证;否则,预计大多数人会等待几天再回答。
  • 是什么让您认为在O(n*k) 时间有可能? “子字符串”是指所有连续子序列还是所有子序列?
  • 啊,是的,这听起来确实像一个竞争问题。我删除了我的答案,但如果您提供 j_random_hacker 要求的链接,我会将其放回
  • 我已经提到这不是任何现场比赛的问题。在 codeforces 上也提出了相同类型的问题。我已经提过了。

标签: string algorithm time-complexity big-o


【解决方案1】:

这是一个非常有趣的问题。与其跳入最终的整体算法,我想我应该从一个不太合理的算法开始,然后对其进行一系列修改,最终得到最终的 O(nk) 时间算法。

这种方法结合了多种不同的技术。主要技术是计算数字上的滚动余数的想法。例如,假设我们要查找字符串中所有为 k 倍数的 前缀。我们可以通过列出所有前缀并检查每个前缀是否是 k 的倍数来做到这一点,但这至少需要 Θ(n2) 时间,因为有 Θ(n2 ) 不同的前缀。然而,我们可以通过更聪明一点在时间 Θ(n) 中做到这一点。假设我们知道我们已经读取了字符串的前 h 个字符,并且我们知道以这种方式形成的数字的其余部分。我们也可以用它来说明字符串的前 h+1 个字符的其余部分,因为通过附加该数字,我们获取现有数字,将其乘以 10,然后添加下一个数字。这意味着如果我们有 r 的余数,那么我们的新余数是 (10r + d) mod k,其中 d 是我们发现的数字。

这里有一个快速的伪代码来计算字符串中 k 的倍数的前缀数量。它在时间 Θ(n) 中运行:

remainder = 0
numMultiples = 0
for i = 1 to n: // n is the length of the string
    remainder = (10 * remainder + str[i]) % k
    if remainder == 0
        numMultiples++
return numMultiples

我们将使用这种初始方法作为整个算法的构建块。

所以现在我们有一个算法可以找到我们的字符串的 前缀 的数量是 k 的倍数。我们如何将其转换为一种算法,该算法可以找到 k 的倍数的 子字符串 的数量?让我们从一种不太奏效的方法开始。如果我们计算原始字符串中所有 k 的倍数的前缀,然后删除字符串的第一个字符并计算剩余部分的前缀,然后删除第二个字符并计算剩余部分的前缀,等等?这最终会找到每个子字符串,因为原始字符串的每个子字符串都是该字符串某些后缀的前缀。

这是一些粗略的伪代码:

numMultiples = 0
for i = 1 to n:
    remainder = 0
    for j = i to n:
        remainder = (10 * remainder + str[j]) % k
        if remainder == 0
            numMultiples++
return numMultiples

例如,在字符串14917 上运行此方法以查找7 的倍数,将会找到这些字符串:

  • 字符串14917:查找14149114917
  • 字符串4917:查找49
  • 字符串917:查找91917
  • 字符串17:什么也没找到
  • 字符串7:查找7

这种方法的好消息是它会找到所有有效的子字符串。坏消息是它的运行时间为 Θ(n2)。

但是让我们看看我们在这个例子中看到的字符串。例如,查看通过搜索整个字符串的前缀找到的子字符串。我们找到了其中三个:14149114917。现在,看看这些字符串之间的“差异”:

  • 1414917 的区别是917
  • 141491 的区别是91
  • 149114917 的区别是7

请注意,这些字符串中的每一个的差异本身就是 14917 的子字符串,它是 7 的倍数,事实上,如果您查看我们稍后在算法运行中匹配的其他字符串,我们会'也会找到这些其他字符串。

这不是巧合。如果您有两个具有相同前缀的数字是相同数字 k 的倍数,那么它们之间的“差异”也将是 k 的倍数。 (检查这方面的数学是一个很好的练习。)

所以这表明我们可以采取另一条路线。假设我们找到原始字符串的所有前缀都是 k 的倍数。如果我们能找到所有这些,我们就可以计算出这些前缀之间有多少成对的差异,并可能避免多次重新扫描。这不一定会找到所有内容,但它会找到可以通过计算两个前缀的差异形成的所有子字符串。对所有后缀重复此操作 - 并注意不要重复计算 - 确实可以加快速度。

首先,假设我们找到了 r 个不同的字符串前缀,它们是 k 的倍数。如果我们包含差异,我们只找到了多少个子字符串?好吧,我们找到了 k 个字符串,每对(无序)元素加上一个额外的字符串,计算结果为 k + k(k-1)/2 = k(k+1)/2 个子字符串。不过,我们仍然需要确保不会重复计算。

要查看我们是否重复计算某些内容,我们可以使用以下技术。当我们计算沿字符串的滚动余数时,我们将存储在每个条目之后找到的余数。如果在计算滚动余数的过程中,我们在某个时刻重新发现了已经计算过的余数,我们就知道我们正在做的工作是多余的;之前对字符串的一些扫描已经计算了这个余数,并且我们从现在开始发现的任何东西都已经找到了。

把这些想法放在一起就得到了这个伪代码:

numMultiples = 0
seenRemainders = array of n sets, all initially empty
for i = 1 to n:
    remainder = 0
    prefixesFound = 0
    for j = i to n:
        remainder = (10 * remainder + str[j]) % k
        if seenRemainders[j] contains remainder:
            break
        add remainder to seenRemainders[j]
        if remainder == 0
            prefixesFound++
    numMultiples += prefixesFound * (prefixesFound + 1) / 2
return numMultiples

那么效率如何?乍一看,由于外部循环,它看起来像在 O(n2) 时间内运行,但这不是一个紧密的界限。请注意,每个元素最多只能在内循环中传递 k 次,因为在那之后没有任何剩余部分仍然是空闲的。因此,由于每个元素最多被访问 O(k) 次,总共有 n 个元素,因此运行时间为 O(nk),符合您的运行时间要求。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-01-24
    • 2017-10-20
    • 2023-04-06
    • 2019-07-16
    • 2023-03-14
    • 2015-03-14
    • 2023-03-28
    • 1970-01-01
    相关资源
    最近更新 更多