Permalänk
Datavetare

Progblemet #3

Ibland kan det finnas anledning att skapa alla möjliga permutationer av element som finns i en lista.

Listan [0,1,2] har följande möjliga permutationer [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,0,1],[2,1,0]]

Som synes kommer en listan med N element ha N! permutationer.

För att jämföra effektiviteten av olika lösningar så kan vi mäta tiden det tar att generera alla möjliga lösningar. För att språk med s.k. lazy-evaluation inte ska optimera bort hela beräkningen måste man använda resultatet.

Så en körning kan se ut något likt detta (från min Ruby lösning nedan), den lista som är helt utskriven är [0,1,2] och den som bara längden är utskriven på är [0,1,2,3,4,5,6,7,8,9]

[[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] 3628800 Elapsed time: 0m32.751s

Språk som stödjer s.k. list comprehension kan få väldigt korta och eleganta lösningar, men kanske inte speciellt effektiva i vissa lägen. Ett sådan exempel är Erlang där man kan lösa detta exempel med väldigt lite kod

% The permutations of an empty list is a list with a single empty list perms([]) -> [[]]; % Permutations of a non-empty list is created by concatenate each % element in the list with every permutation of the rest of the list % with the element used as head removed. perms(L) -> [[Head|Tail] || Head <- L, Tail <- perms(L -- [Head])]. io:fwrite("~w~n", [perms([0,1,2])]). io:fwrite("~w~n", [length(perms(lists:seq(0,9)))]).

Tiden för att köra beräkningen på en Core i7 2600 körandes Erlang R14B04 på 64-bitars Ubuntu 12.04 är 1.69s

Dold text

Den andra lösningen är i Ruby som inte stödjer list comprehension men ändå har bra stöd för att jobba med listor via map, select (filter).

def perms(l) if l.empty? [[]] else l.map{|head| perms(l.select{|e| head!=e}).map{|e|[head]+e}}.flatten(1) end end puts perms((0..2).to_a).to_s puts perms((0..9).to_a).length

Det blir lite mer komplicerat här då varje map skapar en separat lista så resultatet blir
[ [[0,1,2],[0,2,1]], [[1,0,2], [1,2,0]], ....]
I.e. det yttre map genererar en separat lista där element 0 är första element, en separat lista där element 1 är första element etc. Resultatet blir att det är en nivå för djup nästlade listor här, vilket löses med anropet till flatten(1) som plattar ut listan i exakt en nivå.

Farten är inte alls lika bra som i Erlang, att köra detta på samma maskin som ovan tar 32.8s körandes Ruby 1.9

Dold text

Har inte hunnit testa detta i språk som C eller C++ och om tiden det tar generera alla permutationer är <1s så lägg till resultatet för det antal element som gör att beräkningen överstiger i alla fall 1s.

Edit: litet förtydligande. En lista/array/vektor eller liknande med ALLA permutationer ska alltså finnas i RAM när perms (eller vad ni kallar den) funktionen retunerar. Så det räcker inte enbart med att köra std::next_permutation i C++ och bara räkna antal varv, varje permutation måste även sparas för att göra det till en så äpplen mot äpplen jämförelse som möjligt mellan språken.

Och den observante har nog redan noterat att mina lösningar var typiska divide-and-conquer så för den som vill vara lite mer avancerad så finns det potential till massor med parallellism för att accelerera detta över flera CPU-kärnor.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Jag fattar inte riktigt vad man ska göra. Tror det här är för svårt för sweclockers nivån.

Visa signatur

Programmerare -> PHP | HTML | CSS | JS | Java.

Permalänk
Medlem
Skrivet av Yoshman:

[progblembeskrivning]

Scala:

def perms = (0 to 9 permutations).toList

Dold text

Man får lätt slut på heap space!

Och detta svar är verkligen i tråkigaste laget, jag kanske reviderar denna post lite senare om jag får tid

Dold text
Visa signatur

Kom-pa-TI-bilitet

Permalänk
Datavetare

Ok, ska försöka förklara problemet på svenska

Målet med detta problem är att skriva ett program som räknar ut alla olika sätt du kan ordna N st unika objekt.

Om vi har bokstäverna A,B,C på hur många många sätt kan jag ordna dessa?

Svaret är:

  1. A B C

  2. A C B

  3. B A C

  4. B C A

  5. C A B

  6. C B A

d.v.s. 6 olika sätt. Men detta kanske var lite svårare än vad jag hade tänkt mig, trots att man kan skriva ganska kompakta lösningar i många språk.

Kan i alla fall lägga till en Python version

# coding: utf-8 # Ger en lista av alla permutationer av listan L def perms(L): if len(L) == 0: # En tom lista ger endast ett resultat: en tom lista... return [[]] # ...annars, skapa en lista där varje element i L är i första # positionen och addera varje permutation av resterande element # till detta element. Det rekursiva anropet till 'perms' sker med # en lista som har alla element utom Head. return [[Head] + Tail for Head in L for Tail in perms(list(set(L) - set([Head])))] print perms(range(3)) print len(perms(range(10)))

Python-versionen är lite snabbare än Ruby, beräkningen blir klar på 23.1s på en Core i7 2600 körandes Ubuntu 12.04 för x86_64.

Dold text
Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare

C++11

Och här är en version i C++11. Denna var ganska väntat den snabbaste så här långt, men ett problem här är att det inte längre finns något enkelt sätt att förändra problemet så det effektivt kan köra på många CPU-kärnor.

Ska försöka få tid till att sätta ihop en version i plain old C som utan problem kan använda alla 8 CPU-trådar på en Core i7. Är lite stressigt denna vecka, har deadlines att fixa inför nästa vecka

#include <algorithm> #include <iostream> #include <vector> using namespace std; // Detta är bara så att man enkelt kan skicka en lista till 'cout' template <typename T> ostream& operator<<(ostream& os, const vector<T>& v) { os << "[ "; for (auto &e: v) os << e << ' '; os << ']'; return os; } // Att retunera stora strukturer "by-value" är alltid billigt i C++11, så det blir INTE // en kopiering av den gigantiska vector som kommer tillbaka som svar när man // skickar in 'v2' vector<vector<int> > perms(vector<int> &v) { vector<vector<int> > res; sort(begin(v), end(v)); do { res.push_back(v); } while (next_permutation(begin(v), end(v))); return res; } int main(int argc, char *argv[]) { vector<int> v1(3); // iota är det närmaste man kommer "range" i python och (N to M) i // språk som Scala och Ruby första elementet blir 0 (3:e argumentet) // och varje nästa element blir (föregående element + 1) iota(begin(v1), end(v1), 0); vector<int> v2(10); iota(begin(v2), end(v2), 0); cout << perms(v1) << endl; cout << perms(v2).size() << endl; return 0; }

Denna variant skriver ut

[ [ 0 1 2 ] [ 0 2 1 ] [ 1 0 2 ] [ 1 2 0 ] [ 2 0 1 ] [ 2 1 0 ] ] 3628800

och tog bara 0.27s att köra

Tar bara 3.4s att köra med v2 = {0,1,2,3,4,5,6,7,8,9,10} vilket ger

[ [ 0 1 2 ] [ 0 2 1 ] [ 1 0 2 ] [ 1 2 0 ] [ 2 0 1 ] [ 2 1 0 ] ] 39916800

Dold text

Edit:
Det man ska komma ihåg här är att detta problem alltså har en tids och storlekskomplexitet som är proportionellt mot fakulteten av listan man stoppar in. Så en lista med 11 element (0..10) har alltså 11!=39916800 unika kombinationer. Att spara denna lista tar som absolut minimum 40M*4(4 bytes för en int)*11=~1.8GB RAM... Så man inser snabbt att RAM kommer ta slut om man skickar inte lite större listor

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

JavaScript:

/* * set * input-arrayen * * .reduce( function(a, b) { return a.concat(b); }) * används i brist av flatMap. Notera att denna funktion inte finns på arrayer * i alla webbläsare. Exemplet är testat och fungerar i Firefox 16. * * */ function perms(set) { if(set.length == 0) { return [[]]; } else { return set.map(function(a) { return perms(set.filter(function (n) { return n != a; })).map(function(b) { return [a].concat(b); }); }).reduce(function(a, b) { return a.concat(b); }); } }

Dold text

På min i5 950 / Ubuntu 12.04 x64 tog det 102.541 sekunder att köra funktionen på rangen 0 till 9. Firefox gnällde ganska tidigt om att någonting tog väldigt lång tid och kastade upp en debug-ruta, vilket förmodligen lade på några extra sekunder.

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem
Skrivet av Yoshman:

Och här är en version i C++11. Denna var ganska väntat den snabbaste så här långt, men ett problem här är att det inte längre finns något enkelt sätt att förändra problemet så det effektivt kan köra på många CPU-kärnor.

Jag blev riktigt sugen att sätta igång, men tiden har inte räckt till. Har grandiosa planer på att använda mig av factorial number system. På så sätt bör väl varje entry i (N! x N) matrisen av lösningar kunna räknas ut enbart med hjälp av koordinaterna för entryt. Då är problemet pinsamt parallellt och man kan slänga ihop en CUDA kernel som fixar biffen på nolltid!

Permalänk
Datavetare
Skrivet av Mikael07:

Jag blev riktigt sugen att sätta igång, men tiden har inte räckt till. Har grandiosa planer på att använda mig av factorial number system. På så sätt bör väl varje entry i (N! x N) matrisen av lösningar kunna räknas ut enbart med hjälp av koordinaterna för entryt. Då är problemet pinsamt parallellt och man kan slänga ihop en CUDA kernel som fixar biffen på nolltid!

Vet hur det är, har haft väldigt tight med tid överhuvudtaget den senaste veckan. Har börjat på en C version som är tillräckligt parallell för att utan problem använda alla de 8 trådar en Core i7:a har.

Kan i alla fall posta en Clojure version då detta språk, som vanligt, gör det väldigt enkelt att lösa problemet.

(defn perms[List] (if (empty? List) '(()) (for [Head List Tail (perms (filter (partial not= Head) List))] (cons Head Tail)))) (println (perms (range 3))) (println (count (perms (range 10))))

Denna tar 15.2s på en Corei7 2600 körandes 64-bitars Ubuntu 12.04 (Java7+Clojure1.3).

Dold text

Börjar bli dax för ett nytt problem och ska tänka ut något relativt enkelt denna gång då det ser ut att vara tjockt även denna vecka.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Edit: Tja efter ännu mer missförstånd har jag äntligen fattat hela uppgiften

Här är en lösning i C#, Total tid tar ~6,22s

static int[] one = Enumerable.Range(0, 3).ToArray(); static int[] two = Enumerable.Range(0, 10).ToArray(); public static void Perm2() { foreach (var row in Permutations(one)) Console.WriteLine(string.Join(", ", row)); Console.WriteLine(Permutations(two).Count().ToString()); } public static IEnumerable<IEnumerable<int>> Permutations(IEnumerable<int> list) { if (2 > list.Count()) return new [] {list}; return list.SelectMany(ch => Permutations(list.Where(x => x != ch)), (ch, perm) => new int[] { ch }.Concat(perm)); }

Dold text
Visa signatur

Speldator: Ryzen 7800X3D, 64GB DDR5, RTX 3070
Server: i7-8700k, 32GB DDR4, RTX2080
Steam deck + de fiesta konsoller.

Permalänk
Datavetare

Go

Har ju skrivit tidigare att det går att använda flera CPU-trådar för att lösa detta problem. Tänkte först göra en sådan lösning i C, men tänkte om då jag faktiskt sitter med C hela dagarna på jobbet och en anledning till att jag överhuvudtaget tycker det är intressant att lägga tid på sådana här saker är att det ger en möjlighet att lösa uppgifter på sätt som man normalt sett kanske inte gör.

Så det blir ingen lösning av detta i C, har nog av den varan på jobbet. Valde i stället att göra en lösning i Go som kan använda flera CPU-trådar

Är definitivt ingen Go-expert, men Go verkar vara ett språk som är riktigt genomtänkt. Go är designat efter att vara enkelt, man har inte funderat på vilka features man kan lägga till (vilket man gjort rätt mycket i språk som C++ och C#, något som resulterar i ENORMA språk) utan hur mycket man kan ta bort utan att för den delen göra den svårt att uttrycka sig i språket.

Go är nästan lika effektivt som C/C++ och likt Erlang så är det väldigt enkelt att skriva program som hanterar massiva mängder I/O i Go.

Antar att Go är ganska nytt för de flesta så har stoppat in lite kommenterar. En kanal "channel" i Go är en stark typad kommunikationskanal som kan användas för att kommuncera på ett säkert och korrekt sätt mellan s.k. go-routines. En go-routine mappas INTE mot en OS-tråd, utan Go har en egen schemaläggare som skapar "ett lämpligt" antal OS-trådar och lägger sedan ut de go-routiner man har på dessa. En "go-routine" kostar bara några 100-bytes i overhead, så det är möjligt att ha många tusen av dessa. En OS-tråd tar typiskt ett par MB, så systemet mår inte bra om man har 1000-tals av dessa.

Om jag har en kanal med namnet 'ch' av typen int så kan man skriva till kanalen på detta sätt

ch<- 123
var x int = 1234
ch<- x

och man kan läsa ur den på detta sätt

var y int
y <-ch
fmt.Println("Nästa värde i kanalen är", <-ch)

range i Go gör så man kan traversera arrayer. För varje element får man ett par där det första elementet är index i arrayen och det andra elementet är värdet. Är man inte intresserade av något av elementen talar man om det genom att tilldela det elementet till '_' . Go tillåter inte att man inför variabler man inte använder!

package main import "fmt" func fac(n int) int { if n <= 1 { return 1 } return n * fac(n-1) } // Returns a new list where the element at index 'idx' is removed func disassoc(List []int, idx int) []int { nl := make([]int, len(List) - 1) copy(nl, List[:idx]) copy(nl[idx:], List[idx+1:]) return nl } // Single threaded version of "perms" func perms_worker(List []int) [][]int { if (len(List) <= 1) { return [][]int{List} } ret := [][]int{} for hidx, Head := range(List) { for _, Rest := range(perms_worker(disassoc(List, hidx))) { ret = append(ret, append(Rest, Head)) } } return ret } // Creates len(List) independent go-routines that calculates all // permutations for the subset where one elements value and position // is fixed func perms(List []int) [][]int { // The answers from the go-routins will be recevied through this // channel perm_ch := make(chan [][]int) // Start the go-routines for i, _ := range(List) { go func(idx int) { perm_ch<- perms_worker(disassoc(List, idx)) }(i) } // Total number of answers will be the faculty of the list length. acc := make([][]int, fac(len(List))) // Each sub-problem will generate fac(len(List) - 1 permutations, // so use that as the stride when copy the solutions into the // result array. stride := fac(len(List) - 1) for i, Head := range(List) { slice := acc[i*stride:(i+1)*stride] copy(slice, <- perm_ch) for j, Rest := range(slice) { slice[j] = append(Rest, Head) } } return acc } func main() { fmt.Println(perms([]int{0,1,2}) ) fmt.Println(len(perms([]int{0,1,2,3,4,5,6,7,8,9}))) }

Tiden är lista på C/T, där C=fysiska kärnor och T=trådar per kärna (HT). Så 1/2 betyder en kärna där båda HT används, 2/1 är två kärnor och en tråd på varje kärna 4/2 är 4 fysiska kärnor och 2 HT på varje (totalt 8 trådar).

Tid på
1/1: 5.1s
1/2: 4.1s
2/1: 3.1s
2/2: 2.6s
4/1: 2.1s
4/2: 1.9s

Dold text

Att det inte alls skalar linjärt beror dels på att go-routines är inte optimerade för CPU-bundna laster utan optimerade för I/O. Men kanske än viktigare är att det kopieras rejält med data över kanalen perm_ch och det blir nog en flaskhals i L3 cache och på RAM-bussen, detta är också anledning varför denna version inte alls är lika snabb som den i C++.

Går totalt sett x2.7 gånger snabbare när alla CPU-trådarna används jämfört med att bara använda en CPU-tråd.

Edit: och bara för referens mot de andra implementationerna så är här en enkeltrådad variant i Go

package main import "fmt" func disassoc(List []int, idx int) []int { nl := make([]int, len(List) - 1) copy(nl, List[:idx]) copy(nl[idx:], List[idx+1:]) return nl } func perms(List []int) [][]int { if (len(List) <= 1) { return [][]int{List} } ret := [][]int{} for hidx, Head := range(List) { for _, Rest := range(perms(disassoc(List, hidx))) { ret = append(ret, append(Rest, Head)) } } return ret } func main() { fmt.Println(perms([]int{0,1,2}) ) fmt.Println(len(perms([]int{0,1,2,3,4,5,6,7,8,9}))) }

Körning tar 4.5s vilket visar att det är en del overhead i att bara göra det möjligt att köra på flera CPU-kärnor, men det är ganska vanligt

Dold text
Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Haskell

Här kommer en liten implementation i Haskell, tipsa gärna om hur jag kan förbättra den:

import Data.List perms :: Eq a => [a] -> [[a]] perms [] = [[]] perms xs = [ x : rest | x <- xs, rest <- perms (xs \\ [x]) ] main :: IO () main = print (perms [1..9])

Permalänk
Datavetare
Skrivet av htux:

Här kommer en liten implementation i Haskell, tipsa gärna om hur jag kan förbättra den:

perms :: Eq a => [a] -> [[a]] perms [x] = [[x]] perms xs = [ x : rest | x <- xs, rest <- perms (xs \\ [x]) ] main :: IO () main = print (perms [1..9])

Fungerade din version i Haskell? Vilken kompilator körde du med då? Tänkte bl.a. på att '\\' tillhör modulen "Data.Set" som inte är importerad.
Med följande förändringar lyckades jag köra det hela i alla fall

perms :: Eq a => [a] -> [[a]] perms [] = [[]] perms xs = [ x : rest | x <- xs, rest <- perms (filter (\e -> x /= e) xs) ] main :: IO () main = do print (perms [0..2]) print (length (perms [0..9]))

Det tog 1.0s med GHC 7.4.1 på en Core i7 2600 under Ubuntu 12.04

Dold text
Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Fungerade din version i Haskell? Vilken kompilator körde du med då? Tänkte bl.a. på att '\\' tillhör modulen "Data.Set" som inte är importerad.
Med följande förändringar lyckades jag köra det hela i alla fall

perms :: Eq a => [a] -> [[a]] perms [] = [[]] perms xs = [ x : rest | x <- xs, rest <- perms (filter (\e -> x /= e) xs) ] main :: IO () main = do print (perms [0..2]) print (length (perms [0..9]))

Det tog 1.0s med GHC 7.4.1 på en Core i7 2600 under Ubuntu 12.04

Dold text

På något sätt råkade import Data.List försvinna, men nu funkar det. Kompilerat med GHC 7.6.1.

Permalänk
Medlem

Gjorde en lösning i (T-)SQL:

declare @max int = 10 ;with b as ( select cast(v0.number as varchar(max)) as n from spt_values v0 where v0.type='P' and v0.number < @max ), r as ( select n from b union all select r.n +b.n as n from b join r on charindex (b.n, r.n) = 0 ) select COUNT(*) from r where len(r.n) = @max

Dold text

Tyvärr blev det alldeles för slött att göra såhär (52 sek för 0-8, avbröt den efter 5 minuter på 0-9).. gissar att det är bl.a. charindex-grejen som suger, går nog o lösa på ett bättre sätt om man klurar lite (eller gör lösningen på ngt annat vis än med CTE)

Dessutom råkade jag läsa fel i beskrivningen första gången så att jag istället svängde ihop en sql som tog fram alla möjliga kombinationer (000,001,002,010,011..) Den lyckades däremot räkna ut antalet för 0-9 hyfsat snabbt. Vore ju synd o inte visa upp den när den ändå funkar:

declare @sql varchar(max) declare @max int =10 select @sql = 'select count(*)' + ' from ' + stuff( (SELECT N' cross join ' + 'spt_values v' + ltrim(number) FROM spt_values where type = 'P' and number < @max FOR XML PATH('')) ,1,12,N'') + ' where ' + stuff( (SELECT N' and ' + 'v' + ltrim(number)+'.type=''P'' and v' + + ltrim(number)+'.number < ' + LTRIM(@max) FROM spt_values where type = 'P' and number < @max FOR XML PATH('')) ,1,4,N'') select @sql = REPLACE(@sql,'<','<') exec (@sql)