Contando matrices circulantes ortogonales

8

Dos filas de una matriz son ortogonales si su producto interno es igual a cero. Llame a una matriz con todas las filas ortogonales por pares una matriz ortogonal . Una matriz circulante es aquella en la que cada vector de fila gira un elemento a la derecha en relación con el vector de fila anterior. Solo nos interesarán las matrices donde las entradas son -1o 1.

Tarea

Escribir código que cuente como muchos diferentes n/2de nmatrices ortogonales, circulantes como sea posible en 2 minutos (incluso n).

Entrada

El código no tiene entrada. Puede probar cualquier valor par de nme gusta. Por ejemplo, el código podría intentar todo lo nque son múltiplos de 4comenzar desde el más pequeño y también intentarlo n = 2.

Salida

El número de matrices circulantes ortogonales que ha encontrado. También debería haber un simple cambio en su código para permitirle generar las propias matrices.

Puntuación

El número de matrices circulantes que has encontrado.

Consejos

Las matrices ortogonales n/2por ncirculantes solo existen cuando nes un múltiplo de 4o nes menor que 4.

Un ejemplo de matriz circulante ortogonal es:

-1  1 -1 -1
-1 -1  1 -1

Consejos para un enfoque ingenuo

El enfoque más ingenuo es simplemente iterar sobre todas las matrices posibles. Esto se puede acelerar usando las siguientes dos observaciones.

  • Para probar la ortogonalidad de una matriz circulante solo necesitamos comparar cada fila con la primera. Esto se implementa en el código de muestra.

  • Podemos iterar sobre las palabras de Lyndon y luego, si encontramos una matriz ortogonal, multiplique por el número de rotaciones posibles. Esta idea aún no se ha probado, por lo que puede tener errores.

Código de muestra

Esta es una respuesta python muy simple e ingenua. Lo ejecuté usando timeout 120.

import itertools
def check_orthogonal(row):
    for i in xrange(1,int(n/2)):
        if (sum(row[j]*row[(j+i) % n] for j in xrange(n)) != 0):
            return False
    return True

counter = 0
for n in xrange(4,33,4):
    for row in itertools.product([-1,1],repeat = n):
        if check_orthogonal(row):
            counter +=1
            print "Counter is ", counter, ". n = ", n

Pruebas de corrección

Para n = 4,8,12,16,20,24,28, el número de matrices distintas que debe obtener es 12,40,144,128,80,192,560, respectivamente.

Niveles de genialidad

A juzgar por el código de muestra, por la presente presento dos niveles de genialidad que cualquier respuesta puede aspirar a lograr.

  • La genialidad del nivel de plata se logra al obtener una puntuación o 1156 .

  • El nivel de oro de la genialidad es llegar más alto que eso.

Idiomas y bibliotecas

Puede usar cualquier idioma o biblioteca que desee (que no fue diseñada para este desafío). Sin embargo, para fines de puntuación, ejecutaré su código en mi máquina, así que proporcione instrucciones claras sobre cómo ejecutarlo en Ubuntu.

Mi máquina Los tiempos se ejecutarán en mi máquina. Esta es una instalación estándar de Ubuntu en un procesador AMD FX-8350 de ocho núcleos de 8GB. Esto también significa que necesito poder ejecutar su código.

Respuestas principales

  • 332 por flawr en octava

  • 404 por RT en Python

  • 744 por solución de muestra usando pypy

  • 1156 por Thomas Kwa usando Java . ¡Increíble nivel de plata!

  • 1588 por Reimer Behrends en OCaml . ¡Nivel de oro genialidad!


fuente
¿Puedes encontrar uno para cada nque sea múltiplo de cuatro?
flawr
@flawr Ahora que es una gran pregunta! No tengo idea, pero me encantaría saberlo.
Por lo que he visto ahora (si mi guión es correcto), parecen ser bastante raros. Hasta donde yo sé, se conjetura que las matrices hadamard circulantes (matrices cuadradas con entradas en {-1,1}) existen solo para n = 1 y 4.
error
@flawr Sí en ambos aspectos (quería algo que fuera raro para hacer interesante el desafío). Pero no tengo conocimiento de que se sepa nada sobre n / 2 por n matrices circulantes.
1
Tengo una solución usando bitmasking que creo que funcionará para n = 32.
lirtosiast el

Respuestas:

5

