"remove all duplicates"的泛化

Generalization of "remove all duplicates"

本文关键字:泛化 all remove duplicates      更新时间:2023-10-16

related为二元谓词。让我们将"关系子集"定义为所有元素的集合,使得子集中的任何元素都通过"相关"与子集中的至少一个其他元素相关,而不是与自身相关(因此,元素是否与自身相关在形成关系子集时是无关的)。注意:关系子集不一定是强连接的组件。例如,假设A与B相关,而B和C彼此相关。则{A,B,C}根据定义是关系子集,但不是强连接组件,因为没有从B到A或从C通过"related"到A的路径。

注意,"related"不一定是对称的,即related(a,b)==true不一定意味着related(b,a)==true,也不具有传递性,即relative(a,b)==true和related(b,c)==true并不一定意味着相关(a,c)===true。据我所见,为了将集合划分为关系子集,不需要对二元谓词related施加任何限制。如果元素x与任何元素(除了它自己)都不相关,那么{x}本身就是它自己的关系子集。

一个好问题是定义

template <typename Container, typename BinaryPredicate>
void partition (Container& container, BinaryPredicate related);

这将对容器的元素进行排序,以便将容器划分为其关系子集。这个问题在尝试最初的问题后很快就出现了:

template <typename Container, typename BinaryPredicate>
void removeRelated (Container& container, BinaryPredicate related);

其是从每个关系子集移除(从container)除了在容器中找到的每个子集中的第一个之外的所有元素。

当然,等式只是"相关"的一个特例,因此这是"删除所有重复项"的推广(这就是我如何想到这个问题的,通过尝试推广那个众所周知的已解决问题)。我们想要的是保留每个关系子集的一个代表,即根据容器的元素顺序的第一个关系子集。

以下是我尝试实现的代码,具有以下关系:

Bob knows Mary
Mary knows Jim
Jim knows Bob
Sally knows Fred
Fred knows no one.

在这个例子中,A与B有关,当且仅当A知道B。{Bob,Mary,Jim}是关系子集,因为每个人都与其他人有关(Bob与Mary有关,Mary与Jim有关,Jim与Bob有关)。请注意,{Sally,Fred}不是关系子集,因为尽管Sally与Fred有亲属关系,但Fred与Sally没有亲属关系。因此,剩下{Sally}、{Fred}作为另外两个关系子集。

所以最后的答案应该是:鲍勃,莎莉,弗雷德。注意,如果要定义函数partition,那么它只会保持{Bob,Mary,Jim,Sally,Fred}不变,Bob,Sally和Fred是每个分区的第一个元素。因此,即使我们有partition函数(当然,不要与std::partition混淆,但我现在并没有真正考虑一个好的名称),仍然不清楚removeRelated需要什么。

#include <iostream>
#include <algorithm>
#include <list>
#include <set>
template<typename Container, typename BinaryPredicate>
void removeRelated (Container& container, BinaryPredicate related) {
using element = typename Container::value_type;
std::set<element> s;
for (typename Container::iterator it = std::begin(container);  it != std::end(container); ) {
if (std::find_if(s.begin(), s.end(),
[=](const element& x)->bool {return related(*it,x);}) != s.end())
it = container.erase(it);  // *it is related to an element in s.
else {
s.insert(*it);
it++;
}
}
}
struct Person {  // Used for testing removeRelated.
std::string name;
std::list<Person*> peopleKnown;
Person (const std::string& n) : name(n) {}
void learnsAbout (Person* p) {peopleKnown.push_back(p);}
bool knows (Person* p) const {
return std::find(peopleKnown.begin(), peopleKnown.end(), p) != peopleKnown.end();
}
};
int main() {
Person *bob = new Person("Bob"), *mary = new Person("Mary"), *jim = new Person("Jim"),
*sally = new Person("Sally"), *fred = new Person("Fred");
bob->learnsAbout(mary);
mary->learnsAbout(jim);
jim->learnsAbout(bob);
sally->learnsAbout(fred);
std::list<Person*> everybody {bob, mary, jim, sally, fred};
removeRelated (everybody, [](Person* a, Person* b)->bool {return a->knows(b);});
for (const Person* x : everybody) std::cout << x->name << ' ';  // Bob Mary Sally Fred
//  Should be 'Bob Sally Fred' since {Bob, Mary, Jim}, {Sally}, {Fred} are the relation subsets.
}

上面代码中的错误是,Mary被插入到"s"中,因为在这一点上,她与s中的任何人都没有亲属关系(通过"knows")。最后,她与Jim有关系,Jim通过"knows"与Bob(以及Bob与Mary)有关系,因此{Bob,Mary,Jim}是关系子集,因此Bob应该是"s"中这三个人中唯一的一个。

但在迭代过程中,函数并不知道这一点。如何修复算法?一个想法是,也许首先定义上面提到的函数partition,即对容器进行排序,以便将容器划分为其关系子集(这本身就是一个非常好的问题,很可能是手头的主要问题),然后简单地获取每个分区的第一个元素。

另一个想法是取代lambda函数

[=](const element& x)->bool {return related(*it,x);}

带有

[=](const element& x)->bool {return relatedIndirectly(*it,x);}

我认为它可能会解决这个问题,其中helper函数relatedIndirectly搜索与x的关系链。

以下是Cimbali研究的另一个例子(如下)。假设A与B有关,A与C有关,B与C有关。那么{A,B}不可能是关系子集,因为B与A无关。类似地,{A,C}和{B,C}不能是关系子集(C与A不相关,C与B不相关),{A、B、C}绝对不是,因为C与任何人都不相关。{A} ,{B},{C}是唯一满足我对关系子集定义的分区。

