Outline of problem:
Please note I will abuse the life out of ^
and use it as a power symbol, despite the caret symbol being the bitwise XOR operator in JS.
Take a list of positive integers,
[ x_0, x_1, ..., x_n ]
and find the last digit of the equation given by
x_0 ^ ( x_1 ^ (... ^ x_n ) ... )
I'll call this function LD(...)
for the rest of this question.
Example: For a list of integers a = [2, 2, 2, 2]
and given that 2 ^ (2 ^ (2 ^ 2)) = 65536
, it's easy to see that LD(a) = 6
.
Note that 0 ^ 0 === 1
for this question, consistent with x ^ 0 === 1
, but not with 0 ^ x === 0
.
What I've achieved so far
It's easy to conclude that x ^ 0 === 1
, no matter what.
It's also pretty easy to conclude that last digits of powers "loop" around if you do a few test cases:
LD(2 ^ 1) = 2,
LD(2 ^ 2) = 4,
LD(2 ^ 3) = 8,
LD(2 ^ 4) = 6,
LD(2 ^ 5) = 2, // Notice we've looped from hereon
LD(2 ^ 6) = 4,
LD(2 ^ 7) = 8,
...
So if we know the count of numbers that are in the loop for a particular base (4 for the above example of the base 2), we can use the modulus of that count to work out the last digit.
E.g., LD(2 ^ 55) === LD(2 ^ (55 % 4)) === LD(2 ^ 3)
And so with a bit of maths, we can get ourselves a nice array-of-arrays for each last digit, where the index of the array-of-arrays is the base and the index of each array is the modulus of the loop length:
const p = [
[ 0 ], // 0^1=0, 0^2=0 ...
[ 1 ], // 1^1=1, 1^2=1 ...
[ 2,4,8,6 ] // 2^1=2, 2^2=4 ...
[ 3,9,7,1 ] // 3^1=3, 3^2=9 ...
[ 4,6 ]
[ 5 ]
[ 6 ]
[ 7,9,3,1 ]
[ 8,4,2,6 ]
[ 9,1 ]
];
Example of usage: LD(3^7) === p[3][7-1 % 4]
- note that we have to subtract one from the exponent as each array is 0-based.
So we arrive at the JavaScript:
LD(Math.pow(a,b)) === p[a % 10][(b-1) % p[a % 10].length]
The a % 10
should be obvious, it takes just the last digit of the base number as the index in our array-of-arrays, as any non-units do not affect the last digit.
For a list like [1,2,3]
from the beginning of the question, this can be made recursive. We have an initial value of 1, in case of empty lists, as x^1 === x
, and we reverse the list to make use of accumulation of the .reduce()
method:
[1,2,3].reduceRight( (a,v) =>
p[v % 10][(a-1) % p[v % 10].length], 1)
Following through so that makes sense would look like the following:
- First up,
a = 1 (initial value), v = 3
; thenp[3 % 10] === p[3] === [ 3,9,7,1 ]
, and thus[ 3,9,7,1 ][ (1-1) % [ 3,9,7,1 ].length] === [ 3,9,7,1 ][ 0 % 4 ] === 3
. - Then,
a = 3 (last iteration), v = 2
; sop[2] === [ 2,4,8,6 ]
, and so[ 2,4,8,6 ][ 2 % 4 ] === 8
. - Finally,
a = 8, v = 1
;p[1] === [ 1 ]
, and[ 1 ][ 8 % 1 ] === 1
.
So, we get LD([1, 2, 3 ]) === 1
, which isn't hard to verify: 1 ^ (2 ^ 3) === 1 ^ 8 === 1
.
The problem:
This works, so long as the exponent is not over 10 and there isn't another iteration after that. However, if there is things go awry. Let me explain:
Say we have the array, a = [ 2,2,2,2 ]
. As 1
is our initial value, the list is initially a = [ 1,2,2,2,2 ]
. Using the reduction above:
- First iteration
a = 1, v = 2
(remembering that our.reduce
has1
as it's initial value):p[2 % 10][(1-1) % p[2 % 10].length]
= [ 2,4,8,6 ][0 % 4]
= 2
- Easily verified by
2 ^ 1 = 2
, our list is now [ 2,2,2,2 ]
- Second iteration
a = 2, v = 2
:p[2 % 10][(2-1) % p[2 % 10].length]
= [ 2,4,8,6 ][1 % 4]
= 4
- Easily verified by
2 ^ 2 = 4
, our list is now [ 4,2,2 ]
- Third iteration
a = 4, v = 2
:p[2 % 10][(4-1) % p[2 % 10].length]
= [ 2,4,8,6 ][3 % 4]
= 6
- Easily verified by
2 ^ 4 = 16
, our list is now [ 16,2 ]
- Fourth iteration, where the issue becomes apparent.
a = 6, v = 2
:p[2 % 10][(6-1) % p[2 % 10].length]
= [ 2,4,8,6 ][5 % 4]
= 4
- Easily disproved by
2 ^ 16 = 65536
.
And if you study that for a while it becomes obvious why. The third step in the final iteration,
= [ 2,4,8,6 ][5 % 4] = p[ 2,4,8,6 ][1]
should be
= [ 2,4,8,6 ][15 % 4] = p[ 2,4,8,6 ][3]
Therefore giving an incorrect result.
Question:
Is there a way, based on the previous exponent, to capture that "offset" created by only passing on the last digit of the previous iteration? Can I somehow pass on the 6
in that last iteration with another piece of information so that the modulus is correct?
So instead of just returning
p[v % 10][(a-1) % p[v % 10].length)
maybe it could return
[
p[v % 10][fn(a[0], a[1]) % p[v % 10].length],
**some other clever information here to use in the calculation to make the modulus correct**
]
where fn(a[0], a[1])
uses both the accumulated value from before, as well as some other information to calculate the correct mod value. This doesn't necessarily have to be an array, maybe an object or tuple as @aec pointed out in the comments.
One (terrible) solution would be to keep track of the previous iteration in the accumulator (e.g., for that last step, instead of returning 6
, I could return 16
and use that for the next iteration, which would give the correct index). However, it's very impractical if the numbers were very large! Say the previous step had the numbers 4142
and 623
, it's not practical to calculate 4142^623
and pass that on.
Please note that I understand there are other solutions to this, but I am curious if I can change this code to solve this problem in the single .reduce
statement I've written. So is it possible to solve this problem by modifying:
array.reduceRight( (a,v) =>
p[v % 10][(a-1) % p[v % 10].length], 1)
despite the discussed accumulator problem? It nearly works, and I think I'm one trick away from it working!
Please note the brackets! The list [3, 14, 16]
is equivalent to 3 ^ (14 ^ 16) !== (3 ^ 14) ^ 16
A few tests to check against, which can be verified for function call LU(array)
, where array
is the array of numbers:
// Current attempt
const p = [
[ 0 ], // 0^1=0, 0^2=0 ...
[ 1 ], // 1^1=1, 1^2=1 ...
[ 2,4,8,6 ], // 2^1=2, 2^2=4 ...
[ 3,9,7,1 ], // 3^1=3, 3^2=9 ...
[ 4,6 ],
[ 5 ],
[ 6 ],
[ 7,9,3,1 ],
[ 8,4,2,6 ],
[ 9,1 ]
];
// CURRENT ATTEMPT
let LU = array =>
array.reduceRight( (a,v) =>
a === 0 ? 1 : p[v % 10][(a-1) % p[v % 10].length]
, 1);
let LUTest = (array, expected) =>
console.log(
(LU(array) === expected ? "Success" : "Failed"),
"for", array, "got", LU(array), "expected", expected);
LUTest([ 2, 2, 2 ], 6)
LUTest([ 2, 2, 2, 2 ], 6)
LUTest([ 3, 4, 5 ], 1)
LUTest([ 6, 8, 10 ], 6)
LUTest([ 2, 2, 0 ], 2)
LUTest([ 12, 30, 21 ], 6)
LUTest([ 0, 0 ], 1) // x^0 === 1
LUTest([ 0 ], 0)
Tested here: http://www.wolframalpha.com/widgets/view.jsp?id=56c82ccd658e09e829f16bb99457bcbc
Thank you for reading!
Further ideas:
Had a mini-breakthrough! So for any integer that is a base of an exponent (i.e., the x
in x^y
), LD(x) === LD(x % 10)
. This is because the digits past the first (right-to-left) do not affect the unit digit of the exponent result (e.g., LD(23 ^ 7) === LD(3 ^ 7)
)
Also, as in const p = [ ...
, the array-of-arrays containing the cycles of unit values, all numbers have cycles of a length with a lowest common multiple of 4. i.e., all cycles are either 1, 2 or 4 numbers (e.g., the p[3] === [ 3,9,7,1 ]
unit array has length four).
So, we can conclude LD((x % 10) ^ (y % 4)) === LD(x ^ y)
.
Note however if a number is a multiple of 4, it becomes zero. We don't want this most of the time! You don't want 20
to becomes 0
on the exponent side either - we want the range for x
to be 1 through 10, and for y to be 1 through 4:
So, LD((x % 10 || 10) ^ (y % 4 || 4)) === LD(x ^ y)
. We can handle the special cases with
if (x === 0) {
return 0 // 0^anything = 0, including 0^0 for argument's sake.
} else if (y === 0) {
return 1 // anything ^ 0 = 1, excluding 0^0
} else {
...
}
This is very interesting! It means it is now reasonable to calculate LD(x ^ y)
, but I'm not sure what to do with this information.