¿Hay alguna forma de optimizar el doble bucle: for (int I = 0; I <n-1; I ++) {for (int j = I + 1, j <n, j ++) {doSomething (i, j);} }?

Si y no.

Consideremos primero las optimizaciones arquitectónicas, el paralelismo y demás.

Estos pueden hacer una * gran * diferencia en algo como un sombreador.

Dado que, en algunos casos como ese, no puede evitar simplemente iterar sobre todo xy todo y en una imagen, la estrategia más importante es optimizar para el caso más común en su “doSomething”.

Entonces, si tiene un sombreador que dibuja un halo de radio de 10 píxeles alrededor de todas las fuentes de luz en una escena, debería desaparecer rápidamente en aquellos casos en los que no hay nada que hacer por un píxel. Por lo tanto, una verificación rápida para ver si está dentro del cuadro delimitador 10 × 10 comprobable trivialmente alrededor de cada fuente de luz será mucho más rápido que calcular más, donde el píxel está fuera del radio para cualquier fuente de luz. Entonces

Ahora ignoremos el paralelismo y tal, y centrémonos en un bucle de un solo hilo.

Y eso nos lleva a la pregunta: ¿es su tarea algo que absolutamente debe realizarse para cada entrada de X por Y? Si la respuesta es no, entonces no puede evitar el código O (N ^ 2). Por ejemplo, si está procesando una imagen y desea oscurecer cada píxel en un 50%, no puede evitar hacer algo con cada píxel. Dos bucles anidados es lo mejor que puedes hacer.

La única optimización que se me ocurre hacer es que si conoces la estructura de tu memoria es tal que todos los píxeles están contiguos en la memoria, entonces puedes simplemente pasar de 0 a X * Y, en lugar de repetir todo X para todo Y.

Ahora, puede argumentar que ahora es O (N) en lugar de O (N ^ 2), pero es un argumento gracioso, ya que en un caso, N es X o Y, y en el otro, N es X * Y. Convertirlo en un solo bucle evita un incremento y una ramificación al final de cada bucle interno, lo que es muy poco probable que sea una mejora significativa del rendimiento en la gran mayoría de los casos. El doble bucle es posiblemente más fácil de leer y de asimilar, así que quizás te quedes con eso.

Pero, ¿qué sucede con esos casos en los que toca solo un pequeño subconjunto, pero prueba todos los valores para ver si necesita hacerlo? Puede valer la pena ver si hay formas de mejorar eso, para reducir las que necesita probar.

Considere dibujar un círculo sin llenar de radio r (digamos, 100 px) en una pantalla con resolución (X, Y), por ejemplo, una pantalla 4K en (3840,2160), alrededor de un punto en (x, y), que podría ser ( 150,150).

Un enfoque de fuerza bruta es recorrer cada píxel en la pantalla y ver si se encuentra en el círculo, y si es así, trazarlo:

foreach X
foreach Y
if ((Xx) ^ 2 + (Yy) ^ 2 está dentro de 1 de r ^ 2)
dibuja un píxel en X, Y.

Esto se repetirá en una pantalla 4K 3840 * 2160 veces, o 8,294,400 veces.

Un enfoque más óptimo pero aún de fuerza bruta es recorrer cada píxel dentro del radio del círculo y, si se encuentra en el círculo, trazarlo:

para testX = 0 a 2 * r
para testY = 0 a 2 * r
if (testX ^ 2 + testY ^ 2 está dentro de 1 de r ^ 2)
dibuje un píxel en x + testX, y + testY.

Esto hará un bucle, con un radio de 100 px, 2r ^ 2 veces o 40,000 veces. Toda una mejora!

Un enfoque más óptimo pero aún de fuerza bruta es darse cuenta de que las cuatro esquinas del círculo son idénticas, en relación con el centro, y simplemente recorrer cada píxel en un cuarto del círculo, y si se encuentra en el círculo, trazarlo, y sus tres puntos reflejados:

