昨天,我在洗衣服时把袜子配对,结果发现我这样做效率不高。我在做一个天真的搜索——挑选一只袜子,然后“反复”寻找那一双袜子。这需要平均在n/2*n/4=n2/8袜子上迭代。

作为一名计算机科学家,我在想我能做什么?排序(根据大小/颜色/…)当然是为了实现O(NlogN)解决方案。

哈希或其他不到位的解决方案是不可选择的,因为我无法复制我的袜子(如果可以的话,这可能很好)。

因此,问题基本上是:

给定一堆n双袜子,其中包含2n个元素(假设每只袜子正好有一对匹配的袜子),用对数的额外空间高效地将它们配对的最佳方式是什么?(如果需要的话,我相信我可以记住这些信息。)

我将感谢回答以下方面的问题:

大量袜子的一般理论解。袜子的实际数量没有那么多,我不相信我的配偶和我有超过30双。(而且很容易区分我的袜子和她的袜子;这也可以用吗?)它是否等同于元素清晰度问题?


当前回答

两种思路,查找任何匹配项所需的速度,与查找所有匹配项所需要的速度相比,与存储相比。

对于第二种情况,我想指出一个GPU并行版本,它查询所有匹配的袜子。

如果您有多个要匹配的财产,则可以使用分组元组和更高级的zip迭代器以及推力的转换函数,尽管这里是一个基于GPU的简单查询:

//test.cu
#include <thrust/device_vector.h>
#include <thrust/sequence.h>
#include <thrust/copy.h>
#include <thrust/count.h>
#include <thrust/remove.h>
#include <thrust/random.h>
#include <iostream>
#include <iterator>
#include <string>

// Define some types for pseudo code readability
typedef thrust::device_vector<int> GpuList;
typedef GpuList::iterator          GpuListIterator;

template <typename T>
struct ColoredSockQuery : public thrust::unary_function<T,bool>
{
    ColoredSockQuery( int colorToSearch )
    { SockColor = colorToSearch; }

    int SockColor;

    __host__ __device__
    bool operator()(T x)
    {
        return x == SockColor;
    }
};


struct GenerateRandomSockColor
{
    float lowBounds, highBounds;

    __host__ __device__
    GenerateRandomSockColor(int _a= 0, int _b= 1) : lowBounds(_a), highBounds(_b) {};

    __host__ __device__
    int operator()(const unsigned int n) const
    {
        thrust::default_random_engine rng;
        thrust::uniform_real_distribution<float> dist(lowBounds, highBounds);
        rng.discard(n);
        return dist(rng);
    }
};

template <typename GpuListIterator>
void PrintSocks(const std::string& name, GpuListIterator first, GpuListIterator last)
{
    typedef typename std::iterator_traits<GpuListIterator>::value_type T;

    std::cout << name << ": ";
    thrust::copy(first, last, std::ostream_iterator<T>(std::cout, " "));
    std::cout << "\n";
}

int main()
{
    int numberOfSocks = 10000000;
    GpuList socks(numberOfSocks);
    thrust::transform(thrust::make_counting_iterator(0),
                      thrust::make_counting_iterator(numberOfSocks),
                      socks.begin(),
                      GenerateRandomSockColor(0, 200));

    clock_t start = clock();

    GpuList sortedSocks(socks.size());
    GpuListIterator lastSortedSock = thrust::copy_if(socks.begin(),
                                                     socks.end(),
                                                     sortedSocks.begin(),
                                                     ColoredSockQuery<int>(2));
    clock_t stop = clock();

    PrintSocks("Sorted Socks: ", sortedSocks.begin(), lastSortedSock);

    double elapsed = (double)(stop - start) * 1000.0 / CLOCKS_PER_SEC;
    std::cout << "Time elapsed in ms: " << elapsed << "\n";

    return 0;
}

    //nvcc -std=c++11 -o test test.cu

1000万只袜子的运行时间:9毫秒

其他回答

由于人脑的结构与现代CPU完全不同,所以这个问题毫无实际意义。

人类可以利用“找到匹配的对”这一事实来战胜CPU算法,这对于一个不太大的集合来说是一个操作。

我的算法:

spread_all_socks_on_flat_surface();
while (socks_left_on_a_surface()) {
     // Thanks to human visual SIMD, this is one, quick operation.
     pair = notice_any_matching_pair();
     remove_socks_pair_from_surface(pair);
}

至少这是我在现实生活中使用的,我发现它非常有效。缺点是它需要一个平坦的表面,但通常很丰富。

如果你可以将一双袜子抽象为密钥本身,将另一双袜子作为值,那么我们可以使用哈希来发挥作用。

在你身后的地板上做两个假想的部分,一个给你,另一个给配偶。从袜子堆里取一只。现在,按照以下规则将袜子一只一只地放在地板上。确定袜子是你的还是她的,并查看地板上的相关部分。如果你能在地板上找到这双鞋,就把它捡起来,把它们系起来,或者把它们夹起来,或者在找到一双鞋后做任何你想做的事情,然后把它放在篮子里(把它从地板上取下来)。将其放在相关章节中。重复3次,直到所有袜子都从袜子堆上取下。

