몇 가지 추가 제약 조건을 적용하여 파일을 무작위로 섞습니다.

몇 가지 추가 제약 조건을 적용하여 파일을 무작위로 섞습니다.

나는 거대한 음악 재생 목록을 가지고 있는데, 일부 아티스트는 많은 앨범을 가지고 있는 반면 다른 아티스트는 노래가 하나만 있습니다. 같은 아티스트가 연속으로 두 번 재생되지 않거나 그의 노래가 대부분 재생 목록의 시작이나 끝 부분에 나오지 않도록 재생 목록을 정렬하고 싶습니다.

재생목록 예:

$ cat /tmp/playlist.m3u
Anna A. - Song 1
Anna A. - Song 2
I--Rock - Song 1
John B. - Song 1
John B. - Song 2
John B. - Song 3
John B. - Song 4
John B. - Song 5
Kyle C. - Song 1
U--Rock - Song 1

sort -R또는 다음의 출력 shuf:

$ sort -R /tmp/playlist.m3u
Anna A. - Song 1 #
U--Rock - Song 1
Anna A. - Song 2 # Anna's songs are all in the beginning.
John B. - Song 2
I--Rock - Song 1
John B. - Song 1
Kyle C. - Song 1
John B. - Song 4 #
John B. - Song 3 #
John B. - Song 5 # Three of John's songs in a row.

나는 무엇을 기대합니까?

$ some_command /tmp/playlist.m3u
John B. - Song 1
Anna A. - Song 1
John B. - Song 2
I--Rock - Song 1
John B. - Song 3
Kyle C. - Song 1
Anna A. - Song 2
John B. - Song 4
U--Rock - Song 1
John B. - Song 5

답변1

귀하의 예제 데이터와 제약 조건은 실제로 몇 가지 솔루션만을 허용합니다. 예를 들어, John B.를 노래마다 재생해야 합니다. 나는 당신의 실제 전체 재생 목록이 본질적으로 그렇지 않다고 가정합니다.John B, 무작위로 다른 것들과 헤어져.

이것은 또 다른 무작위 방법입니다. @frostschutz의 솔루션과 달리 빠르게 작동합니다. 그러나 결과가 귀하의 기준에 부합한다는 것을 보장하지는 않습니다. 또한 샘플 데이터에서는 작동하지만 실제 데이터에서는 좋지 않은 결과가 나올 것으로 생각되는 두 번째 접근 방식도 생각해 냈습니다. 귀하의 실제 데이터(난독화)를 사용하여 방법 3을 추가했습니다. 이는 동일한 아티스트의 두 곡을 연속해서 포함하지 않는다는 점을 제외하고 균일한 무작위 방법입니다. 남은 노래의 "데크"에 5번만 "끌어오며", 이후에도 중복 아티스트가 나타나면 어쨌든 노래를 출력합니다. 이렇게 하면 프로그램이 실제로 완료되는 것이 보장됩니다.

방법 1

기본적으로 각 지점에서 재생 목록을 생성하고 "아티스트의 재생되지 않은 다른 노래가 무엇입니까?"라고 질문한 다음 무작위로 아티스트를 선택하고 마지막으로 해당 아티스트의 노래를 무작위로 선택합니다. (즉, 각 아티스트에게 노래 수에 비례하는 것이 아니라 균등하게 가중치를 부여합니다.)

실제 재생 목록에서 시도해 보고 균일한 무작위보다 더 나은 결과가 나오는지 확인하세요.

용법:./script-file < input.m3u > output.m3uchmod +x물론 이렇게 하십시오. 일부 M3U 파일의 상단에 있는 서명란을 제대로 처리하지 못한다는 점에 유의하십시오. 하지만 귀하의 예에는 해당 사항이 없습니다.

#!/usr/bin/perl
use warnings qw(all);
use strict;

use List::Util qw(shuffle);