OCaml, 1588 (n = 36)

Esta solución utiliza el enfoque de patrón de bits habitual para representar vectores de -1s y 1s. El producto escalar se calcula como de costumbre tomando el xor de los vectores de dos bits y restando n / 2. Los vectores son ortogonales si su xor tiene exactamente n / 2 bits establecidos.

Las palabras de Lyndon no son per se útiles como representación normalizada para esto, ya que excluyen cualquier patrón que sea una rotación de sí mismo. También son relativamente caros de calcular. Por lo tanto, este código utiliza una forma normal algo más simple, que requiere que la secuencia consecutiva más larga de ceros después de la rotación (o uno de ellos, si hay múltiplos) debe ocupar los bits más significativos. Se deduce que el bit menos significativo siempre es 1.

Observe también que cualquier vector candidato debe tener al menos n / 4 unos (y como máximo 3n / 4). Por lo tanto, solo consideramos vectores con n / 4 ... n / 2 bits establecidos, ya que podemos derivar otros a través del complemento y la rotación (en la práctica, todos estos vectores parecen tener entre n / 2-2 y n / 2 + 2 unos , pero eso también parece ser difícil de probar).

Construimos estas formas normales desde el bit hacia arriba menos significativo, observando la restricción de que cualquier ejecución restante de ceros (llamados "espacios" en el código) debe seguir nuestro requisito de forma normal. En particular, mientras haya que colocar al menos un 1 bit más, debe haber espacio para el espacio actual y otro que sea al menos tan grande como el espacio actual o cualquier otro espacio observado hasta ahora.

También observamos que la lista de resultados es pequeña. Por lo tanto, no intentamos evitar duplicados durante el proceso de descubrimiento, sino simplemente registrar los resultados en conjuntos por trabajador y calcular la unión de estos conjuntos al final.

Vale la pena señalar que el costo de tiempo de ejecución del algoritmo aún crece exponencialmente y a un ritmo comparable al de la versión de fuerza bruta; lo que esto nos compra es esencialmente una reducción por un factor constante, y tiene el costo de un algoritmo que es más difícil de paralelizar que la versión de fuerza bruta.

Salida para n hasta 40:

 4: 12
 8: 40
12: 144
16: 128
20: 80
24: 192
28: 560
32: 0
36: 432
40: 640

El programa está escrito en OCaml, para ser compilado con:

ocamlopt -inline 100 -nodynlink -o orthcirc unix.cmxa bigarray.cmxa orthcirc.ml

Ejecute ./orthcirc -helppara ver qué opciones admite el programa.

En las arquitecturas que lo admiten, -fno-PICpuede ofrecer una pequeña ganancia de rendimiento adicional.

Esto está escrito para OCaml 4.02.3, pero también puede funcionar con versiones anteriores (siempre que no sean demasiado antiguas).


ACTUALIZACIÓN: Esta nueva versión ofrece una mejor paralelización. Tenga en cuenta que utiliza p * (n/4 + 1)subprocesos de trabajo por instancia del problema, y ​​algunos de ellos aún se ejecutarán considerablemente más cortos que otros. El valor de pdebe ser una potencia de 2. La aceleración en 4-8 núcleos es mínima (quizás alrededor del 10%), pero se escala mejor a una gran cantidad de núcleos para grandes n.

let max_n = ref 40
let min_n = ref 4
let seq_mode = ref false
let show_res = ref false
let fanout = ref 8

let bitcount16 n =
  let b2 n = match n land 3 with 0 -> 0 | 1 | 2 -> 1 | _ -> 2 in
  let b4 n = (b2 n) + (b2 (n lsr 2)) in
  let b8 n = (b4 n) + (b4 (n lsr 4)) in
  (b8 n) + (b8 (n lsr 8))

let bitcount_data =
  let open Bigarray in
  let tmp = Array1.create int8_signed c_layout 65536 in
    for i = 0 to 65535 do
      Array1.set tmp i (bitcount16 i)
    done;
    tmp

let bitcount n =
  let open Bigarray in
  let bc n = Array1.unsafe_get bitcount_data (n land 65535) in
  (bc n) + (bc (n lsr 16)) + (bc (n lsr 32)) + (bc (n lsr 48))

module IntSet = Set.Make (struct
  type t = int
  let compare = Pervasives.compare
end)

let worker_results = ref IntSet.empty