猜想:关系子集总是强连通分量的并集,使得并集中的每个强连通分量至少有一个元素与并集中不同强连通分量中的某个元素相关。但这需要数学证明。

更新:我(极大地)加强了上述相关子集的定义,以便:BinaryPredicate"related"应是自反的(related(x,x)==对任何x为true)、对称的(relative(x,y)表示related(y,x))和传递的(related[x,y]和related[y,z]表示related[x,z])。这将任何集合划分为等价类。removeRelated应从每个等价类中删除除容器中的第一个元素之外的所有元素。这推广了"去除所有重复"的经典问题,因为等式是等价关系的特例。下面的代码现在给出了正确的结果,但我想知道是否有办法削弱"related"的条件,仍然得到相同的结果。

#include <iostream>
#include <algorithm>
#include <list>
#include <set>
template<typename Container, typename BinaryPredicate>
void removeRelated (Container& container, BinaryPredicate related) {
using element = typename Container::value_type;
std::set<element> s;
for (typename Container::iterator it = std::begin(container);  it != std::end(container); ) {
if (std::find_if(s.begin(), s.end(),
[=](const element& x)->bool {return related(*it,x);}) != s.end())
it = container.erase(it);  // *it is related to an element in s.
else {
s.insert(*it);
it++;
}
}
}
// Used for testing removeRelated.  Person::isRelativeOf is an equivalence relation.
struct Person {
std::string name;
std::list<Person*> relatives;
Person (const std::string& n) : name(n) {relatives.push_back(this);}  // Forcing reflexivity
void addRelative (Person* p) {
for (Person* relatives_of_p : p->relatives)
relatives.push_back(relatives_of_p);  // Forcing transitivity ('relatives.push_back(p);' included in this)
p->relatives.push_back(this);  // Forcing symmetry
}
bool isRelativeOf (Person* p) const {
return std::find(relatives.begin(), relatives.end(), p) != relatives.end();
}
};
int main() {
Person *bob = new Person("Bob"), *mary = new Person("Mary"), *jim = new Person("Jim"),
*sally = new Person("Sally"), *fred = new Person("Fred");
bob->addRelative(mary);  // crashes
mary->addRelative(jim);
jim->addRelative(bob);
sally->addRelative(fred);
std::list<Person*> everybody {bob, mary, jim, sally, fred};
removeRelated (everybody, [](Person* a, Person* b)->bool {return a->isRelativeOf(b);});
for (const Person* x : everybody) std::cout << x->name << ' ';  // Bob Sally (correct)
}

如果"related"是"knows"或"is friends of",这不需要对称或传递,那么我们还会有一个分区,因此removeRelated仍然可以工作吗?

我想知道的另一个问题是:对上面的内容进行排序,使等价类由连续元素组成,最快的排序算法是什么?这就是我想到的:

template<typename Container, typename BinaryPredicate>
void sortByEquivalenceClasses (Container& container, BinaryPredicate related) {
for (auto it = container.begin();  it != container.end();  ++it)
for (auto jt = std::next(it);  jt != container.end();  ++jt)
if (related(*it, *jt)) {
std::iter_swap(jt, std::next(it));
break;
}
}

但是排序并没有保留元素的原始相对顺序。如何保存?

好的,您可以通过以下定义子集

让我们将"关系子集"定义为所有元素的集合,使得子集中的任何元素都通过"相关"与子集中的至少一个其他元素相关,而不是其自身

这听起来有点递归,但据我所知是

  • 没有传出关系的节点n在作为单例{n}的子集中
  • 仅与singleton具有传出关系的节点n在作为singleton{n}的子集中
  • 任何循环(任何长度),更一般地说,任何强连接组件都形成一个子集,包括关系的所有前置节点

示例。以下关系:

A -> B
B -> A
C -> B
D -> C
E -> F

定义以下子集:{A, B, C, D}{E}{F}


假设以上是正确的,我们可以设计以下算法(伪代码):

int visit(Node n, int s) { // returns 0 iff there is no cycle anywhere beneath
if(length(n.relations) = 0 || n.singleton == true)
// leaves, or only leaves below
{
n.singleton = true;
return false;
}
else if(n.visited == true || n.bigger_subset > 0)
// part of a subcycle, or predecessor of one
{
n.bigger_subset = s;
return true;
}
else
// searching for the nature of what is below
{
n.visited = true;
bool found_cycle = 0;
for each node m such that n is related to m (n -> m)
found_cycle = max(Visit(m), found_cycle);
if( found_cycle > 0 )
n.bigger_subset = found_cycle;
else
n.singleton = true; // parent of only singletons
n.visited = false;
return found_cycle;
}
}
s = length(node_set) + 1; // clearly bigger than the maximal number of subsets
for n in node_set:
{
if( n.singleton == false && n.big_subcycle == 0 )
{
// state unknown, it is thus the first of its subset, unless it is a predecessor (or part) of a known subset
int set = visit(n, s);
// not a singleton, and not the current set : a previous one
if( set > s )
node_set.remove(n);
s--;
}
else
node_set.remove(n);
}

这基本上是从每个元素开始进行深度优先搜索,标记正在访问的节点,以检测循环。通过记住每个节点的状态,子集的任何前身都可以添加到子集中,而无需再次进入循环。


以下是上面给出的示例中该算法的C代码:http://ideone.com/VNumcN