# split the input playlist by artist
my %by_artist;
while (defined(my $line = <>)) {
    my $artist = ($line =~ /^(.+?) - /)
        ? $1
        : 'UNKNOWN';
    push @{$by_artist{$artist}}, $line;
}

# sort each artist's songs randomly
foreach my $l (values %by_artist) {
    @$l = shuffle @$l;
}

# pick a random artist, spit out their "last" (remeber: in random order)
# song, remove from the list. If empty, remove artist. Repeat until no
# artists left.
while (%by_artist) {
    my @a_avail = keys %by_artist;
    my $a = $a_avail[int rand @a_avail];
    my $songs = $by_artist{$a};
    print pop @$songs;
    @$songs or delete $by_artist{$a};
}

방법 2

두 번째 접근 방식으로무작위로 아티스트를 선택하세요, 당신은 그것을 사용할 수 있습니다가장 많은 노래를 가진 아티스트를 선택하세요. 그러면 그 사람이 우리가 선택한 마지막 아티스트가 아닙니다.. 프로그램의 마지막 단락은 다음과 같습니다.

# pick the artist with the most songs who isn't the last artist, spit
# out their "last" (remeber: in random order) song, remove from the
# list. If empty, remove artist. Repeat until no artists left.
my $last_a;
while (%by_artist) {
    my %counts = map { $_, scalar(@{$by_artist{$_}}) } keys %by_artist;
    my @sorted = sort { $counts{$b} <=> $counts{$a} } shuffle keys %by_artist;
    my $a = (1 == @sorted)
        ? $sorted[0]
        : (defined $last_a && $last_a eq $sorted[0])
            ? $sorted[1]
            : $sorted[0];
    $last_a = $a;
    my $songs = $by_artist{$a};
    print pop @$songs;
    @$songs or delete $by_artist{$a};
}

프로그램의 나머지 부분은 변경되지 않습니다. 이것이 가장 효율적인 방법은 아니지만 적당한 크기의 재생 목록에 대해서는 충분히 빠릅니다. 예제 데이터를 사용하면 생성된 모든 재생 목록은 John B. 노래, Anna A. 노래, John B. 노래로 시작됩니다. 그 이후에는 예측하기가 훨씬 어려웠습니다(John B.를 제외한 모든 사람이 한 곡만 남았기 때문입니다). 이는 Perl 5.7 이상을 가정합니다.

방법 3

사용법은 앞의 2와 동일합니다. 이 부분에 주의하세요 0..4. 최대 시도 횟수는 5회입니다. 시도 횟수를 0..9총 10회 등으로 늘릴 수 있습니다. ( 0..4= 0, 1, 2, 3, 4, 실제로는 5개의 항목이라는 것을 알 수 있습니다).

#!/usr/bin/perl
use warnings qw(all);
use strict;

# read in playlist
my @songs = <>;

# Pick one randomly. Check if its the same artist as the previous song.
# If it is, try another random one. Try again 4 times (5 total). If its
# still the same, accept it anyway.
my $last_artist;
while (@songs) {
    my ($song_idx, $artist);
    for (0..4) {
        $song_idx = int rand @songs;
        $songs[$song_idx] =~ /^(.+?) - /;
        $artist = $1;
        last unless defined $last_artist;
        last unless defined $artist; # assume unknown are all different
        last if $last_artist ne $artist;
    }

    $last_artist = $artist;
    print splice(@songs, $song_idx, 1);
}

답변2

이 셔플링을 카드 덱에 적용해야 한다면 덱을 먼저 섞은 다음 앞에 카드를 정렬하고 인접한 클럽이나 하트가 있는지에 관계없이 왼쪽에서 오른쪽으로 처리할 것 같습니다. . . 그중 하나를 제외한 모든 항목을 다른 위치로 무작위로 이동합니다(동일한 유형의 다른 위치에 인접하지는 않음).

예를 들어 한 손으로

답변3

별로 비효율적이라고 생각하지 않는다면...

