函数依赖的极小化,大概是数据库原理课里最让人“背得下定义却下不了笔”的考点了。提到极小函数依赖集,多数人都能背出三条:右部单属性、没有冗余依赖、没有冗余属性。可真给你一张函数依赖集F,让你求Fmin,很多人第一步就卡住:先拆?先删?用谁的闭包?删完还能不能删?这篇就是冲着这些问题来的。我会把极小函数依赖集的求解算法拆成一套可以直接照做的流程,配合两道手算例题,把闭包计算、覆盖判定、删依赖、消属性这些动作全部过一遍,顺便把考试里最容易扣分的细节都指出来。这篇文章适合正在准备数据库期末考试、考研初试的同学,也适合自学数据库原理但被教材例题绕晕的开发者。你不需要多深的基础,只要知道函数依赖是“属性之间的决定关系”,就能跟着走下来。先说明一点:我不只给步骤,我会反复讲“为什么这一步要这么干”,因为考试改步骤分的时候,理由往往比最终答案更重要。1. 先读懂考纲:极小函数依赖集要消灭的到底是什么冗余1.1 三条定义的直白翻译教材上,函数依赖集F的极小化(也叫最小覆盖Fmin)一般要求满足三个条件:F中每个函数依赖的右部是单属性;F中不存在冗余依赖;F中不存在冗余属性。这些定义很多同学背得滚瓜烂熟,却不理解。“右部单属性”好办,就是把X→AB这种依赖替换成X→A和X→B两条。“无冗余依赖”的意思是:删掉某一条依赖之后,剩下的依赖集仍然能推出原来那条依赖,那这条依赖就是多余的。“无冗余属性”指的是:一条依赖左侧如果带着A、B两个属性,而去掉B之后只剩A也能推出右边的结论,那么B就是多余的。这三条读完,你再回头看题目,会发现极小化其实根本不是让你去“背算法”,而是让你反复回答三个问题:这条依赖能不能删?删了之后推理能力还在吗?左部这些属性是不是有凑数的?回答清楚,答案自然出来了。1.2 极小化的本质:推理能力不变的“钥匙串整理”用一句话归纳:Fmin是F的“等价但最小”版本。所谓等价,是说F能推出的每一个函数依赖,Fmin也能推出;反过来Fmin能推出的,F也必然能推出。瘦身,就是在保持这种推理能力不变的前提下尽量删除和压缩。我给学生讲的时候喜欢打一个比方:你有一串钥匙,每把钥匙都能开某一扇门,但有一把钥匙能开的门,别的钥匙全能开,那这把就是冗余依赖;有的钥匙上挂了一个装饰环,去掉环完全不影响开门,这个环就是冗余属性。题目要你做的,就是把钥匙串整理到最精简,但每一扇原来看得上的门都还必须开得了。这个比方看着简单,其实非常贴近算法本质。尤其是后面做删依赖和消左部判断时,你只要反复问自己一句“去掉它,别的钥匙还能开这扇门吗”,思路就不会乱。1.3 极小集不唯一,先给自己松个绑教材里常说“函数依赖集的极小化”,但很少强调一个关键事实:Fmin不是唯一的。删除冗余依赖的顺序不同、消左部属性的先后不同,很可能得到不同的极小集。比如A→B和B→A两条能互相推出的依赖,你先删A→B和后删B→A,留下来的就不一样。考试里只要最终结果满足三条条件且与原集等价,都应该算对。这一点非常容易让考生怀疑自己算错,先打一个预防针:不要因为答案和同学不一样就立刻觉得自己错了,先检查每一逻辑链是否成立。2. 前置能力:闭包与覆盖,整套算法的手感来源极小化的每一步其实都在做“比较”:删掉一条依赖后,还能不能推出原来那条?左部去掉一个属性后,还能不能推出右部?这些话落到计算上,全靠“属性闭包”和“依赖覆盖”两件工具。不把这两个技能练熟,背再多步骤也是空中楼阁。2.1 属性闭包的手工算法给定属性集X和依赖集F,X关于F的闭包记作X(F)+,含义是:从X出发,用F里的依赖反复推导,最终能得到的所有属性集合。手工计算很机械,但必须养成固定节奏:把闭包集合初始化为X本身;循环扫描F,任何一条依赖左侧已经全部出现在当前闭包中,就把它的右侧属性并入闭包;重复扫描,直到一整轮下来闭包没有新增属性;判断某条依赖X→A是否成立,就看A是否落在X(F)+里。我用一个生活类比辅助理解:闭包就是“你认识的人,加上你认识的人的朋友”。假设你认识A,A认识B,B认识C,那么你通过A能认识B,再通过B能认识C,扩散到没有人再提供新面孔为止。谁进了这个圈子,谁就是闭包成员。举个微型例子。设F={A→B, B→C, C→A},让你算B的闭包。初始是{B},B→C把C带进来,闭包变成{B,C};接着C→A把A带进来,闭包变成{A,B,C};再扫一轮,A→B、B→C、C→A都没有新属性了,所以B(F)+={A,B,C}。2.2 覆盖与等价:“谁更能推”的判断标尺接下来是覆盖。如果对于G中的每一条依赖X→A,都能从F推出,即A∈X(F)+,我们就说F覆盖G。如果F覆盖G且G覆盖F,那么F和G等价。等价说白了就是“推理能力完全一样”。这在极小化里有什么用?作用大了。极小化的每次删除操作,都要验证“删完之后的新集合与原集合等价”。一个依赖X→A想从F里删掉,只需要验证A属于X在“去掉它之后的集合”下的闭包。因为这时新的依赖集仍能推出X→A,而原来能推出的其他依赖没有减少,等价关系就保住了。这个判定式是全篇最核心的公式,你最好把它抄在笔记第一行:想删X→A,就算A是否在X(F-{X→A})+里。后面所有的删依赖动作,都是在重复套这条公式。再补充一个覆盖判断的小例子。假设F={A→B, B→C, C→A},G={A→C}。判断F是否覆盖G,算A在F下的闭包,得到{A,B,C},C在里面,所以成立。反过来G当然覆盖不了F,因为G连B都推不出来,所以两者不等价。这种来回验证的思路,就是做题时的“体检”。2.3 考试中提高闭包速度的小规矩闭包计算看起来简单,但依赖一多就容易漏。实际做题建议:每一轮扫描时,在纸上把新加入的属性用下划线标出来,避免重复判定;一条依赖左侧属性已经全部出现时,立刻并入右侧并划掉那条依赖;已经用过的依赖如果右侧没有带来新属性,下一轮就不必再看。很多同学做题慢,不是不会,而是每次都从头扫到尾,导致闭包反复空转。把“扫描+标记+归并”做成肌肉记忆,闭包一分钟内可以算完一组小型依赖集。闭包速度上来之后,极小化题目基本就不再受时间困扰了。3. 求解算法三阶段:拆、删、消的完整逻辑进入核心环节。给定一张函数依赖集F,按以下三个阶段的顺序执行,就能得到Fmin:第一阶段拆右部,第二阶段删冗余依赖,第三阶段消左部冗余属性。下面逐一拆开讲。3.1 阶段一:右部属性逐条拆开对F中每一条右部含多个属性的依赖,例如X→AB,替换成两条:X→A和X→B。这个动作没有任何推导技巧,唯一注意点是不要顺手在拆右部时就把左部拆开。左部属性是否冗余是后面第三阶段的活儿,拆右部阶段要保持左部原样,否则会让后面的等价性判定变混乱。为什么右部必须先拆?第一是因为极小集第一条硬性要求就是右部单属性;第二是因为把右部分开后,每条依赖的冗余性、左部冗余性都能独立判断,不会被“半冗余”状态干扰。比如X→AB里,可能A冗余但B不冗余,整体依赖没法简单删,必须先拆开才能分别处理。3.2 阶段二:删除冗余依赖对当前依赖集中的每一条依赖X→A,做一次“试删”:临时把它从F中拿走,得到F′;在F′下计算X的闭包X(F′)+;如果A仍然在闭包里,说明这条依赖是多余的,正式删除;如果A不在闭包里,说明这条依赖必不可少,放回。这里必须特别强调:判断用的是“去掉这条依赖之后的集合”,而不是原始F。更关键的是,一旦前面有依赖被删除,后续判断全部基于删完之后的当前集合。很多同学用原始F一口气算到底,遇到带传递环的题就会把本不该删的删掉,或该删的删不掉。考试时建议在草稿纸上把“当前F”单独写成一行,每删一条就更新一次,别怕麻烦。这行字既是你的计算依据,也是阅卷老师看步骤分的线索。3.3 阶段三:消左部冗余属性经过阶段二,剩下的依赖都“不可或缺”了,但左部还可能带着多余属性。以AB→C为例,要判断A是否多余,就是把A临时拿掉,看剩下的B在当前F下能不能推出C。如果能,A就是冗余的,把AB→C换成B→C;如果不能,保留A。注意这里的闭包计算对象,是去掉属性后的左部Z,而不是原来的X。很多同学在AB→C里判断B是否冗余,下意识就去算AB的闭包,这是错的。正确的问法是:如果左部只剩A,还能不能推出C?所以你要算的是A在当前F下的闭包,看C在不在里面。左部有多余属性的依赖,每消掉一个属性后,要回头重新检查剩下的左部。比如ABC→D,先发现B冗余变成AC→D,这时还要再检查A和C,直到没有属性可以再减。这个“连环消元”的动作,在综合题里非常常见。3.4 三个阶段能不能换顺序教材上有两种流派:有的先删冗余依赖再消左部属性,有的先消左部属性再删冗余依赖。考试我强烈建议用“拆→删→消”的标准顺序。原因是:冗余依赖如果还留在集合里,计算闭包时会把结果“撑大”,导致左部属性是否冗余的判断失真。先删干净,再做左部消元,每一步闭包计算的对象都是干净集合,判定更可靠。顺序本身不一定严格影响最终正确性,但按固定流程能避免大量低级失误。答题时在开头写一句“按右部拆分、冗余依赖删除、左部属性消除的顺序处理”,阅卷老师一眼就能看出你思路清晰。4. 两道高频考题的完整手算推演前面讲的是套路,下面完整算两道题。题目难度都不高,但包含了极小化几乎全部关键动作。4.1 例题A:一个纯粹的冗余依赖删除过程设F={A→B, B→A, B→C, A→C, C→A},求Fmin。右部本来就是单属性,跳过拆分,直接进入删依赖阶段。当前F就是原F。先看A→B。临时去掉它,在F-{A→B}下计算A的闭包。A→C把C带进来,C→A形成闭环,闭包停在{A,C},始终没有B,所以A→B不能删。再看B→A。去掉它,在剩余集合中计算B的闭包:B→C得到C,C→A得到A,A→B又带回B,闭包扩张到{A,B,C}。A在闭包内,说明B→A可以由其余依赖推出,删除B→A。当前F更新为{A→B, B→C, A→C, C→A}。接着看B→C。去掉后,当前集合是{A→B, A→C, C→A},B从这里出发没有任何以B开头的依赖,闭包只有{B},所以B→C保留。再看A→C。去掉后,在{A→B, B→C, C→A}下算A的闭包:A→B得到B,B→C得到C,C→A闭环,闭包包含C,A→C冗余,删除。当前F变为{A→B, B→C, C→A}。最后看C→A。去掉它后,{A→B, B→C}中C没有可推出的属性,闭包只有{C},不含A,C→A保留。阶段二结束,当前F={A→B, B→C, C→A}。所有依赖左部都是单属性,第三阶段无需操作,最终Fmin就是{A→B, B→C, C→A}。这个例子的重点在于:删除B→A之后,判断A→C用的已经是更新后的集合。如果你始终用原始F算,这一步闭包也会包含C,结果碰巧一样,但一旦遇到更绕的环形依赖,差别就会立刻显现。养成“删一条、更新一条”的习惯,永远不会错。4.2 例题B:同时存在冗余依赖与冗余左部的综合题设F={AB→C, C→A, A→D, AD→E, E→A, D→B, B→C},求Fmin。先拆右部,所有依赖的右部已经是单属性,跳过。阶段二删冗余依赖。先看AB→C,去掉它,在剩余集合{C→A, A→D, AD→E, E→A, D→B, B→C}下计算AB的闭包。初始是{A,B},B→C直接把C带进来,AB的闭包已经包含C,所以AB→C是冗余的,正式删除。当前F更新为{C→A, A→D, AD→E, E→A, D→B, B→C}。接下来逐条检查剩余的依赖。C→A去掉后,C的闭包只有{C},不含A;A→D去掉后,A的闭包只有{A},不含D;AD→E去掉后,AD的闭包从{A,D}出发,经D→B得到B,B→C得到C,C→A得到A,最后停在{A,B,C,D},始终没有E;E→A去掉后,E的闭包只有{E};D→B去掉后,D的闭包只有{D};B→C去掉后,B的闭包只有{B}。这些依赖右部都不在对应闭包内,所以全部保留。阶段三消左部。当前F={C→A, A→D, AD→E, E→A, D→B, B→C},只有AD→E的左部有多个属性。先判A是否冗余。临时去掉A,问题变成:D在当前F下能不能推出E?算D的闭包:D→B得到B,B→C得到C,C→A得到A,A→D闭环,此时闭包里已经有A、B、C、D,而AD→E的左侧A和D都被推出了,所以E也能进入闭包。D的闭包包含E,说明A冗余,把AD→E替换成D→E。当前F更新为{C→A, A→D, D→E, E→A, D→B, B→C}。现在所有依赖左部都是单属性,算法结束。最终Fmin={C→A, A→D, D→E, E→A, D→B, B→C}。这道题同时体现了删依赖和消左部两个动作。注意消左部时,我计算的是D的闭包而不是AD的闭包,这一步对应“去掉A后,D还能不能推出E”的问题。D能推出E,A就是多余的,这个依赖就可以从AD→E瘦身成D→E。4.3 结果的自我校验算出结果不是终点,考试时间充裕时务必做等价性验证。理论上最终G要满足:对G中每条依赖Y→B,B在Y关于原始F的闭包中;对F中每条依赖X→A,A在X关于G的闭包中。完整验证比较费时间,但快速校验至少可以抽查几条。拿例题B的结果来说,G={C→A, A→D, D→E, E→A, D→B, B→C}还原回原F的推理关系:原F里的AB→C,在G下由B→C直接推出;原F里的AD→E,在G下由D→E推出。反过来,G里每一条都能从原F推出,比如D→E在原F下需要走D→B、B→C、C→A、A→D,凑齐AD后再用AD→E。这种互相覆盖的检查做完,你会非常踏实,考试时就不怕“算对了但怀疑自己”。5. 考场中最容易翻车的细节与检查清单5.1 高频错误对照我观察到的错题,前四个翻车点最值得总结。第一,删除依赖时用原始F算闭包,而不是去掉该依赖后的当前集。第二,消左部属性时算错对象,比如AB→C判断B是否冗余,应该算A的闭包,很多人却去算AB的闭包。第三,拆右部之后把左部也顺手拆掉,或者右部只拆了一半。第四,每一步删除后不更新集合,导致后面对同一依赖反复判断,越算越乱。这四个错误有一个共同根源:没有时刻盯着“当前依赖集”。我建议做题时把当前F单独写一行,每删一次就划掉更新一次,闭包计算的每一轮都对照这行最新的集合来写推导。从答题角度讲,这行“当前F”也是重要的得分依据,写清楚不仅能防自己出错,还能让老师顺藤摸瓜给步骤分。5.2 考场快速检查模板进入考场前,把下面这份模板刻在脑子里。拿到题目,第一步检查F中是否有右部多属性的依赖,有则拆,拆完重新整理。第二步对每条依赖做“试删”,临时删掉它,算左侧闭包,看右部是否还在;删除后立刻更新集合,一条一条来。第三步对每个左部多属性的依赖逐个属性做减法测试,算出“去掉该属性后的集合”的闭包,看能否推出右部,能推就删属性,删完回头重新检查剩下属性。第四步把最终结果与原F互相覆盖验证,至少抽查原F里的每条依赖是否都能由结果推出。最后检查结果格式:右部全部单属性,没有哪条依赖删掉后仍可被推出,没有哪个左部属性删掉后仍不影响结论。四条全过,这题基本就稳了。如果题目明确要求“求极小函数依赖集”,过程的书写比最终集合更重要。极小集不唯一,但每一步的推理理由可以统一为同一套闭包判定,你只要把这套判定写清楚,结果不同也没关系。5.3 考前复习节奏建议我在复习备考数据库时,最后一周每天只做两件事:上午练五道闭包计算小题,下午做两道极小化综合题。闭包是极小化的基本功,练到闭包手算不再出错,极小化题目就只是套流程。遇到算出来和参考答案不一样时,不要急着改答案,先检查自己“当前F”每一步更新是否正确、闭包有没有漏推新属性,再检查是不是删了本不该删的依赖。多半是这两个原因。另外一个实用教训:不要试图背题。我见过不少同学背了大量答案,考场换一组依赖就懵了。极小化的核心其实就是一个循环——试删、算闭包、看结果、更新集合。把这个循环理解透,题怎么变都只是同一套动作的重复。先把闭包练稳,再谈极小化,顺序千万别反。
函数依赖的极小化,大概是数据库原理课里最让人“背得下定义却下不了笔”的考点了。提到极小函数依赖集,多数人都能背出三条:右部单属性、没有冗余依赖、没有冗余属性。可真给你一张函数依赖集F,让你求Fmin,很多人第一步就卡住:先拆?先删?用谁的闭包?删完还能不能删?这篇就是冲着这些问题来的。我会把极小函数依赖集的求解算法拆成一套可以直接照做的流程,配合两道手算例题,把闭包计算、覆盖判定、删依赖、消属性这些动作全部过一遍,顺便把考试里最容易扣分的细节都指出来。这篇文章适合正在准备数据库期末考试、考研初试的同学,也适合自学数据库原理但被教材例题绕晕的开发者。你不需要多深的基础,只要知道函数依赖是“属性之间的决定关系”,就能跟着走下来。先说明一点:我不只给步骤,我会反复讲“为什么这一步要这么干”,因为考试改步骤分的时候,理由往往比最终答案更重要。1. 先读懂考纲:极小函数依赖集要消灭的到底是什么冗余1.1 三条定义的直白翻译教材上,函数依赖集F的极小化(也叫最小覆盖Fmin)一般要求满足三个条件:F中每个函数依赖的右部是单属性;F中不存在冗余依赖;F中不存在冗余属性。这些定义很多同学背得滚瓜烂熟,却不理解。“右部单属性”好办,就是把X→AB这种依赖替换成X→A和X→B两条。“无冗余依赖”的意思是:删掉某一条依赖之后,剩下的依赖集仍然能推出原来那条依赖,那这条依赖就是多余的。“无冗余属性”指的是:一条依赖左侧如果带着A、B两个属性,而去掉B之后只剩A也能推出右边的结论,那么B就是多余的。这三条读完,你再回头看题目,会发现极小化其实根本不是让你去“背算法”,而是让你反复回答三个问题:这条依赖能不能删?删了之后推理能力还在吗?左部这些属性是不是有凑数的?回答清楚,答案自然出来了。1.2 极小化的本质:推理能力不变的“钥匙串整理”用一句话归纳:Fmin是F的“等价但最小”版本。所谓等价,是说F能推出的每一个函数依赖,Fmin也能推出;反过来Fmin能推出的,F也必然能推出。瘦身,就是在保持这种推理能力不变的前提下尽量删除和压缩。我给学生讲的时候喜欢打一个比方:你有一串钥匙,每把钥匙都能开某一扇门,但有一把钥匙能开的门,别的钥匙全能开,那这把就是冗余依赖;有的钥匙上挂了一个装饰环,去掉环完全不影响开门,这个环就是冗余属性。题目要你做的,就是把钥匙串整理到最精简,但每一扇原来看得上的门都还必须开得了。这个比方看着简单,其实非常贴近算法本质。尤其是后面做删依赖和消左部判断时,你只要反复问自己一句“去掉它,别的钥匙还能开这扇门吗”,思路就不会乱。1.3 极小集不唯一,先给自己松个绑教材里常说“函数依赖集的极小化”,但很少强调一个关键事实:Fmin不是唯一的。删除冗余依赖的顺序不同、消左部属性的先后不同,很可能得到不同的极小集。比如A→B和B→A两条能互相推出的依赖,你先删A→B和后删B→A,留下来的就不一样。考试里只要最终结果满足三条条件且与原集等价,都应该算对。这一点非常容易让考生怀疑自己算错,先打一个预防针:不要因为答案和同学不一样就立刻觉得自己错了,先检查每一逻辑链是否成立。2. 前置能力:闭包与覆盖,整套算法的手感来源极小化的每一步其实都在做“比较”:删掉一条依赖后,还能不能推出原来那条?左部去掉一个属性后,还能不能推出右部?这些话落到计算上,全靠“属性闭包”和“依赖覆盖”两件工具。不把这两个技能练熟,背再多步骤也是空中楼阁。2.1 属性闭包的手工算法给定属性集X和依赖集F,X关于F的闭包记作X(F)+,含义是:从X出发,用F里的依赖反复推导,最终能得到的所有属性集合。手工计算很机械,但必须养成固定节奏:把闭包集合初始化为X本身;循环扫描F,任何一条依赖左侧已经全部出现在当前闭包中,就把它的右侧属性并入闭包;重复扫描,直到一整轮下来闭包没有新增属性;判断某条依赖X→A是否成立,就看A是否落在X(F)+里。我用一个生活类比辅助理解:闭包就是“你认识的人,加上你认识的人的朋友”。假设你认识A,A认识B,B认识C,那么你通过A能认识B,再通过B能认识C,扩散到没有人再提供新面孔为止。谁进了这个圈子,谁就是闭包成员。举个微型例子。设F={A→B, B→C, C→A},让你算B的闭包。初始是{B},B→C把C带进来,闭包变成{B,C};接着C→A把A带进来,闭包变成{A,B,C};再扫一轮,A→B、B→C、C→A都没有新属性了,所以B(F)+={A,B,C}。2.2 覆盖与等价:“谁更能推”的判断标尺接下来是覆盖。如果对于G中的每一条依赖X→A,都能从F推出,即A∈X(F)+,我们就说F覆盖G。如果F覆盖G且G覆盖F,那么F和G等价。等价说白了就是“推理能力完全一样”。这在极小化里有什么用?作用大了。极小化的每次删除操作,都要验证“删完之后的新集合与原集合等价”。一个依赖X→A想从F里删掉,只需要验证A属于X在“去掉它之后的集合”下的闭包。因为这时新的依赖集仍能推出X→A,而原来能推出的其他依赖没有减少,等价关系就保住了。这个判定式是全篇最核心的公式,你最好把它抄在笔记第一行:想删X→A,就算A是否在X(F-{X→A})+里。后面所有的删依赖动作,都是在重复套这条公式。再补充一个覆盖判断的小例子。假设F={A→B, B→C, C→A},G={A→C}。判断F是否覆盖G,算A在F下的闭包,得到{A,B,C},C在里面,所以成立。反过来G当然覆盖不了F,因为G连B都推不出来,所以两者不等价。这种来回验证的思路,就是做题时的“体检”。2.3 考试中提高闭包速度的小规矩闭包计算看起来简单,但依赖一多就容易漏。实际做题建议:每一轮扫描时,在纸上把新加入的属性用下划线标出来,避免重复判定;一条依赖左侧属性已经全部出现时,立刻并入右侧并划掉那条依赖;已经用过的依赖如果右侧没有带来新属性,下一轮就不必再看。很多同学做题慢,不是不会,而是每次都从头扫到尾,导致闭包反复空转。把“扫描+标记+归并”做成肌肉记忆,闭包一分钟内可以算完一组小型依赖集。闭包速度上来之后,极小化题目基本就不再受时间困扰了。3. 求解算法三阶段:拆、删、消的完整逻辑进入核心环节。给定一张函数依赖集F,按以下三个阶段的顺序执行,就能得到Fmin:第一阶段拆右部,第二阶段删冗余依赖,第三阶段消左部冗余属性。下面逐一拆开讲。3.1 阶段一:右部属性逐条拆开对F中每一条右部含多个属性的依赖,例如X→AB,替换成两条:X→A和X→B。这个动作没有任何推导技巧,唯一注意点是不要顺手在拆右部时就把左部拆开。左部属性是否冗余是后面第三阶段的活儿,拆右部阶段要保持左部原样,否则会让后面的等价性判定变混乱。为什么右部必须先拆?第一是因为极小集第一条硬性要求就是右部单属性;第二是因为把右部分开后,每条依赖的冗余性、左部冗余性都能独立判断,不会被“半冗余”状态干扰。比如X→AB里,可能A冗余但B不冗余,整体依赖没法简单删,必须先拆开才能分别处理。3.2 阶段二:删除冗余依赖对当前依赖集中的每一条依赖X→A,做一次“试删”:临时把它从F中拿走,得到F′;在F′下计算X的闭包X(F′)+;如果A仍然在闭包里,说明这条依赖是多余的,正式删除;如果A不在闭包里,说明这条依赖必不可少,放回。这里必须特别强调:判断用的是“去掉这条依赖之后的集合”,而不是原始F。更关键的是,一旦前面有依赖被删除,后续判断全部基于删完之后的当前集合。很多同学用原始F一口气算到底,遇到带传递环的题就会把本不该删的删掉,或该删的删不掉。考试时建议在草稿纸上把“当前F”单独写成一行,每删一条就更新一次,别怕麻烦。这行字既是你的计算依据,也是阅卷老师看步骤分的线索。3.3 阶段三:消左部冗余属性经过阶段二,剩下的依赖都“不可或缺”了,但左部还可能带着多余属性。以AB→C为例,要判断A是否多余,就是把A临时拿掉,看剩下的B在当前F下能不能推出C。如果能,A就是冗余的,把AB→C换成B→C;如果不能,保留A。注意这里的闭包计算对象,是去掉属性后的左部Z,而不是原来的X。很多同学在AB→C里判断B是否冗余,下意识就去算AB的闭包,这是错的。正确的问法是:如果左部只剩A,还能不能推出C?所以你要算的是A在当前F下的闭包,看C在不在里面。左部有多余属性的依赖,每消掉一个属性后,要回头重新检查剩下的左部。比如ABC→D,先发现B冗余变成AC→D,这时还要再检查A和C,直到没有属性可以再减。这个“连环消元”的动作,在综合题里非常常见。3.4 三个阶段能不能换顺序教材上有两种流派:有的先删冗余依赖再消左部属性,有的先消左部属性再删冗余依赖。考试我强烈建议用“拆→删→消”的标准顺序。原因是:冗余依赖如果还留在集合里,计算闭包时会把结果“撑大”,导致左部属性是否冗余的判断失真。先删干净,再做左部消元,每一步闭包计算的对象都是干净集合,判定更可靠。顺序本身不一定严格影响最终正确性,但按固定流程能避免大量低级失误。答题时在开头写一句“按右部拆分、冗余依赖删除、左部属性消除的顺序处理”,阅卷老师一眼就能看出你思路清晰。4. 两道高频考题的完整手算推演前面讲的是套路,下面完整算两道题。题目难度都不高,但包含了极小化几乎全部关键动作。4.1 例题A:一个纯粹的冗余依赖删除过程设F={A→B, B→A, B→C, A→C, C→A},求Fmin。右部本来就是单属性,跳过拆分,直接进入删依赖阶段。当前F就是原F。先看A→B。临时去掉它,在F-{A→B}下计算A的闭包。A→C把C带进来,C→A形成闭环,闭包停在{A,C},始终没有B,所以A→B不能删。再看B→A。去掉它,在剩余集合中计算B的闭包:B→C得到C,C→A得到A,A→B又带回B,闭包扩张到{A,B,C}。A在闭包内,说明B→A可以由其余依赖推出,删除B→A。当前F更新为{A→B, B→C, A→C, C→A}。接着看B→C。去掉后,当前集合是{A→B, A→C, C→A},B从这里出发没有任何以B开头的依赖,闭包只有{B},所以B→C保留。再看A→C。去掉后,在{A→B, B→C, C→A}下算A的闭包:A→B得到B,B→C得到C,C→A闭环,闭包包含C,A→C冗余,删除。当前F变为{A→B, B→C, C→A}。最后看C→A。去掉它后,{A→B, B→C}中C没有可推出的属性,闭包只有{C},不含A,C→A保留。阶段二结束,当前F={A→B, B→C, C→A}。所有依赖左部都是单属性,第三阶段无需操作,最终Fmin就是{A→B, B→C, C→A}。这个例子的重点在于:删除B→A之后,判断A→C用的已经是更新后的集合。如果你始终用原始F算,这一步闭包也会包含C,结果碰巧一样,但一旦遇到更绕的环形依赖,差别就会立刻显现。养成“删一条、更新一条”的习惯,永远不会错。4.2 例题B:同时存在冗余依赖与冗余左部的综合题设F={AB→C, C→A, A→D, AD→E, E→A, D→B, B→C},求Fmin。先拆右部,所有依赖的右部已经是单属性,跳过。阶段二删冗余依赖。先看AB→C,去掉它,在剩余集合{C→A, A→D, AD→E, E→A, D→B, B→C}下计算AB的闭包。初始是{A,B},B→C直接把C带进来,AB的闭包已经包含C,所以AB→C是冗余的,正式删除。当前F更新为{C→A, A→D, AD→E, E→A, D→B, B→C}。接下来逐条检查剩余的依赖。C→A去掉后,C的闭包只有{C},不含A;A→D去掉后,A的闭包只有{A},不含D;AD→E去掉后,AD的闭包从{A,D}出发,经D→B得到B,B→C得到C,C→A得到A,最后停在{A,B,C,D},始终没有E;E→A去掉后,E的闭包只有{E};D→B去掉后,D的闭包只有{D};B→C去掉后,B的闭包只有{B}。这些依赖右部都不在对应闭包内,所以全部保留。阶段三消左部。当前F={C→A, A→D, AD→E, E→A, D→B, B→C},只有AD→E的左部有多个属性。先判A是否冗余。临时去掉A,问题变成:D在当前F下能不能推出E?算D的闭包:D→B得到B,B→C得到C,C→A得到A,A→D闭环,此时闭包里已经有A、B、C、D,而AD→E的左侧A和D都被推出了,所以E也能进入闭包。D的闭包包含E,说明A冗余,把AD→E替换成D→E。当前F更新为{C→A, A→D, D→E, E→A, D→B, B→C}。现在所有依赖左部都是单属性,算法结束。最终Fmin={C→A, A→D, D→E, E→A, D→B, B→C}。这道题同时体现了删依赖和消左部两个动作。注意消左部时,我计算的是D的闭包而不是AD的闭包,这一步对应“去掉A后,D还能不能推出E”的问题。D能推出E,A就是多余的,这个依赖就可以从AD→E瘦身成D→E。4.3 结果的自我校验算出结果不是终点,考试时间充裕时务必做等价性验证。理论上最终G要满足:对G中每条依赖Y→B,B在Y关于原始F的闭包中;对F中每条依赖X→A,A在X关于G的闭包中。完整验证比较费时间,但快速校验至少可以抽查几条。拿例题B的结果来说,G={C→A, A→D, D→E, E→A, D→B, B→C}还原回原F的推理关系:原F里的AB→C,在G下由B→C直接推出;原F里的AD→E,在G下由D→E推出。反过来,G里每一条都能从原F推出,比如D→E在原F下需要走D→B、B→C、C→A、A→D,凑齐AD后再用AD→E。这种互相覆盖的检查做完,你会非常踏实,考试时就不怕“算对了但怀疑自己”。5. 考场中最容易翻车的细节与检查清单5.1 高频错误对照我观察到的错题,前四个翻车点最值得总结。第一,删除依赖时用原始F算闭包,而不是去掉该依赖后的当前集。第二,消左部属性时算错对象,比如AB→C判断B是否冗余,应该算A的闭包,很多人却去算AB的闭包。第三,拆右部之后把左部也顺手拆掉,或者右部只拆了一半。第四,每一步删除后不更新集合,导致后面对同一依赖反复判断,越算越乱。这四个错误有一个共同根源:没有时刻盯着“当前依赖集”。我建议做题时把当前F单独写一行,每删一次就划掉更新一次,闭包计算的每一轮都对照这行最新的集合来写推导。从答题角度讲,这行“当前F”也是重要的得分依据,写清楚不仅能防自己出错,还能让老师顺藤摸瓜给步骤分。5.2 考场快速检查模板进入考场前,把下面这份模板刻在脑子里。拿到题目,第一步检查F中是否有右部多属性的依赖,有则拆,拆完重新整理。第二步对每条依赖做“试删”,临时删掉它,算左侧闭包,看右部是否还在;删除后立刻更新集合,一条一条来。第三步对每个左部多属性的依赖逐个属性做减法测试,算出“去掉该属性后的集合”的闭包,看能否推出右部,能推就删属性,删完回头重新检查剩下属性。第四步把最终结果与原F互相覆盖验证,至少抽查原F里的每条依赖是否都能由结果推出。最后检查结果格式:右部全部单属性,没有哪条依赖删掉后仍可被推出,没有哪个左部属性删掉后仍不影响结论。四条全过,这题基本就稳了。如果题目明确要求“求极小函数依赖集”,过程的书写比最终集合更重要。极小集不唯一,但每一步的推理理由可以统一为同一套闭包判定,你只要把这套判定写清楚,结果不同也没关系。5.3 考前复习节奏建议我在复习备考数据库时,最后一周每天只做两件事:上午练五道闭包计算小题,下午做两道极小化综合题。闭包是极小化的基本功,练到闭包手算不再出错,极小化题目就只是套流程。遇到算出来和参考答案不一样时,不要急着改答案,先检查自己“当前F”每一步更新是否正确、闭包有没有漏推新属性,再检查是不是删了本不该删的依赖。多半是这两个原因。另外一个实用教训:不要试图背题。我见过不少同学背了大量答案,考场换一组依赖就懵了。极小化的核心其实就是一个循环——试删、算闭包、看结果、更新集合。把这个循环理解透,题怎么变都只是同一套动作的重复。先把闭包练稳,再谈极小化,顺序千万别反。