let test_row vec row mask n =
  bitcount ((vec lxor (vec lsr row) lxor (vec lsl (n-row))) land mask) * 2 = n

let record vec len n =
  let m = (1 lsl n) - 1 in
  let rec test_orth_circ ?(row=2) vec m n =
    if 2 * row >= n then true
    else if not (test_row vec row m n) then false
    else test_orth_circ ~row:(row+1) vec m n
  in if test_row vec 1 m n &&
        test_orth_circ vec m n then
  begin
    for i = 0 to n - 1 do
      let v = ((vec lsr i) lor (vec lsl (n - i))) land m in
      worker_results := IntSet.add v !worker_results;
      worker_results := IntSet.add (v lxor m) !worker_results
    done
  end

let show vec n =
  for i = 0 to n / 2 - 1 do
    let vec' = (vec lsr i) lor (vec lsl (n - i)) in
    for j = 0 to n-1 do
      match (vec' lsr (n-j)) land 1 with
      | 0 -> Printf.printf "  1"
      | _ -> Printf.printf " -1"
    done; Printf.printf "\n"
  done; Printf.printf "\n"; flush stdout

let rec build_normalized ~prefix ~plen ~gap ~maxgap ~maxlen ~bits ~fn =
  if bits = 0 then
    fn prefix plen maxlen
  else begin
    let room = maxlen - gap - plen - bits in
    if room >= gap && room >= maxgap then begin
      build_normalized
        ~prefix:(prefix lor (1 lsl (plen + gap)))
        ~plen:(plen + gap + 1)
        ~gap:0
        ~maxgap:(if gap > maxgap then gap else maxgap)
        ~maxlen
        ~bits:(bits - 1)
        ~fn;
      if room > gap + 1 && room > maxgap then
        build_normalized ~prefix ~plen ~gap:(gap + 1) ~maxgap ~maxlen ~bits ~fn
    end
  end

let rec log2 = function
| 0 -> -1
| n -> 1 + (log2 (n lsr 1))

let rec test_gap n pat =
  if n land pat = 0 then true
  else if pat land 1 = 0 then test_gap n (pat lsr 1)
  else false

let rec test_gaps n maxlen len =
  let fill k = (1 lsl k) -1 in
  if len = 0 then []
  else if test_gap n ((fill maxlen) lxor (fill (maxlen-len))) then
    len :: (test_gaps n maxlen (len-1))
  else test_gaps n maxlen (len-1)

let rec longest_gap n len =
  List.fold_left max 0 (test_gaps n len len)

let start_search low lowbits maxlen bits fn =
  let bits = bits - (bitcount low) in
  let plen = log2 low + 1 in
  let gap = lowbits - plen in
  let maxgap = longest_gap low lowbits in
  worker_results := IntSet.empty;
  if bits >= 0 then
    build_normalized ~prefix:low ~plen ~gap ~maxgap ~maxlen ~bits ~fn;
  !worker_results

let spawn f x =
  let open Unix in
  let safe_fork () = try fork() with _ -> -1 in
  let input, output = pipe () in
  let pid = if !seq_mode then -1 else safe_fork() in
  match pid with
  | -1 -> (* seq_mode selected or fork() failed *)
     close input; close output; (fun () -> f x)
  | 0 -> (* child process *)
    close input;
    let to_parent = out_channel_of_descr output in
    Marshal.to_channel to_parent (f x) [];
    close_out to_parent; exit 0
  | pid -> (* parent process *)
    close output;
    let from_child = in_channel_of_descr input in
    (fun () -> 
      ignore (waitpid [] pid);
      let result = Marshal.from_channel from_child in
      close_in from_child; result)

let worker1 (n, k) =
  start_search 1 1 n k record

let worker2 (n, k, p) =
  start_search (p * 2 + 1) (log2 !fanout + 1) n k record

let spawn_workers n =
  let queue = Queue.create () in
  if n = 4 || n = 8 then begin
    for i = n / 4 to n / 2 do
      Queue.add (spawn worker1 (n, i)) queue
    done
  end else begin
    for i = n / 2 downto n / 4 do
      for p = 0 to !fanout - 1 do
        Queue.add (spawn worker2 (n, i, p)) queue
      done
    done
  end;
  Queue.fold (fun acc w -> IntSet.union acc (w())) IntSet.empty queue

let main () =
  if !max_n > 60 then begin
    print_endline "error: cannot handle n > 60";
    exit 1
  end;
  min_n := max !min_n 4;
  if bitcount !fanout <> 1 then begin
    print_endline "error: number of threads must be a power of 2";
    exit 1;
  end;
  for n = !min_n to !max_n do
    if n mod 4 = 0 then
    let result = spawn_workers n in
    Printf.printf "%2d: %d\n" n (IntSet.cardinal result);
    if !show_res then
      IntSet.iter (fun v -> show v n) result;
    flush stdout
  done

let () =
  let args =[("-m", Arg.Set_int min_n, "min size of the n by n/2 matrix");
             ("-n", Arg.Set_int max_n, "max size of the n by n/2 matrix");
             ("-p", Arg.Set_int fanout, "parallel fanout");
             ("-seq", Arg.Set seq_mode, "run in single-threaded mode");
             ("-show", Arg.Set show_res, "display list of results") ] in
  let usage = ("Usage: " ^
               (Filename.basename Sys.argv.(0)) ^
               " [-n size] [-seq] [-show]") in
  let error _ = Arg.usage args usage; exit 1 in
  Arg.parse args error usage;
  main ()
Reimer Behrends
fuente
¡Esto es genial y bienvenido de nuevo! Habiendo dicho eso ... ¿qué tal una respuesta Nim también? :)
Su código solo llega a 36: 432 en mi máquina a tiempo.
Huh Funciona a 40 en poco menos de dos minutos en mi computadora portátil con un procesador quadcore (Intel Core i7 de 2.5 GHz), por lo que estaba bastante seguro de que también llegaría a 40 en el suyo. Actualizaré en consecuencia. Sobre su otra pregunta, si bien tengo una implementación de Nim de fuerza bruta, esa todavía funciona dos veces más lenta (debido a la parte de fuerza bruta) y no es muy diferente del código de Thomas Kwa (solo un poco más de reducción de la búsqueda espacio), por lo que no es que te estés perdiendo nada emocionante.
Reimer Behrends
¿Tengo razón en que su código necesita un sistema de 64 bits? Acabo de probarlo en uno de 32 bits donde siempre genera 0.
1
Correcto, ya que almacena los vectores como ints. También podría forzar una representación de 64 bits (o incluso enteros grandes), pero eso sería enormemente más lento en un sistema de 32 bits.
Reimer Behrends
3

