August 28, 2008Coding challenge wrap-up
I certainly didn't expect so many reactions to the coding challenge... More than 130 comments so far, wow. First of all, I owe an apology to all commenters for my annoying comment system (which prohibits posting URL's that start with "http"). I'm very sorry about that but I receive so much spam that it's a necessity. Some people braved the odds and posted their solution anyway, others used creative ways to submit their code, such as using Google Documents or Paste Bin (which has the benefit of syntax highlighting). Thank you all for putting up with this and participating in this fun contest anyway. I've learned a lot from all these solutions and discussions, which featured the following languages: Java, C, Perl, Erlang, Javascript, C#, Groovy, Haskell, AS3, C, Fan, LUA, J, OCaml, Factor, Forth, Lisp, Ursala, Prolog. Here is a quick wrap-up. The solutions basically fall into three categories:
Ruby
(98..103).select { |x| x.to_s.size == x.to_s.split(//).uniq.size }
Python
(i for i in xrange(start, end) if len(str(i)) == len(set(str(i)))):Scala for (i <- 1 to 100000; s = i.toString; if HashSet[Char](s:_*).size == s.size) println(i)J f =: [:(#;[:>./2-~/\])(#~([:*/[:~:":)"0)For a minute, I thought that last entry was a joke or that maybe, the poster was disconnected in the middle of posting his solution. But no, J is a real programming language. Ursala #import nat func = ^(nleq$^+ difference*typ,length)+ ~&triK2tkZ2FlS+ iota+successor; * ^lrhPX/~& %nPQuite an intidimidating syntax here too :-) The problem with the concise solutions is that they eliminate duplicate digits by converting the integer into a string, which results in prohibitive running times (the Ruby code takes 27 hours to complete with max = 10^10, which is the baseline I'll be using from now on). Let's turn our attention briefly to solutions that are neither concise nor fast... I was disappointed to see the Erlang code, to be honest, because the (only) solution that was posted is a bit frightening. I would love to see more attempts in Erlang that are either concise or fast. This problem seems to be a good candidate for sharding, since all the numbers that satisfy the requirements can be found in complete isolation of the others, so this a good opportunity for Erlang to show what it's good at. Also, somebody posted a Prolog solution which is shorter than Erlang's, so I don't think the declarative aspect of Erlang is the reason for the length of this solution. Can somebody post an Erlang solution that is either concise or fast? Similarly, Perl and Javascript didn't particularly shine in the contributions that I saw in the comments. People also posted solutions in Forth (a bit hard to read, but my Forth is rusty) and even Factor (which is reasonably concise but also seems to use a lot of libraries). Crazy Bob was the first one to post a solution that is reasonably concise and also scaringly fast. Not surprisingly, it's not brute force, it only uses primitive integers, it uses a bitmask to keep track of which digits have already been used and to top it all, it's recursive (it's not very often you see recursive code that is faster than everything else, although admittedly, the recursion doesn't go very deep). Bob's first attempt was able to calculate all numbers from 1 to 10^10 in half a second, which blew away everything that had been posted so far. Interestingly, the C version of his Java code ran at about the same speed as Java. Quite a few people observed that my problem was similar to generating permutations, with the little twist that '0' is not allowed to appear in first position. With that in mind, I thought I would see people grab the standard implementations for permutations that you can find on the web, adapt them to the constraints of my problem and then post them here. Interestingly, the opposite happened: Bob's solution is not only the fastest, but it can probably form the basis for a canonical solution to solve permutations quickly, especially since it's only limited by the number of characters that you can represent in a bitmask (64 if you want the bitmask to remain a primitive, unbounded if you represent it with a more complex structure). Then, Mauricio entered the fray with OCaml and his initial attempt took 640ms to run up to 10^10. That was quite a shocker to me. I studied quite a bit of OCaml during my PhD (which I did at INRIA, so this should come as no surprise), but Caml and OCaml quickly fell off my radar when I moved on. I was quite surprised to see a real functional language be as fast as the top contenders in a purely algorithmic contest. Mauricio's version is about as concise as Java, which makes this even more impressive. Some time later, Mauricio wrote a more functional version of his solution, which ended up running in about the same time as his imperative approach. And then, Bob came up with a solution that runs twice faster. It's not very often that I get excited by a piece of Java code, because I think that Java is boring (in a good way). Obviously, Bob's trick is not specific to Java, but I did go "wow" the first time I read it. Bob's initial algorithm is not very complicated but it does have a few code paths which, at first sight, would be good candidates for possible optimizations. What I found the most interesting in his approach is that he found the optimization in the most unexpected place: by getting rid of incrementations and by adding a class. Bob introduced a class Digit that is a doubly linked list of digits. Initially, 0 points to 1 which points to both 0 and 2, which points to... You get the idea. Calling next() on such an object gives you the next digit in the sequence without requiring an increment operation. The trick here is to rearrange the list as soon as you use a digit so that invoking next() will always return a digit that can be added to your number without violating the requirements. For example, as soon as you add 1 to your solution, the Digit list now becomes 0 <-> 2 <-> 3... There are a couple of additional tricks with regard to head tracking and backtracking, but that's pretty much the core of the optimization. Beautiful code indeed.
Concluding remarksMy first take away from this little exercise is that conciseness only goes so far. I have seen people post a one-liner solution on their blog in language X and conclude "X rocks". Except that their solution will take hours to complete. A credible language must make it possible for developers to solve problems with either conciseness or performance in mind, and ideally, allow a whole spectrum between these two extremes. The comments also discussed the definition of "brute force", and after some initial turmoil, everybody seems to agree that Bob's solution is not brute force since it only ever considers valid digits. At this point, I challenge anyone who disagrees to come up with a "really non brute force" solution which should, in theory, be much faster than Bob's solution. Good luck with that :-) Finally, I'd like to get a better sense of performances for all the languages that have participated in this little contest, so I offer the following follow-up problem: port Bob's solution to your language of choice, post the code along with 1) how fast Bob's Java solution runs on your system and 2) how fast your solution runs. So far, only Java, C/C++ and OCaml have shown to be up to the task. Can you add your own favorite language to the list? Comments
As one of the people decrying the profligate and inappropriate claim of 'not brute-force', let me state for the record that I wasn't including Bob's solution in my complaint, which was primarily directed at people that seemed to confuse typing time with runtime, and assumed that anything that was fast to type couldn't possibly be brute-force. I think I've reached the stage in my coding career where I'm officially sick of 'elegance'. Yes, it's cute that you can write an algorithm in 60 characters because your language is so high level that all you're doing is gluing your standard library to itself. No, you can't check it into subversion, because it's impossible to comment, debug, and test. Programmers tend to vastly overstate the importance of coding time, because that's the time that we spend. We often willfully ignore the fact that time spent writing code is dwarfed by time spent using it, and then reading it and debugging it, and then optimizing it. . by many orders of magnitude. So a syntax or coding style that shaves 50% off of your initial writing time, but adds 10% to your execution time, and 20% to your eventual test and debug time, 100% to the time it takes another developer to understand it (or 1000%, if it's perl) and 50% to any optimization time is enormously expensive over the lifetime of your software, and isn't even worth considering. Posted by: sidereal at July 10, 2008 11:30 AMA few corrections regarding my OCaml solutions: they are all functional, there Here's a summary of the performance race :) crazybob.org/BeustSequence.java.html 680ms crazybob.org/FastBeustSequence.java.html 269ms (doubly linked list)
eigenclass.org/misc/beust2.ml (expanded pattern matching) I'm convinced the problem is a constraint problem with a solution similar to Peter Norvig's stunningly fast Sudoku solver. But my brain hurts. Posted by: RichB at July 10, 2008 12:28 PMHey Cedric, could you let us know the story behind the challenge? regards, David Posted by: david at July 10, 2008 12:35 PMThere are a lot of intermediate complexities between brute force and closed form. Brute force in this case here would be O(n) [I don't see how you could do worse than O(n) on such a problem] while a closed form would be O(1). A solution that is not O(1) is not necessarily O(n)... Some people here are smoking some heavy crack. A serious discussion should involve big-O, not simply "mine is brute force but less brute force than yours". Ruby - version but it takes longer than 30 sec on my box. def findDigits( used , value , ndigit , start , listener ) 1.upto(10) do |x| C version - if I compile with -O3, it is faster than CrazyBob's Java version #include
int main(char* arg) for( n = 0; n < 10; n++ ) gettimeofday(&start, NULL); for( n = 1; n <= 10; n++ ) gettimeofday(&end, NULL); dT = ( end.tv_usec - start.tv_usec ); printf("time = %15.9lf miillseconds\n", dT); return 0; Here is a non-brute-force dynamic programming solution that counts integers that don't contain duplicate elements, in C#: class UniqueDigits public UniqueDigits(long limit) m_cache = new int[1 << 10, 10, 2, 2]; public int Compute() private int Compute(int taken, int pos, bool firstDigit, bool isLess) int ans = 0; int newTaken = taken; ans += Compute(newTaken, pos - 1, newFirstDigit, isLess || digit < m_limit[pos]); return m_cache[taken, pos, firstDigit ? 1 : 0, isLess ? 1 : 0] = ans; Console.WriteLine(new UniqueDigits(9999999999L).Compute()); This prints "8877690", which I believe is the correct answer. It runs in 4-7ms on my machine. I am pretty sure that there is an efficient solution to the second half of the problem - the biggest jump. It seems like a nasty case-enumeration problem. I'll think a bit about it. Posted by: Igor Ostrovsky at July 11, 2008 01:03 AMIt was easy enough to do something faster than Bob's first attempt, but I don't see any way to beat his final solution. (At least it wouldn't be anything measurable.) Have fun, Posted by: Paulo Gaspar at July 11, 2008 04:17 AMhere is my ugly attempt. It is very fast and does not seem to register a time using DataTime.Now as the metric. So sub millisecond or two. using System; namespace Challenge { Just a follow up to my previous recursive solution. I let it go to 9876543210, which is obviously the largest number possible. Here are the results. Hopefully I didn't miss something and it is correct. It looks like it. There where 5120 items and the biggest jump was from 8012345679->9012345678 it was a jump of 999999999 Darrell, there are a lot more than 5120 items if you let it go to that number, it looks like you are still counting up to 10000 and not 10^10. Another follow up, it looks like the braces are not supported on the forum the types labeled List are all actually generic List's of type long. Posted by: Darrell Wright at July 11, 2008 10:36 AM@Darrell > It took 0.00000000000000000000 milliseconds to complete My version beat that;) Unfortunately I can't post it here because running the program can tear the very fabric of the universe (I still have a black hole where my CD player used to stand). Posted by: Wheelwright at July 11, 2008 03:16 PMOk, I ported Bob's to Common Lisp. I haven't done any performance metrics or anything yet. It feels slower than the Java version, but it still is probably under a second. I did make a few performance enhancements to Bob's Java version though. You can find the details at blog [-dot-] techhead [-dot-] B-I-Z Sorry for the weird link. It was necessary. My TLD is being screened by the software for some reason. A straightforward scala port of Bob's code performs about the same and is marginally shorter. With just one iteration is is a bit slower, due to the extra load time for the scala-library jar. Over 100 iterations it takes 40 seconds to the Java versions 42 seconds. I would expect that some kind of permutation generator working over an array of ints would be a bit faster than this approach, but it would be a bit excessive. object BeustSeq { class Digit (var previous: Digit, val value: Int) { return false; def findAll(max: Long, listener: Listener) { Oh no, forgot my angle brackets. Hopefully this will work: object BeustSeq { class Digit (var previous: Digit, val value: Int) { /** Removes digit from the set. */ /** Puts digit back into the set. */ return false; Bob's solution is indeed pretty elegant. Posted by: xsonymathew at July 14, 2008 10:08 AMCame across my old TRS-80 model 100 this weekend. Seriously considering implementing it on that machine in it's old-as-hell basic language. I don't think it has longs, so I'll probably have to implement integer string operations (addition and comparison) to make it work. Or I could use arrays where each entry represents--hmm, 32767 max IIRC--each array item could be 1-4 digits I guess, then I'd still need custom addition and compare algorithms as well as a display routine. I expect it would be hideously slow, although Crazy Bob's algorithm should help a lot. I could use recursion I guess--although it should be just as easy to implement as loops. sigh... Posted by: Bill Kress at July 14, 2008 10:46 AMOk, looks like nobody bothered to type the funny link to read my blog entry, so I'll just sum it up here. I was able to improve the performance of Bob's Java version by using a singly linked list, eliminating a few method calls and additional conditional statements within the loop and by using a Throwable to unwind the stack once the max is found. The modified version can be found here: code.google.com/p/blogcode/source/browse/trunk/BeustChallenge0708/src/beust/ModifiedBeustSequence.java I also ported to Common Lisp which was a fairly easy task considering Lisp's native use of linked lists. code.google.com/p/blogcode/source/browse/trunk/BeustChallenge0708/beust.lisp Jonathan Hawkes's version converted to C looks like the faster so far: This older version of mine seems a bit slower, but it allows to set up the upper value too: Last C version (from Jonathan Hawkes's version still): A good algorithm is fast even with just Python + Psyco: I put my solution, which is in the category nor fast not concise, here: thecarefulprogrammer.blogspot.com/2008/07/cdric-beusts.html I did not aim for performance, although I tried to avoid things I just would be bad like appending at the end of a list. For me, it was simply an occasion to explore Scala and functional programming. I'll stake my claim in the Java speed challenge, beating both Bob and Jonathan. Code at tobega.blogspot.com/2008/07/cedrics-coding-challenge.html It uses a created object and its fields instead of passing a lot of values on the stack. Also uses a singly-linked list with a guard element. Posted by: Torbjorn Gannholm at July 21, 2008 04:24 PMI can knock about 3/4 of the time off of Bob's first version by noting that the largest non-repeating value is 1234567890 which fits in an int. Convert the code to only check up to this number at maximum, and use int everywhere rather than long. Posted by: Doug at July 24, 2008 05:00 PMHere's my improvement on Bob's original, which I measure at 14.6s for 100 10^10 executions vs 47.2s for the original. I'm sure this same improvement makes all the other algorithms improve similarly: public void testBobBeust() { private static class Listener { Sorry, didn't notice the code to html transformation doesn't work.
private static int _max; public void testBobBeust() { findAll((long) Math.pow(10, 10)); } /** _max = (int) Math.min(max, _maxNoRepeat); for (int length = 1; length < 11; length++) { if (find(1, length, 1, 0)) { return; /** private static boolean find(int first, int remaining, int value, int used) { for (int digit = first; digit < 10; digit++, value++) { int mask = 1 << digit; if (remaining == 1) { return true; } else if (find(0, remaining - 1, value * 10, used | mask)) { return true; } private static class Listener { // System.out.println(value); Doug, you're on to something good, except 9876543210 is the largest non-repeating value. Posted by: Svend at August 4, 2008 09:07 AMOf course I'm wrong. The number is 1023456789, which is lower than 1234567890. Posted by: Svend at August 4, 2008 09:52 AMHi Cedric, It was interesting trying to come up with a faster solution than the ones already posted. I've blogged about my experiences at www.indiangeek.net/2008/08/29/a-case-study-in-micro-optimization/ if you're interested. Posted by: Sijin Joseph at August 29, 2008 09:35 AMs='0123456789'
Hopefully I don't make a fool of myself by posting something that is wrong, but here's my longer/faster JavaScript:
function clean(results){ var k=0; I've got to say, I'm not 100% sure what the requirements for this "challenge" are. Anyway, here's a straightforward port to C of Bob's java code. codepad.org/Pf4N5ojK It runs in ~65ms on my machine. Posted by: Harvey Swik at August 31, 2008 01:58 AMThe Erlang version that was posted looks more verbose (and -module(buest). unique_list([]) -> count(Upto, Upto, State) -> finder({RunStart,MaxGap,Counter},Element) when Element - RunStart >= MaxGap -> challenge(Max) ->
My last one was completely wrong. Generates to 10^10 in 343,808 ms (slow, but much faster than the other JavaScript ones). Posted by: MrDamien at September 1, 2008 07:21 PMComputing the length of the list up to length k digits is trivially fast: for k digits, you can pick the first digits 9 ways (it can't be zero). The number of ways to pick the other digits is the number of permutations of the 9 remaining digits taken k-1 at a time. Call this function "bc". The number of permutations of n elements taken r at a time is just (n-r+1)*(n-r)*...*n. Write this in Haskell as: permutations n r = product [r+1 .. n] Then we can write the bc function: bc k = 9 * permutations 9 (k-1) Finally, the total size of the list UP TO k digits is just the sum of the size for 1 digit, 2 digits, etc: main = print (sum [bc k | k <- [1..4]) Posted by: none at September 6, 2008 12:20 AMPost a comment
|