I tinkered with my programs a bit more. Traversing the search space depth-first (i.e. with recursion) instead of the breadth-first (iterative) approach I employed didn't do me any favors in UserRPL. The program just grew bigger and slower. In SysRPL on the other hand it worked out pretty well. I also found an improvement for what was the inner loop (now the only loop), which is now a DO...+LOOP like I wanted it to be (BINT range concerns previously prevented it).
Code:
::
#ZERO#ONE %0
' ::
%10* BINT10 3PICK 3PICKOVER
UNCOERCE %MOD COERCE #-
DO
INDEX@ GETLAM
4PICKOVER #AND #0<>
ITE_DROP
::
4PICK#+ 3PICK#1+_
BINT10 #=casedrop
::
DROPDUP INDEX@ UNCOERCE
%+ BINT28 UNROLL
;
3PICK INDEX@ UNCOERCE %+
10GETLAM EVAL
;
OVER +LOOP
3DROP
;
DUPONE
BINT9 ONE_DO
DUP #2*
LOOP
' NULLLAM BINT10 NDUPN DOBIND
EVAL ABND
;
This is significantly faster (2.05 ... 2.06 seconds) than my previous SysRPL efforts, and tied for size with my previous smallest one too (145 bytes, #6AB8h). For further acceleration I tried exiting early when the single solution is found - by throwing an error with the result as message (insert BINT27 NDROP a%>$ DO$EXIT after the BINT28 UNROLL). Unfortunately that error propagates through TEVAL too, preventing it from finishing a measurement.
No big problem though, just surround the EVAL with ERRSET ... ERRTRAP SysErrorTrap to catch the error, display it, and carry on into TEVAL. (If there's an edit line active, it would wait for confirmation on the error box and mess up the timing, but what sane person runs TEVAL in the editor?) All that together brings the size up to 162.5 bytes (and the checksum is #0F42, if there's anybody typing along), but the time goes down to 1.01 ... 1.05 seconds. Note that the SysErrorTrap seems to take up a good portion of this measured time, as a TEVAL on this ...
Code:
::
"381654729." ERRSET DO$EXIT ERRTRAP SysErrorTrap
;
... shows: 0.29 ... 0.34 seconds for that part alone, just so TEVAL doesn't get aborted. That means without the error trap, the time would be closer to 0.7 seconds.
---
Small note on the theory side: I identified another reason for checking divisibility first, like Claudio and I did: it has a higher rejection rate than the duplicate digits check - and as you probably know it's usually prudent to run the more strict check first, giving you a higher chance to skip the other one altogether for a quicker rejection. (Unless the check with the higher rejection rate is significantly slower, that is. But in this case it's definitely not.)
On the first digit, neither check rejects anything because divisibility by 1 is always true and there are no digits already taken ... but on the second only 1 out of 9 is a duplicate, whereas 5 out of 9 are odd. This continues through all recursion levels:
- 2/9 vs. 6/9 on the third digit,
- 3/9 vs. 7/9 on the fourth,
- 4/9 vs. 8/9 on the fifth,
- 5/9 vs. either 7/9 or 8/9 on the sixth (allowed digits are {2 8} or {4} or {6}, depending on the prior digits),
- 6/9 vs. again either 7/9 or 8/9 on the seventh, and
- 7/9 vs. 8/9 on the eighth digit.
Only on the last digit they agree again on rejecting all but one digit.
---
Now for something different: what if we expand the puzzle to arbitrary base-N, i.e. build a (N-1)-digit number in base-N where no digits are 0 or a duplicate, and the leading-digits requirement is satisfied too?
Code:
::
CK1NoBlame CKREAL
#ZERO#ONE %0
4PICK COERCEDUP #2-
BINT19 #> caseSIZEERR
BINT1
OVER ONE_DO
SWAPOVER #2*
LOOP
SWAP
' ::
1GETLAM %* 3GETLAM 3PICK
3PICKOVER
UNCOERCE %MOD COERCE #-
DO
INDEX@ #3+ GETLAM
4PICKOVER #AND #0<>
ITE_DROP
::
4PICK#+ 3PICK#1+_
3GETLAM OVER#=case
::
SWAPDROP OVERINDEX@
UNCOERCE %+SWAP
BINT3 #* #2- UNROLL
;
3PICK INDEX@ UNCOERCE %+
2GETEVAL
;
OVER +LOOP
3DROP
;
OVER #6+ ROLL
' NULLLAM 4PICK #3+ NDUPN
DOBIND 2GETEVAL ABND
;
This program (185 bytes, #CF4Eh) expects a real number designating the base on the stack. Supported are bases 2 to 21 (larger bases are not compatible with the use of BINTs as bitset to remember already used digits, but real number precision probably breaks things before that in the bigger bases). Interesting to note: odd bases never have solutions, and some even ones have more than one (e.g. base 4: \(123_4 = 27_{10}\) and \(321_4 = 57_{10}\)). I'm sure Albert Chan will analyse these observations in detail, he seems to enjoy doing that. Ideas for him: the divisibility test for the final digit is a division by N-1, which in base-N has a sum-of-digits shortcut regardless of base (we know the N=10 case well: divisibility by 9 has this shortcut); for that final digit we are testing divisibility on the entire number, which has all non-zero digits of that base exactly once, i.e. the sum of digits has to be \(\sum_{i=1}^{N-1}{i} = \frac{N \cdot (N-1)}{2}\). If we know this check cannot succeed, there cannot be a solution either. Albert, your turn, fill in the blanks.
This program tries to find all solutions, and therefore does not use an early abort mechanism like throwing a custom error.