如何解决涉及大排列的性能问题
我正在创建一个程序来计算弦乐器上的和弦指法。这就是我所拥有的:
from itertools import product
# Returns the notes you get from a certain fingering with a certain tuning
def notes(tuning,fingering):
return map(lambda x,y: (x+y)%12,tuning,fingering)
# Just a constraint function to filter out chords impossible to finger due to too large spreading.
def spread(fingering):
return max(fingering) - min(i for i in fingering if i > 0)
# Get all the possible fingerings for a certain chord
def fingering(tuning,chord):
return [i for i in product(range(12),repeat=len(tuning)) if
set(notes(tuning,i)) == set(chord) and
spread(i) < 5]
示例输出:
>>> cf.fingering([0,5,10],[2,7])
[[2,2,4],[7,9,9]]
>>> cf.fingering([0,1,2],3])
[[2,1],0],[3,0]]
到目前为止,这似乎有效,只是它太慢了。当我将它用于 7 弦吉他(tuning
是一个长度为 7 的列表)时,计算需要大约 45 秒。
我想把它降低到对人类来说是瞬时的,所以大约 0.1 秒左右。几秒钟是可以接受的。
我怀疑问题在于 product
生成所有可能的列表,然后再过滤它们。在生成之前过滤会更有效,但我不知道如何实现这一点。
解决方法
按照我的评论:
找出每根弦上的哪个品格会演奏和弦所需的音符应该相当容易。然后得到这些品丝集的乘积,而不是迭代所有可能的组合。
import itertools
MAX_FRETS = 24
FRETS_PER_OCTAVE = 12
MAX_SPREAD = 5 # Because everyone isn't John Petrucci https://i.stack.imgur.com/tKIKA.png
def fingering(tuning,chord):
chord = set(chord)
allowed_frets = [] # {note: [] for note in chord}
for string_num,open_string_note in enumerate(tuning):
string_frets = []
for wanted_note in chord:
wanted_fret = wanted_note - open_string_note
while wanted_fret <= MAX_FRETS:
if wanted_fret >= 0:
string_frets.append((wanted_fret,wanted_note))
wanted_fret += FRETS_PER_OCTAVE
# Now we have all the frets on this string that will give us one of the
# notes we need in string_frets
allowed_frets.append(string_frets)
fingerings = []
# Now,allowed_frets[i] gives us the frets that are useful on the i-th string
# You can only select one fret per string,so you'd run product() on these and
# then check if that product makes the chord.
for selection in itertools.product(*allowed_frets):
# Check if selection contains all the required notes
selection_notes = set()
selection_frets = []
for fret,note in selection:
selection_notes.add(note)
selection_frets.append(fret)
if chord == selection_notes and max(selection_frets) - min(selection_frets) < MAX_SPREAD:
fingerings.append(selection)
return fingerings
# Running for your first example:
f = fingering([0,5,10],{2,7})
使用 print(f)
,我们得到:
((2,2),(2,7),(4,2))
((14,(14,(16,2))
((7,(9,7))
((19,(21,7))
记住,每个元组的第一个元素是品格,第二个是音符。此方法找到了您的方法的 [2,2,4]
和 [7,9,9]
结果,但在指板更高处也找到了相同的和弦。如果您不想这样做,可以将 while wanted_fret <= MAX_FRETS
循环更改为 while wanted_fret <= FRETS_PER_OCTAVE + MAX_SPREAD
用三个字符串对两个函数进行计时,我们看到了大约 25 倍的加速:
%timeit fingering_yours([0,[2,7])
2.37 ms ± 294 µs per loop (mean ± std. dev. of 7 runs,100 loops each)
%timeit fingering([0,7})
96.1 µs ± 14.1 µs per loop (mean ± std. dev. of 7 runs,10000 loops each)
实际的三音和弦 (Cmaj)在标准调的也无关六弦吉他上并不重要,它提供了更大的加速~50x
%timeit fingering_yours([4,7,11,4],[0,4,7])
6.3 s ± 554 ms per loop (mean ± std. dev. of 7 runs,1 loop each)
%timeit fingering([4,{0,7})
127 ms ± 4.68 ms per loop (mean ± std. dev. of 7 runs,10 loops each)
也许可以进行更多优化,但这留给读者作为练习。
如果我们不想强制要求必须播放所有字符串,我们可以将 None
值添加到 string_frets
数组以指示“没有音符" 是一个有效的可能性。
# Change this line:
string_frets = []
# to this:
string_frets = [(None,None)] # Start off allowing "None" to be a valid note played on this string
接下来,当我们检查 None
中的每个 selection
时,我们需要过滤掉 itertools.product(...)
。
for fret,note in selection:
# New condition: Only append if the string is actually being played
if fret is not None:
selection_notes.add(note)
selection_frets.append(fret)
# New condition: No strings are being played,so this is not a chord,# skip this iteration
if not selection_frets:
continue
在三个字符串上用二元组试试这个,我们得到以下组合,看起来很合理:
((None,None),7))
((None,2))
((None,2))
((2,(None,None))
((2,7))
((14,None))
((14,7))
((7,None))
((7,2))
((19,None))
((19,2))
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。