6

So I have a 4x4 matrix like so

 |0 1 2 3
-+-------
0|0 1 3 6
1|2 4 7 a
2|5 8 b d
3|9 c e f

and I am traversing it in the order specified by the hexadecimal characters in it. so start at (0, 0), then (1, 0), (0, 1), (2, 0), (1, 1), (0, 2)...

So here is the code:

def diagonal(n):
    for a in range(n):
        for b in range(a + 1):
            yield a - b, b
    for a in range(n - 1):
        for b in range(n - a - 1):
            yield n - b - 1, b + 1 + a

iterating through this gives

for x, y in diagonal(4):
    print((x, y))

# (0, 0)
# (1, 0)
# (0, 1)
# (2, 0)
# (1, 1)
# (0, 2)
# (3, 0)
# (2, 1)
# (1, 2)
# (0, 3)
# (3, 1)
# (2, 2)
# (1, 3)
# (3, 2)
# (2, 3)
# (3, 3)

which is exactly as I intended. The part I am stuck on is trying to make a function where I give it the index, and it gives me the coordinates. So for my 4x4 matrix,(this is still in hexadecimal)

0 -> (0, 0)
1 -> (1, 0)
2 -> (0, 1)
3 -> (2, 0)
4 -> (1, 1)
5 -> (0, 2)
6 -> (3, 0)
7 -> (2, 1)
8 -> (1, 2)
9 -> (0, 3)
a -> (3, 1)
b -> (2, 2)
c -> (1, 3)
d -> (3, 2)
e -> (2, 3)
f -> (3, 3)

I have intentions to move on to variable sized square matrices so I cannot just hard code these values into a dict.

I have been tinkering for hours trying to get this to work but I cannot for the life of me get it to work.

This is not homework, just something I'm working on in my spare time, and it's slowly driving me up the wall.

If anything is not clear please don't hesitate to ask.

Thanks in advance.

Edit: I presume someone will comment about this post Traverse Matrix in Diagonal strips which is similar but as with my first function this only iterates over the coordinates, and I cannot work out the coordinates from an index.

Gal Avineri
  • 524
  • 5
  • 13
dangee1705
  • 3,445
  • 1
  • 21
  • 40

2 Answers2

5

Here is a function that seems to do what you want. An explanation comes after the code.

from math import sqrt

def triangular(n):
    return n * (n + 1) // 2

def coords_from_index(ndx, n=4):
    if ndx < triangular(n):
        basecol = (int(sqrt(8 * ndx + 1)) - 1) // 2
        row = ndx - triangular(basecol)
        col = basecol - row
    else:
        oldcol, oldrow = coords_from_index(n**2 - 1 - ndx, n)
        row = n - 1 - oldrow
        col = n - 1 - oldcol
    return col, row

# Test code
n = 4
for ndx in range(n**2):
    print(hex(ndx)[2:], '->', coords_from_index(ndx, n))

The printout from that test code is:

0 -> (0, 0)
1 -> (1, 0)
2 -> (0, 1)
3 -> (2, 0)
4 -> (1, 1)
5 -> (0, 2)
6 -> (3, 0)
7 -> (2, 1)
8 -> (1, 2)
9 -> (0, 3)
a -> (3, 1)
b -> (2, 2)
c -> (1, 3)
d -> (3, 2)
e -> (2, 3)
f -> (3, 3)

Here is a brief explanation of my code.

Like your code to generate the coordinates in order, I treat the upper-left triangle of the square differently from the lower-right triangle. Let's look first at the upper-left triangle, which for the square of size 4 includes the indices 0 through 9.

If you look at the top number in each column, you see that those are the "triangular numbers", which are the sum of consecutive integers starting from 0. So the top row is 0, 0+1, 0+1+2, and 0+1+2+3. The well-known formula for those numbers is

triangular(n) = n * (n + 1) // 2

So I wrote a small routine for that. If you know the triangular number (call it ndx) and want to find n, you can use algebra to solve the quadratic equation and you get

n = (sqrt(8 * ndx + 1) - 1) // 2