Java, 1156 matrices

Utiliza una máscara de bits bastante ingenua, y toma menos de 15 segundos para n = 28 en mi máquina.

Las matrices circulantes están determinadas por sus primeras filas. Por lo tanto, represento las primeras filas de las matrices como vectores de bits: 1 y 0 representan -1 y 1. Dos filas son ortogonales cuando el número de bits configurados cuando se juntan es n / 2.

import java.util.Arrays;

class Main {

    static void dispmatrix(long y,int N)
    {
        int[][] arr = new int[N/2][N];
        for(int row=0; row < N/2; row++)
        {
            for(int col=0; col < N; col++)
            {
                arr[row][col] = (int) ((y >>> (N+row-col)) & 1L);
            }
        }
        System.out.println(Arrays.deepToString(arr));
    }

  public static void main(String[] args) {

    int count = 0;
    boolean good;
    boolean display = false;
    long y;
    for(int N=4; N <= 28 ;N += 4)
    {
    long mask = (1L << N) - 1;
    for(long x=0; x < (1L<<N); x++)
    {
        good = true;
        y = x + (x << N);

        for(int row = 1; row < N/2; row++)
        {
            good &= N/2 == Long.bitCount((y ^ (y >>> row)) & mask);
        }

        if(good)
        {
            if(display) dispmatrix(y,N);
            count++;
        }

    }
    System.out.println(count);
    }
  }
}

No puedo hacer que Eclipse funcione en este momento, así que esto se probó en repl.it.

Aquí está el número de primeras filas que son ortogonales a las primeras r filas posteriores para n = 28:

[268435456, 80233200, 23557248, 7060320, 2083424, 640304, 177408, 53088, 14896, 4144, 2128, 1008, 1008, 560]

Optimizaciones:

  • Dado que la ortogonalidad no cambia con una rotación cíclica de ambas filas, solo debemos verificar que la primera fila sea ortogonal al resto.
  • En lugar de hacer manualmente un cambio cíclico de N bits N / 2 veces, almaceno los bits que se desplazarán en la parte superior longy luego uso uno andcon los N bits más bajos para extraer los necesarios.