while [ 1 ]
do
    R="`shuf playlist`"
    D="`echo "$R" | sed -e 's/ - .*//' | uniq -c -d`"
    if [ "$D" == "" ]
    then
        break
    #else # DEBUG ONLY:
    #    echo --- FAIL: ---
    #    echo "$D"
    #    echo -------------
    fi
done

echo "$R"

연속적으로 두 개 이상의 John이 없는 결과를 얻을 때까지 계속 스크롤합니다. 재생 목록에 존이 너무 많아서 그러한 조합이 존재하지 않거나 롤링할 가능성이 거의 없다면 정지됩니다.

입력 결과의 예:

John B. - Song 4
Kyle C. - Song 1
Anna A. - Song 2
John B. - Song 3
Anna A. - Song 1
John B. - Song 1
U--Rock - Song 1
John B. - Song 2
I--Rock - Song 1
John B. - Song 5

디버그 줄의 주석 처리를 제거하면 실패한 이유를 알 수 있습니다.

--- FAIL: ---
      3 John B.
-------------
--- FAIL: ---
      2 John B.
      2 John B.
-------------

무한정 정지되는 경우 원인을 파악하는 데 도움이 됩니다.

답변4

또 다른 방법은 Bash를 사용하는 것입니다. 재생 목록을 무작위 순서로 읽고 행이 중복된 경우 목록의 반대쪽 끝에 삽입하려고 시도하며 단일 중복 항목을 따로 보관하여 다른 위치에 다시 삽입합니다. 세 개의 중복 항목(첫 번째, 마지막, 예약된 항목이 동일함)이 있으면 실패하며 해당 오류 항목이 목록 끝에 추가됩니다. 대부분의 시간에 업로드하는 광범위한 목록을 처리할 수 있는 것 같습니다.

#!/bin/bash

first_artist=''
last_artist=''
bad_artist=''
bad_line=''
result=''
bad_result=''

while read line
do
    artist=${line/ - */}
    line="$line"$'\n'

    if [ "$artist" != "$first_artist" ]
    then
        result="$line""$result"
        first_artist="$artist"

        # special case: first = last
        if [ "$last_artist" == '' ]
        then
            last_artist="$artist"
        fi

        # try reinserting bad
        if [ "$bad_artist" != '' -a "$bad_artist" != "$first_artist" ]
        then
            first_artist="$bad_artist"
            result="$bad_line""$result"
            bad_artist=''
            bad_line=''
        fi
    elif [ "$artist" != "$last_artist" ]
    then
        result="$result""$line"
        last_artist="$artist"

        # try reinserting bad
        if [ "$bad_artist" != '' -a "$bad_artist" != "$last_artist" ]
        then
            last_artist="$bad_artist"
            result="$result""$bad_line"
            bad_artist=''
            bad_line=''
        fi
    else
        if [ "$bad_artist" == '' ]
        then
            bad_artist="$artist"
            bad_line="$line"
        else
            # first, last and bad are the same artist :(
            bad_result="$bad_result""$line"
        fi
    fi
done < <(shuf playlist)

# leftovers?
if [ "$bad_artist" != '' ]
then
    bad_result="$bad_result""$bad_line"
fi

echo -n "$result"
echo -n "$bad_result"

더 똑똑할 수도 있습니다... John의 예에서 John은 항상 첫 번째 아티스트를 먼저 추가하려고 시도하므로 일반적으로 마지막 아티스트가 되어야 한다고 주장합니다. 따라서 중간에 두 명의 다른 아티스트가 있는 경우 트리플 존을 피하기 위해 하나를 시작 부분에 추가하고 다른 하나를 끝에 추가하는 것은 충분히 똑똑하지 않습니다. 따라서 기본적으로 다른 모든 아티스트가 John이어야 하는 목록의 경우 예상보다 더 많은 실패가 발생하게 됩니다.

관련 정보