和弦命名
* * * *
拉格朗日计划
* * * *
和弦命名

给定一系列三和弦,打印每组三和弦的名称,名称由根音和类型两部分组成。

三和弦由三个音组成,每个音符用一个大写字母表示,每个字母还可以接一个变音记号,变音记号有两种,分别是升记号(♯ U+266F)和降记号(♭ U+266D)。

十二平均律由十二个音级组成,由于升降记号♯和♭的存在,有些音级可以有两种记法,具体如下:

0   A	
1   A♯   B♭
2   B    C♭
3   C    B♯
4   C♯   D♭
5   D	
6   D♯   E♭
7   E    F♭
8   F    E♯
9   F♯   G♭
10  G	
11  G♯   A♭
三和弦的三个音可以按顺序排成依次相差三度的音,其中相差三度又可以分为大三度和小三度:

小三度是指音级间的距离为3,例如 A C是小三度(0到3)。

大三度是指音级间的距离为4,例如 C E是大三度(3到7)。

排序后位置最前的音称为根音。注意根音不一定是音级最小的音,因为音级的计算可以视作是加法群$\mathbb Z_{12}$中的运算,例如E G B中E是根音。

若用X Y Z表示排序后的三个音,那么根据X Y和Y Z是大三度或小三度,共有四种情况,其名称如下:

减三弦:小+小	例子:B D F
小三弦:小+大	例子:E G B
大三弦:大+小	例子:C E G
增三弦:大+大	例子:D F♯ A♯
输入格式:

三个音符的字母总是可以排成在字母表中的位置(G之后再从A开始)依次相差二的序列,例如音级分别为$3,6,9$可能会以B♯ D♯ F♯或C E♭ G♭的形式出现,但不会以C D♯ F♯的形式出现(C与D只相差一)。

输入的音符是乱序的,例如C E G或E C G都可能出现。

输出格式:

先输出根音的字母和变音记号,再根据三弦的类型输出至多一个字符,共有四种情况:

若为减三弦,输出°(U+00B0)

若为小三弦,输出m

若为大三弦,无需输出

若为增三弦,输出+

注:在本题中,由于输入数据已经限定了格式,根音只需通过字母判断,将三个音符的字母排成在字母表中的位置(Z之后再从A开始)依次相差二的序列,位置最前的就是根音的字母。例如E G♯ C和E G♯ B♯,正确的输出分别是C+和E+。

注:本人不了解乐理,此处的翻译只能力求尽可能准确。

本题难度:



解答

先提取首字母并排序,一般情况下最小的字母就是根音的字母,涉及到经过G又回到A的例外情况有四种,通过字典q将这四种例外映射到正确的排序。

提取首字母的同时用字典b记录首字母和音符的对应关系,据此输出根音。

用匿名函数h通过字符串查找计算音级。

设三个音级分别是XYZ,亦即Y-X和Z-Y分别有(3,3)、(3,4)、(4,3)、(4,4)这四种情况,简单尝试可以发现3Y-2Z-X+5模12是这四组数对到0123的单射,将之视作字符串“m+° ”的下标即可输出类型。

最终代码有四行。

代码长度:283字节 vs. 全站第一:127字节。

注:本题的难度来自理解题意和设计判断三和弦类型的哈希函数。

import sys
h=lambda x:'A-BC-D-EF-G'.find(x[0])+{'♯':1,'♭':-1}.get(x[-1],0)
q={'ADF':'DFA','BEG':'EGB','ACF':'FAC','BDG':'GBD'}
for a in sys.argv[1:]:b={i[0]:i for i in a.split()};c=''.join(sorted(b.keys()));x,y,z=q.get(c,c);print(b[x]+'m+° '[(3*h(b[y])-2*h(b[z])-h(b[x])+5)%12])