说明:

哈希和抽象

抽象是一个非常强大的概念,已用于改善用户体验(UX)。现实生活中与计算机交互的抽象示例包括:

用于在GUI(图形用户界面)中导航以访问地址的文件夹图标,而不是键入实际地址以导航到某个位置。GUI滑块用于控制不同级别的音量、文档滚动位置等。。

哈希或其他不到位的解决方案是不可选择的,因为我无法复制我的袜子(如果可以的话,这可能很好)。

我相信提问者正在考虑使用哈希,这样在放置袜子之前,应该知道袜子的位置。

这就是为什么我建议将放在地板上的一只袜子抽象为哈希键本身(因此不需要复制袜子)。

如何定义哈希键?

如果有不止一双类似的袜子,下面的密钥定义也适用。也就是说,假设有两双黑色男士袜子PairA和PairB,每双袜子都被命名为PairA-L、PairA-R、PairB-L和PairB-R。因此,PairA-L可以与PairB-R配对,但PairA-L和PairB-L不能配对。

假设任何袜子都可以通过以下方式唯一标识,

属性[性别]+属性[颜色]+属性(材质)+属性[类型1]+属性[类别2]+属性[左_右]

这是我们的第一个哈希函数。让我们对这个h1(G_C_M_T1_T2_LR)使用一个简短的符号。h1(x)不是我们的位置键。

消除Left_or_Right属性的另一个哈希函数是h2(G_C_M_T1_T2)。第二个函数h2(x)是我们的位置键!(你身后地板上的空间)。

要定位插槽,请使用h2(G_C_M_T1_T2)。一旦找到了槽,就使用h1(x)来检查它们的哈希值。如果它们不匹配,你就有一对。否则,把袜子扔到同一个槽里。

注意:由于我们在找到一个插槽时删除了一个插槽,因此可以安全地假设最多只有一个插槽具有唯一的h2(x)或h1(x)值。

如果我们每只袜子正好有一对匹配的袜子,那么使用h2(x)来查找位置,如果没有袜子,则需要进行检查,因为可以安全地假设它们是一对。

为什么把袜子放在地板上很重要

让我们考虑一个场景,袜子堆在一起(最坏的情况)。这意味着我们别无选择,只能进行线性搜索来找到一对。

将它们铺在地板上可以提高可见度,从而提高发现匹配袜子(匹配哈希键)的机会。当第三步把袜子放在地板上时,我们的大脑已经下意识地记录了位置。-因此,如果这个位置在我们的内存中可用,我们可以直接找到匹配的配对。-如果没有记住位置,不要担心,然后我们可以一直返回到线性搜索。

为什么从地板上取下这对鞋很重要?

短期人类记忆在需要记忆的项目较少时效果最好。因此,增加了我们使用哈希来识别这对的概率。当使用线性搜索对时,它还将减少要搜索的项目的数量。

分析

情况1:最坏的情况是,Derpina无法记住或直接使用哈希技术在地板上发现袜子。Derp对地板上的物品进行线性搜索。这并不比遍历堆以找到对更糟。比较上限:O(n^2)。比较下限:(n/2)。(当Derpina每捡一只袜子都是上一只的时候)。案例2:德普记得他放在地板上的每一只袜子的位置,每只袜子正好有一双。比较上限:O(n/2)。比较下限:O(n/2)。

我说的是比较操作,从袜子堆里挑选袜子必然是n次操作。因此,实际的下限是n次迭代,n/2次比较。

加快进度

为了获得完美的分数,使Derp获得O(n/2)比较,我建议Derpina,

花更多时间穿袜子来熟悉它。是的,这意味着也要花更多时间穿着德普的袜子。玩记忆游戏,如在网格中找出对,可以提高短期记忆性能,这是非常有益的。

这是否等同于元素清晰度问题?

我建议的方法是用于解决元素区分问题的方法之一,将它们放在哈希表中并进行比较。

考虑到您的特殊情况,即只有一个精确的对,它已经变得非常等价于元素区别问题。因为我们甚至可以对袜子进行分类,并检查相邻袜子是否成对(EDP的另一种解决方案)。

然而,如果给定袜子可能存在不止一双,那么它就偏离了EDP。

排序解决方案已经提出,但排序有点太多了:我们不需要排序;我们只需要平等团体。

所以散列就足够了(而且更快)。

对于每种颜色的袜子,形成一堆。重复输入篮中的所有袜子,并将它们分配到颜色堆上。在每个桩上循环,并通过其他度量(例如模式)将其分配到第二组桩中递归地应用此方案,直到您将所有袜子分发到非常小的堆上,您可以立即进行可视化处理

