趣题:选取最多的子集使得任两子集恰有一个公共元素

探讨在集合{1,2,...,n}
部署运行你感兴趣的模型镜像

    这里有一个有趣的问题:在集合{1, 2, ..., n}中选取尽可能多的子集,使得任意两个子集的交集有且仅有一个元素。例如,当n=7时,选取{1,2,3,4}、{4,5,6,7}、{1,7}这3个集合可以满足条件。子集数还可以更大一点吗?最大是多少?给出一种构造,然后证明这个数目不可能更大了。

    当n=7时,仅仅取3个子集实在是太弱了。一种最简单的办法就可以让子集数达到6,只需取{1,2}、{1,3}、{1,4}、{1,5}、{1,6}、{1,7}即可。再仔细观察,我们发现这个结果还可以进一步改进:我们还可以再往里面添加进一个子集{1},使得这7个子集两两间仍然恰有一个公共元素。这下我们似乎不能再往里面添加任何新子集了。我们还可以做得更好吗?一个新的思路是在{1,2}、{1,3}、{1,4}、{1,5}、{1,6}、{1,7}里面加上{2,3,4,5,6,7},这同样可以让子集数达到7个,可惜我们仍然无法再往里面添加新的子集了。经过若干次尝试后,我们逐渐开始确信,在集合{1, 2, ..., n}里面最多只能选出n个两两恰有一个公共元素的子集,并且构造方法无外乎上面两种。这一猜想不但与直觉相符,而且貌似也很好证明。你或许会从一些看似很直观的结论出发开始证明:“显然不可能有两个大小为1的子集”,“选取多个元素个数大于2的子集显然不划算”……但牛B就牛B在,偏偏就有这样一种子集数为n的取法,每个子集里都有不止两个元素,但仍然保证任意两个子集间恰有一个公共元素:

{1,2,3}、{1,4,5}、{1,6,7}、{2,4,7}、{3,4,6}、{3,5,7}、{2,5,6}

    这一个例子对我们的猜想足以构成威胁:子集数为n真的已经到极限了吗?证明结论有那么容易吗?看来,情况貌似比我们想象中的要复杂得多。

 
 
    事实上,子集数最大为n,这个数目不可能再大了。其证明出人意料的简单。考虑每个子集所对应的n维01向量,例如n=7时集合{1,2,3}就对应向量{1,1,1,0,0,0,0},而子集{1,4,5}则可以表示成{1,0,0,1,1,0,0}。可以看出,两个子集的交集的元素个数就等于它们的向量的点积。下面我们证明,满足要求的一组子集其对应的向量一定是线性无关的,从而直接得到向量个数不超过n的结论。
    记m个向量分别为v_1, v_2, ..., v_m,令s=Σa_i v_i,考虑点积<s,s>=Σ Σ a_i · a_j · <v_i, v_j>。注意对每一对不同的i和j都有<v_i, v_j>=1,而<v_i, v_i>就等于子集A_i的元素个数|A_i|。于是,<s,s>就等于(a_1 + a_2 + ... + a_m)^2 + Σ(a_i)^2·(|A_i|-1)。当s=Σa_i v_i=0时,<s,s>也应该为0;而上面<s,s>表达式中的每一项都是非负整数,要让<s,s>为0只可能是所有a_i都取0,这就说明这m个向量是线性无关的。
    这就是著名的de Bruijn-Erdős定理。

    线性代数理论不止一次帮助我们证明了看似与线性代数毫无关系的问题。如果你喜欢这个证明,千万不要错过这个经典的图论问题

    Update: "<"符号被吃掉了,现在已经改过来了

您可能感兴趣的与本文相关的镜像

GPT-SoVITS

GPT-SoVITS

AI应用

GPT-SoVITS 是一个开源的文本到语音(TTS)和语音转换模型,它结合了 GPT 的生成能力和 SoVITS 的语音转换技术。该项目以其强大的声音克隆能力而闻名,仅需少量语音样本(如5秒)即可实现高质量的即时语音合成,也可通过更长的音频(如1分钟)进行微调以获得更逼真的效果

### C++ 使用数组求所有子集的算法实现 以下是基于位运算方法实现的一个完整的 C++ 示例代码,用于计算给定整数数组的所有子集: ```cpp #include <iostream> #include <cmath> // 用于 pow 函数 using namespace std; void GetSubSets(int* nums, int len) { int total = pow(2, len); // 子集总数为 2 的 n 次方 for (int i = 0; i < total; ++i) { // 遍历每一个可能的子集 cout << "["; bool isFirst = true; for (int j = 0; j < len; ++j) { // 对于每一位进行判断 if (i & (1 << j)) { // 如果第 j 位为 1,则选取该位置上的元素 if (!isFirst) { cout << ","; } cout << nums[j]; isFirst = false; } } cout << "] "; } } int main() { int nums[] = {1, 2, 3}; // 输入数组 int len = sizeof(nums) / sizeof(nums[0]); // 计算数组长度 GetSubSets(nums, len); return 0; } ``` #### 解析 上述代码通过遍历 `0` 到 `2^n - 1` 的所有数字来生成子集。其中,`n` 是输入数组的大小。 - **外层循环**:控制生成的子集数量,范围是从 `0` 到 `pow(2, len)-1`[^4]。 - **内层循环**:逐位检查当前数字的二进制表示中哪些位为 `1`,并根据这些位选择对应的数组元素加入子集中。 此方法的时间复杂度为 \(O(n \cdot 2^n)\),因为需要枚举所有的 \(2^n\) 种情况,并且每种情况下最多处理 \(n\) 个元素。 --- ### 更加通用的解决方案(使用标准库) 如果希望利用更现代的 C++ 特性,可以改用 `std::vector<int>` 来代替原始数组。下面是改进后的版本: ```cpp #include <iostream> #include <vector> #include <cmath> using namespace std; void PrintAllSubsets(const vector<int>& nums) { int total = pow(2, nums.size()); for (int mask = 0; mask < total; ++mask) { cout << "["; bool firstElement = true; for (size_t j = 0; j < nums.size(); ++j) { if ((mask >> j) & 1) { // 检查第 j 位是否为 1 if (!firstElement) { cout << ", "; } cout << nums[j]; firstElement = false; } } cout << "]" << endl; } } int main() { vector<int> nums = {1, 2, 3}; PrintAllSubsets(nums); return 0; } ``` 这段代码同样实现了相同的功能,但更加灵活,支持动态调整数组大小[^1]。 --- ### 总结 两种方法的核心思想均是通过位掩码的方式逐一生成所有可能的子集组合。第一种方式适用于固定大小的 C-style 数组;第二种则更适合现代化开发场景下的动态数据结构操作。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值