更好的算法来洗牌(或交错)多个不同长度的列表
我喜欢在外出时观看我最喜欢的电视节目。我在播放列表中保存了我正在追的每个节目的所有剧集。不过,不是所有的节目都有相同数量的剧集。与一些喜欢一次性看完所有剧集的人不同,我喜欢把一个节目的剧集和另一个节目的剧集交替播放。
举个例子,如果我有一个叫ABC的节目有2集,还有一个叫XYZ的节目有4集,我希望我的播放列表看起来像这样:
XYZe1.mp4
ABCe1.mp4
XYZe2.mp4
XYZe3.mp4
ABCe2.mp4
XYZe4.mp4
生成这种交替播放列表的一种方法是把每个节目看作是一系列剧集,然后对所有节目进行一种叫做“riffle shuffle”的洗牌。可以写一个函数,计算每集剧集在一个单位时间区间内的位置(这个位置在0.0到1.0之间,0.0代表季节的开始,1.0代表季节的结束),然后根据这些位置对所有剧集进行排序。
我在Python 2.7中写了一个简单的函数来执行这种洗牌:
def riffle_shuffle(piles_list):
scored_pile = ((((item_position + 0.5) / len(pile), len(piles_list) - pile_position), item) for pile_position, pile in enumerate(piles_list) for item_position, item in enumerate(pile))
shuffled_pile = [item for score, item in sorted(scored_pile)]
return shuffled_pile
要得到上面例子的播放列表,我只需要调用:
riffle_shuffle([['ABCe1.mp4', 'ABCe2.mp4'], ['XYZe1.mp4', 'XYZe2.mp4', 'XYZe3.mp4', 'XYZe4.mp4']])
这个方法大多数时候都能很好地工作。不过,有时候结果并不理想——播放列表中有两个相邻的条目是来自同一个节目的剧集。例如:
>>> riffle_shuffle([['ABCe1', 'ABCe2'], ['LMNe1', 'LMNe2', 'LMNe3'], ['XYZe1', 'XYZe2', 'XYZe3', 'XYZe4', 'XYZe5']])
['XYZe1', 'LMNe1', 'ABCe1', 'XYZe2', 'XYZe3', 'LMNe2', 'XYZe4', 'ABCe2', 'LMNe3', 'XYZe5']
注意到有两个'XYZ'的剧集是并排出现的。这种情况可以很简单地解决(手动把'ABCe1'和'XYZe2'交换一下)。
我很好奇是否有更好的方法来交替播放,或者对多个长度不同的列表进行洗牌。我想知道你们有没有更简单、更高效,或者更优雅的解决方案。
belisarius 提出的解决方案(谢谢!):
import itertools
def riffle_shuffle_belisarius(piles_list):
def grouper(n, iterable, fillvalue=None):
args = [iter(iterable)] * n
return itertools.izip_longest(fillvalue=fillvalue, *args)
if not piles_list:
return []
piles_list.sort(key=len, reverse=True)
width = len(piles_list[0])
pile_iters_list = [iter(pile) for pile in piles_list]
pile_sizes_list = [[pile_position] * len(pile) for pile_position, pile in enumerate(piles_list)]
grouped_rows = grouper(width, itertools.chain.from_iterable(pile_sizes_list))
grouped_columns = itertools.izip_longest(*grouped_rows)
shuffled_pile = [pile_iters_list[position].next() for position in itertools.chain.from_iterable(grouped_columns) if position is not None]
return shuffled_pile
示例运行:
>>> riffle_shuffle_belisarius([['ABCe1', 'ABCe2'], ['LMNe1', 'LMNe2', 'LMNe3'], ['XYZe1', 'XYZe2', 'XYZe3', 'XYZe4', 'XYZe5']])
['XYZe1', 'LMNe1', 'XYZe2', 'LMNe2', 'XYZe3', 'LMNe3', 'XYZe4', 'ABCe1', 'XYZe5', 'ABCe2']
4 个回答
这样做可以确保每次都能真正随机打乱,也就是说每次的结果都不一样,并且尽量避免相邻的项目。
你问的那个可能会根据你的请求返回几个(1或2个)结果。
from random import choice, randint
from operator import add
def randpop(playlists):
pl = choice(playlists)
el = pl.pop(randint(0, len(pl) -1))
return pl, el
def shuffle(playlists):
curr_pl = None
while any(playlists):
try:
curr_pl, el = randpop([pl for pl in playlists if pl and pl != curr_pl])
except IndexError:
break
else:
yield el
for el in reduce(add, playlists):
yield el
raise StopIteration
if __name__ == "__main__":
sample = [
'A1 A2 A3 A4'.split(),
'B1 B2 B3 B4 B5'.split(),
'X1 X2 X3 X4 X5 X6'.split()
]
for el in shuffle(sample):
print(el)
编辑:
如果剧集的顺序是必须的,那就简单化一下 randpop
函数:
def randpop(playlists):
pl = choice(playlists)
el = pl.pop(0)
return pl, el
我尝试的代码:
program, play = [['ABCe1.mp4', 'ABCe2.mp4'],
['XYZe1.mp4', 'XYZe2.mp4', 'XYZe3.mp4', 'XYZe4.mp4',
'XYZe5.mp4', 'XYZe6.mp4', 'XYZe7.mp4'],
['OTHERe1.mp4', 'OTHERe2.mp4']], []
start_part = 3
while any(program):
m = max(program, key = len)
if (len(play) >1 and
play[-1][:start_part] != m[0][:start_part] and
play[-2].startswith(play[-1][:start_part])):
play.insert(-1, m.pop(0))
else:
play.append(m.pop(0))
print play
一个确定性的解决方案(也就是说不是随机的)
先把你的节目按照集数从多到少排序。
选择集数最多的那个节目,然后根据它的集数来安排一个矩阵,矩阵的列数就是这个节目的集数,填充的方式如下:
A A A A A A <- First show consist of 6 episodes
B B B B C C <- Second and third show - 4 episodes each
C C D D <- Third show 2 episodes
然后按列收集数据
{A,B,C}, {A,B,C}, {A,B,D}, {A,B,D}, {A,C}, {A,C}
接着把它们连接起来
{A,B,C,A,B,C,A,B,D,A,B,D,A,C,A,C}
现在给每个节目分配一个顺序号
{A1, B1, C1, A2, B2, C2, A3, B3, D1, A4, B4, D2, A5, C3, A6, C4}
编辑
你的情况
[['A'] * 2, ['L'] * 3, ['X'] * 5])
X X X X X
L L L A A
-> {X1, L1, X2, L2, X3, L3, X4, A1, X5, A2}
编辑 2
因为这里没有Python代码,或许可以用Mathematica的代码来帮忙:
l = {, , ,}; (* Prepare input *)
l[[1]] = {a, a, a, a, a, a};
l[[2]] = {b, b, b, b};
l[[3]] = {c, c, c, c};
l[[4]] = {d, d};
le = Length@First@l;
k = DeleteCases[ (*Make the matrix*)
Flatten@Transpose@Partition[Flatten[l], le, le, 1, {Null}], Null];
Table[r[i] = 1, {i, k}]; (*init counters*)
ReplaceAll[#, x_ :> x[r[x]++]] & /@ k (*assign numbers*)
->{a[1], b[1], c[1], a[2], b[2], c[2], a[3], b[3], d[1], a[4], b[4],
d[2], a[5], c[3], a[6], c[4]}