para testX = 0 to r
para testY = 0 to r
if (testX ^ 2 + testY ^ 2 está dentro de 1 de r ^ 2)
dibuje un píxel en x + testX, y + testY.
dibuje un píxel en x + testX, y – testY.
dibuje un píxel en x – testX, y + testY.
dibuje un píxel en x – testX, y – testY.

Esto repetirá r ^ 2 veces, o 10,000 veces. Esto es más de 800 veces más rápido que recorrer toda la pantalla.

Esperemos que esto demuestre que nuestra selección de límites para X e Y es importante .

Un enfoque aún más óptimo es darse cuenta de que cada * octavo * de un círculo es simétrico, y que cuando dibuja un círculo, comenzando en la parte inferior y hacia la derecha, durante el primer octavo círculo la pendiente nunca será mayor que 45 grados, por lo que solo trazará un solo píxel para cada X. Y cada Y será la Y anterior, o como máximo 1 mayor. Y luego puedes reflejar eso 8 veces alrededor del círculo.

testX = 0
testY = r
mientras testX <testY
testX ++
dibuje un píxel en x + testX, y + testY.
dibuje un píxel en x + testX, y – testY.
dibuje un píxel en x – testX, y + testY.
dibuje un píxel en x – testX, y – testY.
dibuje un píxel en y + testX, x + testY.
dibuje un píxel en y + testX, x – testY.
dibuje un píxel en y – testX, x + testY.
dibuje un píxel en y – testX, x – testY.
if (testX ^ 2 + testY ^ 2 es mayor que r ^ 2)
irascible-

Esto se repetirá r veces, o 100 veces en nuestro ejemplo, para trazar 800 píxeles. Esta es una mejora de velocidad de 82,944 veces sobre nuestro algoritmo original, e incluso en comparación con la mejor fuerza bruta, es O (r) en lugar de O (r ^ 2).

[Tenga en cuenta que en un sistema con paralelismo mayor que r (para que r ^ 2 iteraciones se ejecuten en r tiempo o mejor), lo anterior podría * no * ser el algoritmo más rápido, ya que cada ciclo se basa en el anterior, por lo que no puede ser en paralelo]

El punto aquí es demostrar que nuestra selección de algoritmos es aún más importante .

Entonces, si puede encontrar algún algoritmo que no use O (N ^ 2) para su aplicación específica, puede obtener grandes mejoras de velocidad, al menos en sistemas lineales.


Algo más que acabo de notar, al releer la pregunta, que será cierto al menos en algunos idiomas, es que el -1 se puede mover fuera del ciclo, evitando que se realice la resta en cada iteración:

para (int i = 0; i <n – 1; i ++) {
para (int j = i + 1; j <n; j ++) {
hacer algo (i, j);
}
}

Convirtiéndose:

límite = n – 1;
para (int i = 0; i <límite; i ++) {
para (int j = i + 1; j <n; j ++) {
hacer algo (i, j);
}
}

También está agregando 1 a i en dos lugares, lo que podría refactorizarse, a costa de la legibilidad, para otra aceleración insignificante:

límite = n – 1;
para (int i = 0; i <límite;) {
para (int j = i; j <límite;) {
hacer algo (++ i, ++ j);
}
}

Ahora, lo que parece estar haciendo, es para cada entrada, iterar sobre todas las entradas posteriores en una lista.

Si su DoSomething es, por ejemplo, imprimir (i * j) para imprimir una tabla de multiplicar, esta doble iteración no se puede evitar.

Pero puede haber casos en los que un algoritmo más óptimo funcione en su lugar.

Agregar este prefacio, ya que los comentarios en la pregunta se agregaron después de responder la pregunta (mi respuesta está a continuación). Y los comentarios cambiaron la naturaleza de la pregunta por completo. Los comentarios son ” No me preocupa lo que hace dosomething () ” y ” … una forma de hacer el doble bucle en menos de O (n²) “.

hacer algo()

