有一个从(即 size=n)索引的数组,其中0-n
包含其中的元素(假设 m 比 n 小 100 或 1000 倍,即 m 远小于 n),因此必须重复许多元素或子数组,我们有找到大小为 1 或大于 1 的重复子数组的数量。例如,考虑一个数组,这里重复 2 次重复 1 次重复 1 次重复 1 次重复 1 次重复 1 次是重复 1 次重复 1 次重复 1 次重复 1 次
所以最终答案是这些的总和,即 110-m
m < n
1 2 4 1 2 4 1 5 6 3 5
1
2
4
5
1 2
2 4
4 1
1 2 4
2 4 1
1 2 4 1
任何人都可以建议一些数据结构或有效的算法来解决这个问题。我正在考虑散列 m 并注意它出现的索引值但没有正式化它。
认为n,m < 10^5
3 回答
创建一个以整数为键的散列,以及整数的可扩展列表(例如链表)作为值。
通读您的输入列表。将每个被扫描的输入元素视为一个键;将以下元素的索引附加到关联的值列表中。
现在,您重复的 1 元素计数是 n - |keys|
接下来,检查你的钥匙。对于每个键,将原始列表的索引元素视为新的输入列表。创建一个新的散列并按照步骤 1 继续。请注意,当我们存储索引时,我们将继续使用原始列表的索引。
示例:1 2 4 1 2 4 1 5 6 3 5(假设我们是零索引的)。这里,n=11。
- 1 -> [1, 4, 7]
- 2 -> [2, 5]
- 3 -> [10]
- 4 -> [3, 6]
- 5 -> [8, 无]
- 6 -> [9]
重复 1-elt 的数量为 11 - 6 = 5。
现在,我们通过密钥。我们从 1 开始。索引列表是 [1, 4, 7],它对应于元素 [2, 2, 5]。
- 2 -> [2, 5]
- 5 -> [8]
这会选择 3 - 2 = 1 个重复的 2 元素列表。
ETC...
运行时间:输入+输出线性
我使用了一种基于后缀树的方法,来自Skienna 的算法设计一书中。后缀树是一种特殊的trie 树,您可以将给定字符串的每个后缀插入到 trie 树中。Trie 树是一种在单个树中存储多个单词的方式:根是空字符串,每条边添加一个字符。我使用了一个非常简单的实现作为概念证明,如下所示。
#include <iostream>
#include <string>
using std::string;
using std::cout;
class Node
{
Node* m_descendants[10];
int m_count;
public:
Node()
{
for(int i=0; i<10; ++i) { m_descendants[i] = nullptr; }
m_count = 0;
}
void Add(const char* str) {
// Increment number of times we've reached this node
m_count++;
const int firstDigit = str[0] - '0';
if (!(0 <= firstDigit && firstDigit <= 9)) return; // recursion base case
// Access descendant node correponding to current digit
Node*& descendant = m_descendants[firstDigit];
if (descendant == 0) { // create descendant if necessary
descendant = new Node;
}
// Recurse on rest of string
descendant->Add(str +1);
}
void Print(const string& str, int countAdj=0) const // debug function
{
for(int nextDigit=0; nextDigit<10; ++nextDigit) {
const Node* node = m_descendants[nextDigit];
if (node) {
const int adjustedCount = node->Count() - countAdj;
if (adjustedCount > 0) {
char c = '0' + nextDigit;
string strWithC = str;
strWithC += c;
cout << strWithC << ": " << adjustedCount << "\n";
node->Print(strWithC, countAdj);
}
}
}
}
int SumRepeated() const
{
int sumRepeated = 0;
for(int nextDigit=0; nextDigit<10; ++nextDigit) {
const Node* node = m_descendants[nextDigit];
if (node) {
const int repeatCount = node->Count() -1;
if (repeatCount > 0) {
sumRepeated += repeatCount;
sumRepeated += node->SumRepeated();
}
}
}
return sumRepeated;
}
int Count() const { return m_count; }
};
int main(int argc, const char* argv[])
{
// Construct
Node* const root = new Node;
for(const char* str = "12412415635"; *str; ++str) {
root->Add(str);
}
// Print
root->Print(string(), 1);
cout << "Sum repeated is " << root->SumRepeated() << "\n";
return 0; // (nodes are leaked)
}
输出
1: 2
12: 1
124: 1
1241: 1
2: 1
24: 1
241: 1
4: 1
41: 1
5: 1
Sum repeated is 11
注意这里有一个额外的重复子串,即 1241。
正如我所说,这是概念证明,因此,例如,您希望使用字典而不是数组来存储后代,并且m
. 此外,即使就整体算法而言,这种实现也不是最优的:它在时间和空间上都是 O(n^2)。您可以通过折叠没有分支的路径来优化以获得 O(n) 空间,并使用巧妙的构造算法来获得 O(n) 时间。此外,正如其他答案之一所指出的那样,您不需要处理最多为原始长度一半的子字符串。
这是 JavaScript 中的一些内容:(输出或多或少是即时的,我认为 JavaScript 是最慢的之一):
<script>
function f (s, m) {
var i = 0, n = s.length, c = 0, H = {}, Hi = {}
while (i < n-1) {
for (var j=i+1; j<=Math.min (i + 1 + m,n - 1); j++) {
if (s[i] == s[j]) {
var i_= i, j_= j, tmp = ''
while (s[i_] == s[j_] && i_ < j) {
tmp += String (s[i_])
i_++
j_++
}
var k = tmp.length - 1
while (k >= 0) {
if (!H[tmp]) {
H[tmp] = 1
Hi[tmp] = i
c++
}
else if (i == Hi[tmp]) {
H[tmp]++
c++
}
tmp = tmp.substr(0,k--)
}
}
}
i++
}
var t1 = ''
for (var i in H) t1 += i + ", "
return [t1,c]
}
var a1 = [1,2,4,1,2,4,1,5,6,3,5],
a2 = []
for (var i=0;i<100000;i++) a2.push(Math.floor(Math.random()*10))
console.log(a1 +"\n"+ f(a1,10))
console.log(f(a2,10))
</script>
输出:
1,2,4,1,2,4,1,5,6,3,5
1, 2, 4, 5, 12, 24, 41, 124, 241, ,10
["0...6358, 3582, 038, 4304, 2068, 095,
9252, 37866, 3786, 7866, 7893, 8701, 0669, ", 771]