当SQL Server需要对庞大的数据集进行哈希连接或哈希聚合时,这种递归哈希分区实际上是由它完成的。它将其构建输入流分配到许多独立的分区中。该方案可线性扩展到任意数量的数据和多个CPU。

如果您可以找到一个分发密钥(哈希密钥),该密钥提供足够的存储桶,使得每个存储桶足够小,可以快速处理,那么您就不需要递归分区。不幸的是,我认为袜子没有这种特性。

如果每只袜子都有一个名为“PairID”的整数,那么可以根据PairID%10(最后一位)轻松地将它们分配到10个桶中。

我能想到的现实世界中最好的分区是创建一个堆积的矩形:一个维度是颜色,另一个是图案。为什么是长方形?因为我们需要O(1)随机访问桩。(3D长方体也可以,但这不太实用。)


更新:

并行性呢?多人能更快地匹配袜子吗?

最简单的并行化策略是让多个工人从输入篮中取出袜子,然后将袜子放到堆上。这只会增加这么多——想象100人在10个桩上战斗。同步成本(表现为手部碰撞和人类通信)破坏了效率和加速(参见通用可扩展性定律!)。这是否容易陷入僵局?不,因为每个工人一次只需要访问一堆。只有一个“锁”,就不会出现死锁。活锁可能是可能的,这取决于人类如何协调对桩的访问。他们可能只是使用随机退避,就像网卡在物理级别上那样,以确定什么卡可以独占地访问网络线路。如果它适用于NIC,那么它也应该适用于人类。如果每个工人都有自己的一组桩,它几乎可以无限扩展。然后,工人可以从输入篮中取出大块袜子(很少有人争抢,因为他们很少这样做),而且他们在分发袜子时根本不需要同步(因为他们有线程局部堆)。最后,所有工人都需要联合他们的桩组。我相信,如果工人形成一个聚合树,这可以在O(log(工人计数*每个工人的桩数))中完成。

元素的清晰度问题呢?正如文章所述,元素区别问题可以用O(N)来解决。袜子问题也是如此(如果你只需要一个分发步骤(我提出了多个步骤,只是因为人类不擅长计算-如果你在md5上分发(颜色、长度、图案…),即所有属性的完美哈希),那么一个步骤就够了)。

显然,一个速度不能比O(N)快,所以我们已经达到了最佳下限。

虽然输出不完全相同(在一种情况下,只是布尔值。在另一种情况中,是袜子对),但渐近复杂性是相同的。

我的解决方案并不完全符合您的要求,因为它正式需要O(n)“额外”空间。然而,考虑到我的条件,它在我的实际应用中非常有效。因此,我认为这应该很有趣。

与其他任务合并

我的特殊情况是,我不用烘干机,只是把衣服挂在普通的烘干机上。挂布需要O(n)操作(顺便说一句,我在这里总是考虑垃圾箱包装问题),这个问题本质上需要线性的“额外”空间。当我从桶里拿出一只新袜子时,如果这双袜子已经挂好了,我会试着把它挂在旁边。如果是新袜子,我会在旁边留出一些空间。

Oracle机器更好;-)

显然,这需要一些额外的工作来检查是否有匹配的袜子已经挂在某个地方,这将为计算机提供系数约为1/2的解O(n^2)。但在这种情况下,“人为因素”实际上是一种优势——如果匹配的袜子已经挂起,我通常可以很快(几乎为O(1))识别出它(可能涉及到大脑缓存中的一些难以察觉的因素)——将其视为一种有限的“预言机”,如oracle Machine;-)我们人类在某些情况下比数字机器有这些优势;-)

快到O(n)!

因此,将袜子配对的问题与挂布的问题联系起来,我可以免费获得O(n)“额外的空间”,并有一个及时的解决方案,大约O(n),只需要比简单的挂布多一点的工作,即使在非常糟糕的星期一早晨,也可以立即获得一双完整的袜子…;-)

我所做的就是拿起第一只袜子,把它放下(比如,放在洗衣碗的边缘)。然后我拿起另一只袜子,检查它是否与第一只袜子相同。如果是,我会把它们都去掉。如果不是,我把它放在第一只袜子旁边。然后我拿起第三只袜子,将其与前两只袜子进行比较(如果它们还在的话)。等

这种方法可以很容易地在阵列中实现,假设“移除”袜子是一个选项。实际上,你甚至不需要“脱掉”袜子。如果您不需要对袜子进行排序(见下文),那么您只需移动它们,就可以得到一个数组,该数组中所有袜子都成对排列。

假设袜子的唯一操作是比较相等,这个算法基本上仍然是n2算法,尽管我不知道平均情况(从未学会计算)。

当然,分类可以提高效率,尤其是在现实生活中,你可以很容易地将袜子“插入”在另外两个袜子之间。在计算中,树也可以做到这一点,但这是额外的空间。当然,我们又回到了NlogN(或者更多,如果有几只袜子按排序标准是相同的,但不是来自同一双)。

除此之外,我想不出什么,但这种方法在现实生活中似乎非常有效