Posibles optimizaciones adicionales:

  • Genera palabras de Lyndon. Según mis cálculos, esto solo tiene sentido si las palabras de Lyndon se pueden generar en menos de ~ 1000 ciclos cada una.
  • Si las palabras de Lyndon tardan demasiado, solo podemos verificar los vectores de bits que comienzan 00y usar sus rotaciones (y las rotaciones del NOT) cuando encontramos una matriz ortogonal. Podemos hacer esto porque 0101...01y 1010...10no son posibles las primeras filas, y todas las demás contienen a 00o a 11.
  • Puede ramificarse a mitad de camino cuando la matriz es probablemente ortogonal. Sin embargo, no sé cuánto costará la ramificación, así que tendré que probar.
  • Si lo anterior funciona, comience con una fila que no sea la fila 1?
  • Quizás haya alguna forma matemática de eliminar algunas posibilidades. No sé qué sería eso.
  • Escribe en C ++, por supuesto.
lirtosiast
fuente
Esto es genial. ¡Gracias! Espero con interés algunas de sus optimizaciones para que también podamos ver los números n=36.
¡Ah, y alcanzaste la genialidad de nivel Plata! :)
2

Python (matrices 404 en i5-5300U)

Publicando esto principalmente como un punto de partida para que otros mejoren, esto se puede limpiar mucho, paralelizar, etc.

import numpy
import itertools
import time

def findCirculantOrthogonalRows(n, t1, timeLimit):
  validRows = []
  testMatrix = numpy.zeros((n//2, n))
  identityMatrixScaled = n*numpy.identity(n//2)
  outOfTime = False
  for startingRowTuple in itertools.product([1, -1], repeat=n):
     for offset in range(n//2):
       for index in range(n):
         testMatrix[offset][index] = startingRowTuple[(index-offset) % n]

     if(numpy.array_equal(identityMatrixScaled, testMatrix.dot(testMatrix.transpose()))):
      validRows.append(startingRowTuple)

    if(time.clock() - t1 >= timeLimit):
      outOfTime = True
      break

  return (validRows, outOfTime)

n = 4
validFirstRows = []
t1 = time.clock()
timeLimit = 120
fullOutput = True

while(True):
  print('calling with', n)
  (moreRows, outOfTime) = findCirculantOrthogonalRows(n, t1, timeLimit)

  if(len(moreRows) > 0):
    validFirstRows.extend(moreRows)

  if(outOfTime == True):
    break

  n += 4

print('Found', len(validFirstRows), 'circulant orthogonal matrices in', timeLimit, 'seconds')

if(fullOutput):
  counter = 1
  for r in validFirstRows:
    n = len(r)
    matrix = numpy.zeros((n//2, n))
    for offset in range(n//2):
      for index in range(n):
        matrix[offset][index] = r[(index-offset) % n]
    print('matrix #', counter, ':\n', matrix)
    counter += 1
    input()
RT
fuente
Agregué un código de muestra. La primera mejora obvia es iterar sobre las palabras de Lyndon, pero no estoy seguro de cómo codificar eso.
2

Matlab / Octave, 381/328 matrices

También solo el enfoque ingenuo, probando todas las combinaciones posibles.

counter = 0;
%ok: 2,4,8
%none: 
tic
for n=[2,4,8,12,16,20];
    m=n/2;
    N=2^n-1;
    for k=1:N

        k/N; %remove ; in order to see progress
        v=(dec2bin(k,n)-'0')*2-1;               %first row

        z=toeplitz([v(1) fliplr(v(m+2:n))], v); %create circulante matrix
        w = z * z.';
        if norm(w - diag(diag(w))) < eps
            counter = counter+1;
            %n    %remove % in order to see the matrices
            %z
        end
        if toc>120;
            break;
        end
    end
end
counter
falla
fuente
En octava, el código imprime una gran cantidad en la pantalla que podría estar ralentizándolo. "ans = ...."
Ah, claro, olvidé agregar eso: las líneas más sangradas que hay son una ny una z, estas dos se pueden comentar con una sola %. Y luego puede agregar un ;después de counter = counter+1y el k/N que suprimirá la salida.
flawr