Si se distancian de lo que está haciendo dosomething (), entonces NO pueden optimizar el ciclo. La optimización supone que se conserva la corrección (el comportamiento) del programa. Hay puristas que dirían que a + b + c y c + b + a no son lo mismo computacionalmente, porque el redondeo ocurrirá de manera ligeramente diferente. Entonces, aunque la suma es asociativa en el mundo matemático, NO se cumple en el mundo computacional. La forma de evitar este argumento específico es confirmar que el margen de error introducido por el cambio en la secuencia de operaciones es aceptable para el problema en cuestión, pero para eso, no solo tienen que preocuparse por dosomething () , pero más aún con por qué lo estás haciendo. ¿Por qué mencioné la asociatividad? Porque si realmente estuvieras haciendo algo como “ sum = sump + A [I, J] ”, cualquier cambio como cambiar el ciclo o revertir la indexación de los ciclos, etc., dependería de la asociatividad de La operación de suma.

[matemática] \ displaystyle {\ matemática {O} (n ^ 2)} [/ matemática]

La reducción de la complejidad temporal de un algoritmo generalmente es difícil y depende completamente del problema y su implementación, lo que nos lleva de vuelta a dosomething () . Si estaba haciendo algo con una matriz de 2-d de tamaño n * n, entonces no habrá forma de solucionar el problema, ya que en última instancia, debe leer al menos en su totalidad (o como parecen indicar los bucles: la mitad) la matriz de 2-d. Las mejoras arquitectónicas generalmente tampoco afectan la complejidad del tiempo, ya que el propósito de la complejidad del tiempo es comprender cómo el tiempo de ejecución del algoritmo se escala con el tamaño del problema. El tamaño del problema superará cualquier truco arquitectónico que pueda aplicar, ya que el propósito de comprender la complejidad del tiempo es corregir el H / W (es decir, no aumentará con el tamaño del problema).

Mi respuesta original sigue.


Depende de cuál sea la arquitectura de destino, cuál es el código dentro del bucle y qué compilador está utilizando.

¿Es una CPU de un solo subproceso? ¿Hay vectorización? ¿Hay una GPU? ¿Hay acceso a la memoria: matrices, índices, qué avances? ¿Existe un paralelismo dentro del bucle o depende estrechamente de las operaciones de las iteraciones anteriores?

Algunas técnicas pueden ser intercambiar los bucles. Desenrollar partes o la totalidad de uno o ambos bucles. cambiando la dirección de los contadores. Poner en compilador sugerencias. Comprender la localidad temporal y espacial del acceso a la memoria. Comprender la jerarquía de almacenamiento en caché de la arquitectura de destino, etc.

More Interesting

¿Cuáles son los diversos desafíos que enfrentan los ingenieros de desarrollo de software?

¿Qué hace el 1% de los mejores ingenieros de software de manera diferente y dónde trabaja la mayoría de ellos?

¿Por qué se usa Eclipse más que NetBeans para el desarrollo de Java?

¿Podría la tecnología desarrollarse infinitamente más rápido usando una simulación?

¿Por qué se discute la complejidad del tiempo con más frecuencia que la complejidad del espacio?

¿Viaja el estudiante al extranjero durante M.Tech Software Engineering (Programa integrado de 5 años en la Universidad VIT?

¿Cuál es el tema más candente en la automatización de procesos robóticos?

¿En qué gastan sus compañías de software miles de millones de dólares?

¿Cuál es la tecnología detrás del flujo de actividad de Facebook?

¿Cómo funcionará en Karma IT Solutions, Chennai para mujeres más frescas?

¿Es posible conseguir un trabajo como ingeniero de software en Google sin haber estudiado conceptos informáticos como algoritmos y estructuras de datos?

¿Es más difícil obtener una licenciatura en Ingeniería de Software o Matemática Actuarial?

¿Las API de mensajería de texto (por ejemplo, Twilio, Nexmo) me permiten enviar mensajes como desde mi número de teléfono?

¿Va a ocurrir realmente la 'singularidad'?

¿Cómo es la Universidad Carnegie Mellon de Silicon Valley, California para el curso y las perspectivas laborales? ¿Puede alguien sin antecedentes de CS en pregrado solicitar MS en Ingeniería de Software sin carta de recomendación (pero con puntajes decentes GRE / TOEFL / CGPA) y ser aceptado?