If you replace the sqrt with int(sqrt( you get that same result for a triangular number, and you also get the "base" number for any ndx that falls between two triangular numbers. You can then use your index and the "base number" to find the corresponding column and row for the index.

Now, if you look at the lower-right triangle, which includes the indices a through f, you see that there is symmetry with the upper-left triangle. I chose to use that symmetry to calculate the row and column for any of these indices. I could have calculated them more directly, but the approach I used works well.

Note that if you use really large values of n and ndx that the int(sqrt( does not always give the correct answer, due to the vagaries of floating-point arithmetic. But ndx needs to be really large, on the order of 2**50, before that happens. Let me know if you need a more reliable routine to calculate the integer square root.

Rory Daulton
  • 21,934
  • 6
  • 42
  • 50
  • This is an impressive answer. Thanks. As a follow-up question. Is there an equally ellegant way to compute the inverse? That is given two coordinates can I find the (zigzag) value? I am trying to build an inverse to coords_from_index in the form of index_from _coords(x,y). – Boris Reif Jan 08 '23 at 22:44
4

This is basically an algorithmic question :)

I'd suggest the following algorithm:

  1. Find in what diagonal the requested index relies
  2. Find the position of the index on that diagonal
  3. Infer the coordinate

The implementation i'd suggest is the following:

As a initial step, prepare an array of the first linear index in each diagonal.
for example in a 4x4 matrix the array would look like this: [0, 1, 3, 6, 10, 30, 15].
Notice These are basically bins. This means that all the linear indices that maintain a[i] <= index < a[i+1] are found in the i'th diagonal.

Than, when receiving an index:

  1. Find in which diagonal the requested index is found.
    You can do this by finding d which fulfills bin[d] <= index < bin[d]

  2. Find the position on that diagonal.
    This is distance between bin[d] and index. This means that if index - bin[d] = k, than the index is in the k'th position on that diagonal.

  3. Infer the coordinate.
    Up until now you've found that the index is in the k'th position on the d'th diagonal.

    Let us denote the size of the matrix is MxM.
    if x <= M-1 than we are on the diagonal (k, d - k)
    if x > M-1 than we are on the diagonal (d - (m-1) + k, (m-1) - k)
    When we will insert k that we've found earlier we will find the requested coordinate.

Implementation

def ind2cor(index):
    d = bisect_left(a, index)
    if index != a[d]:
        d -= 1

    k = index - a[d]

    if d <= m-1:
        return k, d - k
    else:
        return d - (m - 1) + k, (m - 1) - k

Example:

m = 4
a = [0, 3, 6, 10, 13, 15]
for i in range(16):
    print('{}: {}'.format(i, ind2cor(i)))

Yields:

0: (0, 0)
1: (0, 1)
2: (1, 0)
3: (0, 2)
4: (1, 1)
5: (2, 0)
6: (0, 3)
7: (1, 2)
8: (2, 1)
9: (3, 0)
10: (1, 3)
11: (2, 2)
12: (3, 1)
13: (2, 3)
14: (3, 2)
15: (3, 3)

I can also provide a method to generate the indices of the diagonals:

def genereate_diagonal_indices(m):
    a = [0]
    for i in range(1, m):
        a.append(a[-1] + i)
    for i in range(m, 1, -1):
        a.append(a[-1] + i)
     return a

So you could use a = genereate_diagonal_indices(m) in the code above instead.

Gal Avineri
  • 524
  • 5
  • 13
  • Out of interest i compared the complexities of the algorithms Rory and I provided. It seems that in time complexity both Rory's algorithm and mine are O(log(m)), where m is the length of one axis of the matrix. In memory complexity Rory's algorithm is O(1) and mine is Theta(m). – Gal Avineri Nov 23 '18 at 10:30
  • I believe you have misunderstood my algorithm. There is one `if` statement and one resulting recursive call, but those happen at most once and do not increase the algorithmic complexity. The `sqrt` operator is done in O(1) in modern CPUs, I believe, though I could be wrong for some CPUs. As a result, my algorithm does straight calculations and thus is O(1) in both time and memory. Unless you mean that `m` is the number of *bits* of the length of one side of the square? – Rory Daulton Nov 23 '18 at 12:12
  • I might have misunderstood, i'd love to discuss it! I claimed your algorithm has a time complexity of O(log(m)) due to the calculation of the sqrt. I searched the internet for the complexity of calculation of sqrt and found this https://stackoverflow.com/questions/23103847/what-is-the-time-complexity-of-the-following-function which claims it is O(log(m)). However it might be wrong. Could you support the claim that sqrt time complexity is O(1)? :) – Gal Avineri Nov 23 '18 at 16:20
  • This point is debatable. I could claim that I noted that my code is meant for `ndx < 2**50`, so the limitation makes the code O(1). Instead, I'll give the timing results on my computer. Using `%timeit` on `int(sqrt(n))` gives `295 ns` for `n=1`, `299 ns` for `n=2`, `313 ns` for `n=2**25-1`, and `371 ns` for `n=2**50-1`. This is worse than my claimed O(1) and better than your claimed O(log(n)). I'm not sure what that is--it looks a little better than O(log(log(n))). – Rory Daulton Nov 23 '18 at 16:41
  • If we assume ```ndx < c``` than i could also infer that ```m < c2``` and that would make my algorithm O(1) as well. So for the sake of the argument should we continue without this assumption? Thank you for trying to collect timing results! The results are interesting, but i'm not sure we can reliably try to infer the complexity by them due to many other factors which my act. Therefore i'd like to find the theoretical compelxity. I've searched some more and found this https://cstheory.stackexchange.com/questions/9706/complexity-of-finding-the-square-root-of-a-perfect-square – Gal Avineri Nov 23 '18 at 17:00
  • One last comment from me: The time complexity depends on how the CPU does the square root calculation, and I believe that is protected information. We don't know how the CPU handles small numbers or large numbers, and it is conceivable that they are treated the same, making the calculation O(1). We don't know how much information is pre-calculated and stored. We just can't say for sure. I recant my first claim and now claim that my code is slightly worse than O(1) but is close enough to it for practical purposes. Thanks for the stimulating discussion--I already upvoted your answer. – Rory Daulton Nov 23 '18 at 17:24
  • I haven't thought of it this way. Thank you too for the enriching discussion! :) – Gal Avineri Nov 23 '18 at 18:25