martes, 6 de mayo de 2014

Tuenti Challenge 2014 -6ª Parte

Challenge 11 - Pheasant

Después de todo el tiempo perdido en el desafío 10, apenas me quedaban unas horas por delante para la finalización del concurso. Por lo que intente ir más deprisa de cara a poder finalizar el máximo de problemas posibles, de ahí que quizás el código se resienta un poco (¿más aún?).

 

El problema 11 nos comentaba que habían tenido un problema con su servidor de feeds, del cual se habían perdido los datos, y que teníamos que ayudarles en la labor de intentar recuperar parte de la información.

 

Para dicha tarea, nos proporcionaban las copias de respaldo que tenían a través del archivo  https://contest.tuenti.net/resources/feeds.tar.gz. En dicho archivo podemos ver el contenido de 2 carpetas distintas: por una parte los timestamps en texto plano, y por otra el contenido de las tablas cifrado en AES-256 (32 bytes) en modo ECB (cada bloque de forma individual, lo cual es bastante inseguro). Todo ello muy bien ordenadito dentro de sus respectivas carpetas, nombradas con los dos últimos dígitos del 'userID'.

   

La clave AES nos las proporcionaban a través del input junto al userID, con el único añadido de que faltaban los 3 últimos bytes. Por lo que nos tocaría probar cada una de las combinaciones posibles en el rango a-zA-z.

 

Para la tarea de descifrado eché mano de OpenSSL, que tiene unas funciones muy cucas para trabajar con AES (a la vez que mal documentadas), pero bueno, en esta ocasión la librería está en C, así que bendito problema.

 

Tras hacer algunas pruebas me percaté de que los archivos presentaban tamaños irregulares, por lo que tardaba más en descifrar algunos que en descifrar otros. Por lo que desde mi punto de vista, el truco estaba en comenzar a descifrar haciendo uso de una combinación un bloque de un archivo, y si dicho texto descifrado no era legible (es decir no contenía un user-ID) pasar a la siguiente combinación (no continuando con el descifrado total del archivo para dicha combinación). Más allá de esto no pensé en alguna otra mejora, que posiblemente las haya (podéis dejarlas en los comentarios), porque tampoco andaba muy sobrado de tiempo.

 

Comprobé que el script de test lo superaba en apenas 1 segundo, y que el script final tardaba en torno a un par de minutos (dado que presentaba unos inputs bastante tochos). Me daba por satisfecho y me dispuse a realizar el desafio 12.

 

Nota: Gracías a @Uxio0 he podido averiguar otro de los trucos que aceleraban el proceso: leer los timestamps en texto plano, para así solo tener que realizar las pruebas de descifrado sobre los archivos con mayor timestamp. Esto probablemente hubiese acelerado el proceso de un par de minutos a un par de segundos.

 

Os dejo mi código en C++: 

/* C HEADERS */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <openssl/aes.h>


/* C++ HEADERS */
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <bitset>
#include <complex>

using namespace std;


int main(){

    string tmp,tmp2,tmp3;

    while(cin>>tmp){

        int T;

        tmp.erase(tmp.end()-1, tmp.end());
        T=stoi(tmp);

        string salida;

        unsigned char userID[16];
        unsigned char userKey[33];

        vector<pair<int, int> > events;


        bool end = false;
        while(!end){

            cin>>tmp;
            if(tmp.back()!=';'){ //Ultima iteracion
                end = true;
            }else{
                tmp.erase(tmp.end()-1, tmp.end());
            }

            size_t found = tmp.find(",");
            tmp2 = tmp.substr(0, found);
            strcpy((char*)userID, tmp2.c_str());

            tmp3 = tmp.substr(found+1, tmp.size());
            strcpy((char*)userKey, tmp3.c_str());


            string filepath("./encrypted/");
            filepath += tmp2.substr(tmp2.size()-2, tmp2.size());
            filepath += string("/");
            filepath += string((char*)userID);
            filepath += string(".feed");

            ifstream file(filepath.c_str(), ios::in|ios::binary|ios::ate);
            if (file.is_open()){

                streampos size = file.tellg();
                unsigned char * entrada;
                entrada = new unsigned char [size];
                file.seekg (0, ios::beg);
                file.read ((char*)entrada, size);
                file.close();


                AES_KEY decKey;

                for(int c1=65; c1<=122; c1++){
                    for(int c2=65; c2<=122; c2++){
                        for(int c3=65; c3<=122; c3++){

                            if(c1==91)
                                c1=97;

                            userKey[29] = c1;
                            userKey[30] = c2;
                            userKey[31] = c3;

                            int flag=0;
                            char * ptr;
                            unsigned char out[32000];
                            for(int i=0; i<size; i+=16){//Descifra cada bloque
                                AES_set_decrypt_key(userKey, 256, &decKey);
                                AES_ecb_encrypt(&entrada[i], &out[i], &decKey, AES_DECRYPT);
                                if(!flag){
                                    flag=1;
                                    ptr = strstr((char*)&out[0], (char*)&userID[0]);
                                    if(ptr<=0){
                                        break;
                                    }
                                }
                            }
                            out[size]=0;

                                ptr = strstr((char*)&out[0], (char*)&userID[0]);
                                if(ptr>0){

                                    string dcp((char*)out);
                                    string buf;
                                    stringstream ss(dcp);

                                    vector<string> tokens;
                                    string uID((char*)userID);

                                    while (ss >> buf)
                                        tokens.push_back(buf);

                                    for(int j=0; j<tokens.size(); j++){
                                        if(tokens[j] == uID){

                                            int timestamp = stoi(tokens[j+1]);
                                            int event = stoi(tokens[j+2]);
                                            pair<int,int> p = make_pair(timestamp, event);
                                            events.push_back(p);
                                            j+=2;
                                        }
                                    }

                                    c1=122; c2=122; c3=122;
                                }

                        }
                    }
                }

                delete[] entrada;
            }

        }


        sort(events.begin(), events.end());
        vector<pair<int,int>>::iterator it;
        int n=0;
        for (it=events.end()-1; it>=events.begin()&&n<T; --it){
            std::cout << it->second;
            n++;
            if(it-1>=events.begin()&&n<T)
                cout << ' ';
        }

        cout << endl;

    }

    return 0;
}

Como veis, después realizo la ordenación de los eventID, en función de su timestamp, en orden descendente. Para ello utilizo un vector de pares de enteros, y la función sort() de la librería "algorithm" de STL.

Challenge 12 - Taxi Driver

 

Turno para el problema 12, en el que un joven Robert De Niro nos presenta un problema de encontrar la ruta más corta desde un origen a un destino, a través de un mapa de obstáculos. El problema que su taxi solo gira a la derecha (no me mezcles a 'Speed' con 'Taxi Driver primer aviso xd).

 

Es un programa de tipo laberinto (o eso pensaba yo) que intenté resolverlo haciendo uso de una búsqueda en anchura con el algoritmo de Lee http://en.wikipedia.org/wiki/Lee_algorithm y algunas modificaciones.

 

Finalmente no conseguí hacer que llegara a funcionar y me quedé sin tiempo para más. De todos modos os dejo el código para que os hagais una idea de lo que intentaba conseguir:


#define RIGHT 11
#define LEFT 22
#define DOWN 33
#define UP 44



class TaxiSolver{

    private:

        int M,N;

        int Si,Sj; //Start
        int Xi,Xj; //Destination

        int casilla[101][101];

        int distancia;

        queue<pair<int, int>> cola;
        queue<int> dir; //Direccion
        queue<int> dist; //Distancia

    public:

        void readCity(){

            cin >> M;
            cin >> N;

            //Realizamos la lectura del tablero
            char input;
            for(int n=0; n<N; n++){
                for(int m=0; m<M; m++){

                    cin >> input;

                    if(input == '.'){
                        casilla[n][m] = 0;
                    }else if(input=='#'){
                        casilla[n][m] = 999999;
                    }else if(input=='S'){
                        Si = n;
                        Sj = m;
                        casilla[n][m] = 0;
                    }else if(input=='X'){
                        Xi = n;
                        Xj = m;
                        casilla[n][m] = -1;
                    }
                }
            }
        }

        void printCity(){

            for(int n=0; n<N; n++){
                for(int m=0; m<M; m++){

                    if(n==Si && m==Sj){
                        cout << 'S';
                    }else if(n==Xi&&m==Xj){
                        cout << 'X';
                    }else if(casilla[n][m] == 0){
                        cout << '.';
                    }else if(casilla[n][m]==999999){
                        cout <<  '#';
                    }else if(casilla[n][m]>0){
                        cout << casilla[n][m];
                    }
                }
                cout << endl;
            }
            cout << "\n" << endl;
        }

//        void printPath(){
//          }


        int insert_in(int i, int j, int num){

            if(casilla[i][j]==-1){
                pair<int,int> s = make_pair(i, j);
                cola.push(s);
                dist.push(num);
                return 1;

            }else if(i>=0 && j>=0 && i<N && j<M){
                if(casilla[i][j]==0){
                    casilla[i][j]=num;
                    pair<int,int> s = make_pair(i, j);
                    cola.push(s);
                    dist.push(num);
                    return 1;
                }else if(casilla[i][j]==999999){
                    return 999999;
                }
            }
            return 0;
        }


        void insert_up(int i, int j, int num){
            i--;
            int r=insert_in(i,j,num);
            if(r== 1){
                dir.push(UP);
            }else if(r == 999999){

                //dir.push(RIGHT);
            }
        }

        void insert_down(int i, int j, int num){
            i++;
            if(insert_in(i,j,num)==1)
                dir.push(DOWN);
        }

        void insert_right(int i, int j, int num){
            j++;
            if(insert_in(i,j,num)==1)
                dir.push(RIGHT);
        }

        void insert_left(int i, int j, int num){
            j--;
            if(insert_in(i,j,num)==1)
                dir.push(LEFT);
        }

        int shortestPath(){

            int distancia=1;

            //Introducimos la posicion inicial
            insert_up(Si,Sj,distancia);
            insert_down(Si,Sj,distancia);
            insert_right(Si,Sj,distancia);
            insert_left(Si,Sj,distancia);

            distancia++;

            //printCity();

            while (!cola.empty()){

                distancia = dist.front()+1;
                dist.pop();

                int i = cola.front().first;
                int j = cola.front().second;
                cola.pop();

                int direccion = dir.front();
                dir.pop();

                if(casilla[i][j]==-1){
                    return distancia-1+8;

                }else{

                    if(direccion==UP){
                        insert_up(i,j,distancia);
                        insert_right(i,j,distancia);

                    }else if(direccion==DOWN){
                        insert_down(i,j,distancia);
                        insert_left(i,j,distancia);

                    }else if(direccion==RIGHT){
                        insert_down(i,j,distancia);
                        insert_right(i,j,distancia);

                    }else if(direccion==LEFT){
                        insert_up(i,j,distancia);
                        insert_left(i,j,distancia);
                    }
                    distancia++;
                    //printCity();
                }
            }

            return -1;
        }
};


int main(){

    int T;
    cin >> T;

    for(int t=0; t<T; t++){

        TaxiSolver mySolver;

        mySolver.readCity();

        //mySolver.printCity();

        int path = mySolver.shortestPath();

        if(path<0)
            cout << "ERROR" << endl;
        else
            cout << "Case #" << t << ": " << path << endl;

    }

    return 0;
}

Creo que dadas las restricciones del problema mi planteamiento inicial fue erróneo, y si volviese a abordar el problema seguramente haría uso de mi algoritmo A* previamente programado. Además en este caso sería muy fácil programar una buena heurística para el problema y que fuera óptima (el propio algoritmo de Lee podría servir de base para una buena heurística).

Fin del juego, y conclusión


Desgraciadamente no me dio tiempo a más, y aquí terminó mi primera participación en el Tuenti Challenge. Mi posición provisional final fue cincuenta y algo (no la recuerdo exactamente), y si no hubiese perdido tanto tiempo en el problema 10 o al menos lo hubiese podido resolver, seguramente hubiese podido subir algún puesto más arriba.


El concurso la verdad es que me ha sorprendido positivamente, me lo esperaba más cutre (para ser honesto mucho más cutre), y me he llevado una verdadera sorpresa. Es más, durante estos días se celebran las primeras fases de la Codejam 2014 y los problemas de la Tuenti Challenge me han parecido mucho más logrados y originales (si bien es cierto que el de Google utiliza un formato de tiempo reducido), y creo que le falta algo más de difusión para lo currado que esta.
Si no recuerdo mal en su momento recibí un correo de la universidad, pero creo que esta labor de propaganda del desafío se puede mejorar, y que llegue a oídos de un montón de personas que de haberlo conocido se hubiesen interesado por él sin ninguna duda.


Por mi parte, el año que viene si las circunstancias laborales/personales me lo permiten espero estar nuevamente en la Tuenti Challenge 2015 y en esta ocasión para intentar llegar aún más arriba.


Nos vemos


Tuenti Challenge 2014 -5ª Parte

Challenge 10 - Random Password


Y llegamos, al que en mi caso se puede considerar como el GRAN MURO del concurso. 3 días. 3 días dedicados al problema, para finalmente tener que pulsar el dichoso botón de SKIP. Y lo que es peor, con la sensación de estar a las puertas de solucionar el problema, y no comprender el porqué de no ser capaz de entrar en la fortaleza. Frustrante cuanto menos.

Pero como dijo Jack El destripador "Por partes vamos mejor" (sí, la he modificado, para darle un toque más andaluz xd), así que pasaré a relatar mi odisea por el desierto, que nada tiene que envidiar a la de Moisés xd.
El problema pertenecía al conjunto de problemas que podríamos englobar como de seguridad-astucia, dado que no requieren altos conocimientos de seguridad informática, sino unos conocimientos básicos acompañados de una buena cantidad de astucia (y para que vamos a negarlo, de suerte).

Se nos daba la siguiente web http://random.contest.tuenti.net/?input=INPUT, a la que tras entrar en ella podíamos ver un único mensaje de texto que nos saludaba con "Wrong". Rapidamente uno podía echar un vistazo a la URI y fijarse en que tenemos como argumento de entrada la palabra "INPUT" para el parámetro "input". Lo primero que a cualquiera se le pasa por la cabeza es probar otros argumentos como "hola", "adios", "123456", "root", o incluso dejarlo vacío. 

 

Tras ver que nada funciona, y echar un pequeño vistazo al test-script de los chicos de tuenti, pude ver como este nos daba una entrada que claramente iba dirigida a introducir en dicho input. Por lo tanto, nuestro objetivo no debía ser realizar ningún cambio en este, sino que más bien los tiros debían ir por otro lado.

 

El siguiente paso fue trastear un poco la web. Para ello comencé invocandola sin ningún parámetro: "http://random.contest.tuenti.net". El resultado idéntico, el puñetero mensaje de Wrong. Por lo que probé a invocar "http://random.contest.tuenti.net/index.html" y con ello llegó la primera revelación:


sabíamos ya por tanto el servidor http que utilizaba la máquina (nginx), pero lo que era mejor, la web nos indicaba aquellas páginas que existían en dicha máquina (en muchas ocasiones se suele desactivar, ya que muchos admins lo consideran como un 'bug' de revelación de información), y por tanto lo podíamos aprovechar para encontrar otros archivos en la máquina.

 

Lo primero averiguar el nombre de la página que nos estaba sirviendo el mensaje_de_las_narices "wrong": index.php. Vamos que se había dejado por defecto. A continuación probar otras muchas opciones como "robots.txt", index[x].php, default.php, admin.php... Incluyendo otras muchas variedades como búsqueda de logs y directorios fuera del directorio raiz. Pero nada, mis resultados no tuvieron sus frutos.

 

Decidí tomarme un descanso, y echarle un vistazo a la estructura de directorios del servidor Nginx. Una búsqueda rápida en google, unos cuantos ejemplos de servidores mal configurados y al rato volvía con ideas frescas. Pero nuevamente los intentos fueros inútiles. Pensé por tanto, que quizás la gente de Tuenti esperase una explotación del sistema en toda regla. Por lo que comencé mi búsqueda de exploits para nginx: CVE-2013-2028 Nginx HTTP Server 1.3.9-1.4.0 Chunked, CVE-2014-0133, CVE-2013-2070, en concreto este último viable en la versión 1.1.19, y fácil de explotar. Tras comprender un poco más como llevarlo a cabo, e intentar replicarlo contra el servidor, nuevamente en una encrucijada sin salida. Nada había funcionado.

 

Pensé entonces en atacar PHP, para ello levanté Wireshark y me dispuse a ver con más detenimiento los paquetes que nuestro amado nginx nos enviaba:



¡Aja! ¡Ya te tengo! Teníamos a php 5.3.10 precompilado para ubuntu. Nuevo vistazo a las vulnerabilidades de PHP, nuevo intento de explotación, en esta ocasión acompañado de nuestro amigo MetaSploit, pero nuevo tropiezo y primeros momentos de desesperación del concurso.

 

Reflexioné. Me paré y pensé. Intenté utilizar los conocimientos que durante la carrera de informática había aprendido: matemáticas discretas, algebra, estadística, redes neuronales... Mierda, estaba perdido

 

Estaba claro que nada de esto me iba a ayudar lo más mínimo ante un reto como este. Pero había algo que sí que había aprendido durante la carrera, y que precisamente no me lo habían enseñado los profesores, y es que los alumnos que tienen interés por la programación y el desarrollo, suelen no tener mucho interés por la telemática y la seguridad, y a la inversa (para atentar contra mi persona los comentarios chicos/as). Y realmente esto se puede seguir extrapolando al ámbito profesional, donde la mayoría se encasilla claramente en una categoría, y suele pillarle un poco lejana la otra rama. ¿Y por qué iba a ser diferente para este caso? A fin de cuentas la gran mayoría de los desafíos tienen una alta carga algorítmica, por lo que seguramente sus autores hayan escrito un reto de seguridad "light". Olvidémonos de ASLR, DEP y demás lindezas y volvamos a un terreno más cercano al de un programador.


Así que nuevamente  a probar alternativas sobre la URL: algún "directory traversal", algún truquito de la vieja escuela, y por fin a la tercera fue la vencida: PHP Source era la clave. Estos archivos con código fuente php, que su utilizan para poner tu código maquetado con colorines y que todo el mundo pueda ver lo buen programador que eres, tenían la solución a nuestros problemas.

"Index.phps" El archivo que acababa con toda una tarde de búsqueda infructuosa, y que nos abría las puertas del cielo (o más bien del infierno viendo lo que se me venía encima). 

De acuerdo, el código era muy simple, hasta para alguien como yo con un mediocre nivel de PHP. Al comienzo se generaba la semilla ('srand'), para el número aleatorio que se generaba al final ('rand') y que se comparaba con la contraseña, que debíamos pasar mediante el parámetro 'password'.

Pero recordemos que cuando hablamos de aleatoriedad, en realidad hacemos referencia a pseudo-aleatoriedad, y cuantas veces habremos oído repetido eso de que a la función de inicialización de aleatoriedad solo hay que llamarla una vez, dado que si no nuestra secuencia aleatoria se hace previsible (dado que si es la única semilla siempre producirá la misma secuencia para dicha semilla).

Pues bien, para colmo además, el fallo se produce en un servidor web, donde cada vez que invoquemos a index.php, el servidor generará un proceso hijo (o hebra) para atender a dicha petición, y por tanto se generará en cada una de nuestras peticiones http una nueva semilla.

 

Ahora solo queda averiguar el valor de dicha semilla, que se forma con la hora en formato timestamp de Unix (con los segundos a 0) multiplicado por el PID del proceso padre. La hora podría ser un problema, ya que el servidor realmente podría tener la que le diese en gana, pero para mayor facilidad han incluido en la cabecera http la hora del servidor. Por tanto solo nos queda ya averiguar el PID del proceso padre.

 

Ya que antes hemos visto que se trata, de un servidor ubuntu (es decir linux), seguramente el PID del padre se encuentre entre los 10000 primeros, por lo que la búsqueda no debería ser demasiado complicada. Por fuerza bruta probaremos los 10000 primeros PID, y a ver si tenemos suerte.

 

Para ello antes de nada, realicé las pruebas sobre mi propio servidor web, con el mismo código que había en el index.phps del servidor. En mi servidor web tardó apenas un segundo en encontrar la solución (lógico si pensamos que estaba en la propia máquina, y no hay retardos de red), con un PPID 6637. Ahora quedaba realizar la prueba contra el servidor web del desafió. Os dejo aquí el código php que utilicé para ello: 

<html>
 <head>
  <title>Por la fuerza</title>
 </head>
 <body>
 <?php 

/* LOCURA DE PROBLEMA XD */

//Evitamos que muestre los Warnings
error_reporting(E_ERROR | E_PARSE);

 
//$server = 'http://127.0.0.1/probe.php';
$server = 'http://random.contest.tuenti.net/';

$contents = file_get_contents($server.'?password=2036317783&input=326b5d287a');

for ($ppid = $_GET['i']; $ppid <= $_GET['f']; $ppid++){


 $pos = strpos($contents, "wrong!");

 if ($pos !== false){

  $nHeaders = count($http_response_header);
  for ($i = 0; $i <= $nHeaders; $i++){
   if(strpos($http_response_header[$i], "Date") !== false){
    break;
   }
  }

  $hora = substr($http_response_header[$i], 23, -10);
  $minuto = substr($http_response_header[$i], 26, -7);


  srand(mktime($hora, $minuto, 0) * $ppid);
  $rand = rand();

  echo $rand." ";

  $contents = file_get_contents($server.'?password='.$rand.'&input=326b5d287a');

 }else{
  echo "El ppid valido era ".($ppid-1)." fecha:".$hora." ".$minuto. "<br/>";
  echo $contents;
  break;
 }
}
 ?>
 </body>
</html>

El código era muy simple, solamente leía la hora y minuto que nos enviaba el servidor, y lo multiplicaba por un supuesto ppid que iba desde _GET(i) hasta _GET(f) (que eran pasados como argumentos). Después comprobaba si la respuesta que recibía contenía el mensaje 'Wrong' o no.

 

El mismo código funciono perfecto, sobre el mismo index.phps en mi servidor. Pero contra el servidor del concurso no tuve suerte. Intenté entonces probar los pid hasta 35000. Pero fallé nuevamente. Y empecé a mosquearme, y a pensar que mi aproximación tenía que ser erronea. A pensar que quizás el retardo pudiese estar estropeando mi solución. Pero lo descarté porque yo leía la hora actualizada para cada petición.

 

Me acordé entonces de una cosa que leí hace un tiempo sobre un módulo llamado Suhosin encargado de maximizar la seguridad en PHP. Y entonces pensé que probablemente la función srand() fuese diferente para diferentes versiones de PHP con diferentes características (era una de las características de Suhosin).

Me fuí entonces a http://sandbox.onlinephpfunctions.com/ y probé el siguiente código sobre 2 versiones distintas de PHP:


 

¡Lo que me esperaba! La función srand funciona de diferente forma en diferentes versiones de PHP, por lo que me esperaba intentar replicar las condiciones que tenía la máquina del servidor. Así que monté una máquina virtual, le metí Ubuntu y PHP 5.3.10 y probé nuevamente. Y nuevamente el vacío. Nada de nada. No sabía que podía estar fallando en una solución que creía haber encontrado. Me fuí a dormir, y como último intento dejé el programa toda la noche probando PPID entre 0-500000 con varias threads en paralelo, y con un poco de miedo por si provocaba un DOS en el servidor (nah, es coña).

 

Al día siguiente, el servidor seguía vivo (que alivio xd), pero ninguno de los PPID parecía valido.

 

Comenzaba entonces mi travesía por el desierto de intentos disparatados y desesperados, entre los que se incluían un escaneo completo con NMAP por si algún pequeño resquicio me daba algo. 

 

No encontré nada, pero después me enteré que intentando resolver este problema había resuelto el desafío 18 (que tenía que ver con realizar ingeniería inversa sobre un bytecode de python. Os dejo el código por si os interesa:

import json, sys
print 'Content-Type: text/txt\n'
q = sys.argv[-1]

def error(m):
    print m
    sys.exit()


if len(q) == 0:
    error('Input missing')
try:
    h, p = q.split(':')
except ValueError:
    error('Password missing')

try:
    i = json.load(open('../keys.json'))[h]
except:
    error('Invalid input')

print 'Right!' if p.isdigit() and len(p) < 15 and pow(i[0], int(p), i[1]) == i[2] else 'Wrong!\n' + '\n'.join(map(str, i))

 

Trataba de jugar con potencias y módulos (problema recurrente en programación competitiva), para de nuevo obtener una clave en un archivo "keys.json".

 

Por supuesto también llegué al problema de la calavera, antes de haber leído su anunciado, en mi proceso de exploración. Y como eso muchas otras cosas que aprendí acerca de los servidores de Amazon en mi camino de búsqueda. Pero nada que me sirviese para resolver el problema.

 

Tras 3 días de pruebas, me daba por vencido. Con la sensación de haber estado cerca pero resignándome a tener que saltarme dicho desafío, y sobre todo lamentándome por haber desperdiciado tanto tiempo en un solo problema.

 

Pd: Después pude saber gracias a @dvil88 que mi enfoque era correcto y que el PPID era 1336, no sé si fijado también con premeditación y utilizando "argot haxor".

Desafío 11  http://programacioncompetitiva.blogspot.com.es/2014/05/tuenti-challenge-2014-6-parte.html

lunes, 5 de mayo de 2014

Tuenti Challenge 2014 -4ª Parte

Challenge 8 - Tuenti Restructuration

El problema 8 comenzaba contándonos los planes de re-estructuración que el jefe de personal de Tuenti tenía en mente. Dichos cambios estaban apuntados en una tabla de 3x3 que se correspondía con la oficina, y cada una de esas casillas con una de las mesas de los empleados.

Pues bien, para llevar a cabo la mudanza de escritorio se había pensado en. que los empleados se intercambiaran su mesa con el compañero que tuviera adyacente, llevándose a cabo un cambio por día. Al final del proceso, tras un periodo de X días, la disposición final de los compañeros debe ser tal cual, el jefe había planificado en su tabla (también se menciona que la casilla central debe estar vacía).

El objetivo es encontrar el número mínimo de días necesario para llevar a cambio la mini-mudanza, dado como entrada la situación original, y la situación final deseada.

Nada más comenzar a leer el problema no podía evitar acordarme de dos tipos a los que seguro que muchos de vosotros conoceis:


Quizás estas caras no os digan nada, pero si os digo el título de uno de sus libros seguro que algo os suena: 
"Artificial Intelligence: A modern approach" , en español "Inteligencia Artificial: Un enfoque moderno". 

Este libro es a día de hoy, el más utilizado en las universidades de todo el mundo para enseñar a sus alumnos las bases de la inteligencia artificial. Y aún recuerdo, cuando hace apenas 2 años lo estudiaba, y en uno de sus capítulos aparecía la siguiente figura:


Se trata ni más ni menos del juego del 8-puzzle, el hermano menor del 15-puzzle, y en el libro era uno de los ejemplos principales de mejora de rendimiento al utilizar heurísticas óptimas con A*. Se explicaban los fundamentos del algoritmo A*, sus variantes, y se explicaban la creación y elección de una heurística apropiada. Para quien quiera conocer algo más acerca de A* visitar http://es.wikipedia.org/wiki/Algoritmo_de_b%C3%BAsqueda_A*

A grosso modo, se trata de un algoritmo greedy, que elige en cada momento aquella opción con un menor coste f(n), donde f(n) = g(n)+h(n), siendo g(n) la distancia actual recorrida (o el coste verdadero), y h(n) la distancia esperada si tomamos esa decisión (esperada porque es una heurística). A cambio, el coste que pagamos por elegir un algoritmo heurístico es que aún obteniendo una solución de mucha calidad, puede no ser óptima.

Pero este aspecto está también cubierto para el caso de A*, y bajo 2 condiciones una heurística ofrecerá un resultado óptimo:
  • Que sea una heurística admisible, es decir, que en ningún momento piense que el valor de una elección es mayor que el que realmente es (que siempre piense de forma optimista).
  • Que cumpla la desigualdad triangular, lo que se traduce en que el coste para llegar desde un nodo a otro nodo, siempre debe ser menor que la suma de llegar a este nodo pasando por otro y desde este al buscado, si estos sumados nos dan un coste mayor. 
Es importante remarcar que todo esto se engloba dentro de una búsqueda en arboles, por lo que la elección de una heurística de calidad tendrá un peso decisivo en el factor de ramificación del árbol de búsqueda (una mejor heurística proporcionara un árbol mucho más reducido). Por tanto, cuanto mejor sea la heurística menor tiempo de ejecución, y sobre todo, menos gasto de memoria, ya que en el algoritmo A* el gran limitante es el consumo de memoria al guardar todos los nodos previamente generados (hay variantes de A* que sacrifican rendimiento a favor de un consumo menor de memoria).

Las heurísticas son por tanto dependientes del problema, y deben de ser estudiadas cuidadosamente para el problema en cuestión que estemos tratando. Dentro de las heurísticas más usuales para resolver problemas de tipo 8-puzzle/15-puzzle podemos encontrar:
  • Número de casillas discordantes
  • Distancia de Manhattan
  • Conflicto lineal
  • Patrones disjuntos
Aparecen ordenadas de menos eficientes a más eficientes (desgraciadamente también de menos complejas de implementar a más complejas de implementar).

Bueno, una vez soltado esta "intro", abordaré como afronté este problema.

En primer lugar, antes de lanzarse a implementar nada, es importante reflexionar acerca de las herramientas y ayudas previas con las que contamos. Si por ejemplo, tenemos una contenedor de tipo "stack" no tiene sentido implementar nuestra propia pila. Además en C++ contamos tanto con STL (dentro del standard), como con Boost (que amplia vastamente las posibilidades de la STL).

Sin embargo, a la hora de tratar con estructuras de datos como son los arboles, no existe una librería de uso general a la que acudir por antonomasia, dado que las implementaciones de un árbol suelen ser bastante dependientes del problema, por lo que se suele implementar para el propósito concreto.

En mi caso, hace algún tiempo atrás había implementado el algoritmo A*, pero no disponía a día de hoy de dicho código. Por tanto opté, por implementarlo desde 0 y de repaso pegarle un repaso a algunos conceptos que tenía un poco oxidados.

A continuación el código:


/* C++ HEADERS */
#include <iostream>
#include <fstream>
#include <array>
#include <deque>
#include <forward_list>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <algorithm>
#include <bitset>
#include <complex>
#include <iterator>
#include <utility>

using namespace std;

int NODOS=0;


//Tuenti Challenge 8:
//  Problema de tipo 8-puzzle con strings
/*-----------------------------------------------------------------------------------------------*/
/*---------------- SOLUCION CON A ESTRELLA Y HEURISTICA DE MANHATTAN MODIFICADA -----------------*/
/*-----------------------------------------------------------------------------------------------*/

template <class T>
class node{

    private:
    //Cada nodo tendrá su propio contenido
    T _data;

    //Puntero al nodo padre
    node * _parent;

    //Apuntamos a los nodos hijos de dicho nodo
    vector<node *> _child;

    int _gn; //g(n) = profundidad de dicho nodo

    //Función a la que se llama para generar los hijos de un nodo
    void (*funcPtr)();

    public:

        node(){
            _parent=0;
            _child.reserve(12); //12 para este caso
            _gn = 0;
        }

        void insert(const T &in){
            _data = in;
        }

        T data(){
            return _data;
        }

        node * parent(){
            return _parent;
        }

        void printPath(){
            node * nodo = this;
            cout << nodo->_data << endl;
            while(nodo->_parent!=0){
                nodo = nodo->_parent;
                cout << nodo->_data << endl;
            }
        }

        int gn(){
            return _gn;
        }

        node * child(int n){
            return _child[n];
        }

        node<T>* addChild(T data){
            node<T> *nodo = new node;
            nodo->insert(data);
            nodo->_parent = this;
            for(int i=0; i<12; i++)
                nodo->_child[i] = 0;
            nodo->_gn = _gn+1; //Aumentamos en 1 la profundidad del recorrido
            _child.push_back(nodo);
            return nodo; //Devolvemos una referencia al nodo insertado
        }


};

template <class T>
class link{

    private:

    public:

        node<T>* ptr;
        int _fn;

};

bool operator<(const link<int>& op1, const link<int> op2){
            return op2._fn < op1._fn;
}


template <class Te>
class tree{

    private:

        node<Te> root;

        node<Te> * nodoActual;

        int (*genFunction)(Te&, const int);//Función sucesor

        int (*hFunction)(const Te&, const Te&);//h(n)

        priority_queue<link<Te>> liveNodes;

        int best=-1;

        node<Te>* addNode(Te data){
            NODOS++;
            return nodoActual->addChild(data);

        }


        void recursiveFree(node<Te> *nodo){

            for(int i=0; i<12; i++){
                if(nodo->child(i) == 0){
                    delete nodo;
                    //nodo = nodo->parent();
                    break;
                }else{
                    recursiveFree(nodo->child(i));
                }
                //nodo = nodo->parent();
            }
        }

    public:

        tree(const Te &data){
            root.insert(data);
            nodoActual = &root; //Nodo Actual = nodoRaiz
        }

        ~tree(){
            node<Te> *nodo = &root;
            recursiveFree(nodo);
        }

        //Función a la que se llama para generar cada nodo (Función Sucesor)
            //Como primer parámetro se pasa el contenido de dicho nodo
            //Como segundo parámetro se pasa el numero de iteración en dicho nivel(comenzando por 1)
        void generationFunction(int (*funcPtr)(Te&, const int)){
            genFunction = funcPtr;
        }

        //Función heurística a utilizar en la búsqueda A*
            //Como primer parámetro se pasa el estado actual
            //Como segundo parámetro se pasa el estado objetivo
        void heuristicFunction(int (*funcPtr)(const Te&, const Te&)){
            hFunction = funcPtr;
        }

        //Realiza una búsqueda A* sobre dicho arbol, encontrando la solución óptima
        int Astar(Te searchFor){

            //Si no quedan más nodos para expandir no existe solución

            //Llamamos a la función 'genFunction' sobre el nodo actual
            Te data = nodoActual->data();

            if(data == searchFor){
//                cout << "Encontrado con numero de movimientos : " << nodoActual->gn() << endl;
//                nodoActual->printPath();
                best = nodoActual->gn();
                return best;
            }

//            if(NODOS>500000){
//                best = 14;
//                return best;
//            }

            for(int n=1; genFunction(data, n) != -1; n++){

                link<Te> tmpLink;
                tmpLink.ptr = addNode(data); //Añadimos cada uno de los nodos hijos
                tmpLink._fn = hFunction(data, searchFor) + nodoActual->gn() + 1; //f(n) = h(n)+g(n)

                liveNodes.push(tmpLink); //Añadimos un puntero al nuevo nodo hijo, junto con su valor f(n) a la cola con prioridad

                data = nodoActual->data();
            }

            //link<Te> debug = liveNodes.top();

            if(!liveNodes.empty()){


                //Una vez generados todos los nodos del nivel inferior, elegimos el nodo vivo con menor valor f(n)
                //mediante una consulta a nuestra cola con prioridad, y cambiamos el nodoActual por dicho nodo

                nodoActual = liveNodes.top().ptr;


                //Eliminamos dicho enlace de la cola con prioridad
                liveNodes.pop();

                //debug = liveNodes.top();
                //cout << debug.ptr->data() << endl;

                Astar(searchFor);
            }


            //Devolvemos el valor encontrado
            return best;
        }

};

//La funcion debe devolver -1 cuando haya realizado la ultima generación
int exchange(int &num, int numLlamada){

    string tmp = to_string(num);

    if(numLlamada == 1){
        swap(tmp[0],tmp[1]);
    }else if(numLlamada == 2){
        swap(tmp[1],tmp[2]);
    }else if(numLlamada == 3){
        swap(tmp[3],tmp[4]);
    }else if(numLlamada == 4){
        swap(tmp[4],tmp[5]);
    }else if(numLlamada == 5){
        swap(tmp[6],tmp[7]);
    }else if(numLlamada == 6){
        swap(tmp[7],tmp[8]);


    }else if(numLlamada == 7){
        swap(tmp[0],tmp[3]);
    }else if(numLlamada == 8){
        swap(tmp[3],tmp[6]);
    }else if(numLlamada == 9){
        swap(tmp[1],tmp[4]);
    }else if(numLlamada == 10){
        swap(tmp[4],tmp[7]);
    }else if(numLlamada == 11){
        swap(tmp[2],tmp[5]);
    }else if(numLlamada == 12){
        swap(tmp[5],tmp[8]);
    }else if(numLlamada == 13){
        return -1;
    }

    num = stoi(tmp);
    return 1;
}


//Función heurística para el problema basada en la distancia de Manhattan (dividida por 2 dadas las caracteristicas del problema)
int Manhattan(const int &confActual, const int &confBuscada){

    int distanciaM = 0;

    int origen[3][3];
    int destino[3][3];

    string tmp = to_string(confActual);
    int k=0;
    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){
            origen[i][j] = tmp[k] - '0';
            k++;
        }
    }

    tmp = to_string(confBuscada);
    k=0;
    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){
            destino[i][j] = tmp[k] - '0';
            k++;
        }
    }

    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){

            for(int l=0; l<3; l++){
                for(int k=0; k<3; k++){

                    if(origen[i][j] == destino[l][k]){
                        //Calculamos distancia
                        int dist = abs(l-i) + abs(k-j);
                        distanciaM += dist;
                        l=3; k=3;
                    }
                }
            }
        }
    }

    int valor;

    if(distanciaM > 2)
        valor = (distanciaM/2) + (distanciaM/12);
    else
        valor = (distanciaM/2);
    return valor;
}

//Función heurística para el problema basada en el numero de piezas mal colocadas
int MalColocadas(const int &confActual, const int &confBuscada){

    int mal = 0;

    int origen[3][3];
    int destino[3][3];

    string tmp = to_string(confActual);
    int k=0;
    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){
            origen[i][j] = tmp[k] - '0';
            k++;
        }
    }

    tmp = to_string(confBuscada);
    k=0;
    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){
            destino[i][j] = tmp[k] - '0';
            k++;
        }
    }

    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){

            if(origen[i][j] != destino[i][j]){
                mal++;
            }
        }
    }

    return mal/2;
}



void miFuncion(int &number){
    number++;
}

int encodeTable(int table[3][3]){

    int entero = 0;
    entero += table[0][0]*100000000;
    entero += table[0][1]* 10000000;
    entero += table[0][2]*  1000000;
    entero += table[1][0]*   100000;
    entero += table[1][1]*    10000;
    entero += table[1][2]*     1000;
    entero += table[2][0]*      100;
    entero += table[2][1]*       10;
    entero += table[2][2]*        1;
    return entero;
}


class parseTables{

    private:

        string origen[3][3];
        string destino[3][3];

        //Tabla hash que asocia cada numero con su correspondiente string
        unordered_map<int, string> hash_itos;

        //Tabla hash que asocia cada string con su correspondiente numero
        unordered_map<string, int> hash_stoi;


        void readTable(string table[3][3]){

        string name;

        for(int i=0; i<3; i++){
            for(int j=0; j<3; j++){
                cin >> name;
                if(name.find(',')!=string::npos)
                    name.erase(name.find(','),1);
                table[i][j] = name;
//                cout << name << " " ;
            }
//            cout << endl;
        }
        table[1][1] = "*"; //Elemento central

        }

    public:

        void readTables(int orig[3][3], int dest[3][3]){

            readTable(origen);
            readTable(destino);

            //Sustituimos cada string por un digito del 1 al 9
            for(int i=0; i<3; i++){
                for(int j=0; j<3; j++){
                    int digito = 3*i + j + 1;
                    orig[i][j] = digito;
                    hash_itos[digito] = origen[i][j];
                    hash_stoi[origen[i][j]] = digito;
                }
            }

            //Realizamos dicha sustitucion tambien en la tabla destino
            for(int i=0; i<3; i++){
                for(int j=0; j<3; j++){
                    auto it = hash_stoi.find(destino[i][j]);
                    dest[i][j] = it->second;
                }
            }
        }

};


int main(){

    int T;
    cin >> T;

    for(int t=0; t<T; t++){

        int origen[3][3];
        int destino[3][3];

        parseTables myParser;

        myParser.readTables(origen, destino);

        tree<int> arbol(encodeTable(origen)); //El nodo raiz contiene el valor 5

        arbol.generationFunction(exchange); //Fijamos la función de generación de nuevos nodos

        arbol.heuristicFunction(Manhattan); //Fijamos la función heurística = h(n)
        //arbol.heuristicFunction(MalColocadas);

        cout << arbol.Astar(encodeTable(destino)) << endl;

    }
}

Quizás tendría que haber escogido alguna otra modificación de A* como BRPM, o similares con el fin de minimizar la cantidad de nodos en memoria, pero tras algunos ajustes a la heurística la cantidad de memoria utilizada me pareció razonable.

En cuanto a la estrutura de la clase he de reconocer que todavía esta en pañales porque la he creado a toda prisa expresamente para este problema. Pero la idea es que tuviera una estructura lo suficientemente genérica como para poder re-aprovecharla en futuros problemas similares.

Con ese fin he creado los métodos generationFunction(), y heuristicFunction(), los cuales toman como parámetros la respectiva función de generación y función heurística a utilizar por la clase A*, en este caso las funciones exchange y manhattan (esta también codificada una heurística Mal_Colocadas que se corresponde con una heurística de número de casillas discordantes, pero obviamente su rendimiento era mucho peor).

El tema de la heurística, comentar que es una modificación de la distancia de Manhattan, la cual hay que dividir por 2 en este caso para que siga siendo óptima, dado que los intercambios que propone dicho problema difieren del concepto original de 8 puzzle en el que se mueve una ficha cada vez. Queda pendiente para un futuro implementar una heurística de bases disjunta (quizás el año que viene?).

Nota: Otra opción que he visto que ha utilizado @Landertxu0 es la siguiente: dado que tenemos solo un tablero de tamaño 3x3 (esto nos da un total de 9! = 362880 posiciones distintas en el tablero), precalcular la distancia a cada una de estas, almacenarlas y después utilizarla para cada uno de los casos que se nos pedían. Esta técnica se conoce como precomputación y me parece una solución fantástica dado el tamaño del problema. Después podemos obtener cada solución en un tiempo constante. Podéis consultar su descripción original en http://www-ma2.upc.edu/lramos/wp-content/uploads/2014/05/writeup.pdf

Queda en el tintero una posible solución con A* + precomputación. Si alguno de los que me leéis habéis utilizado dicha solución, agradecería mucho que lo comentarais.

@Landertxu0 además sugiere una búsqueda bidireccional como otra posible solución al problema.

Precisamente la idea de utilizar una búsqueda bidireccional para problemas de tipo 8-puzzle también aparece en el libro de Russell y Norvig:


Si bien es cierto que la eficiencia de una búsqueda bidireccional es bastante más pobre que la de un A* con una heurística adecuada: O(b^d/2), siendo 'd' la profundidad de la solución y 'b' el factor de ramificación.

Challenge 9 - Bendito Caos

Un enunciado inicialmente muy confuso, o por lo menos esa fue la impresión que inicialmente me dió. El problema nos comentaba la situación de un joven que quería montar una fiesta para los habitantes de las ciudades cercanas. Y para tener el suficiente avituallamiento para todos, debía de calcular la cantidad de personas que podrían llegar teniendo en cuenta las limitaciones de la carretera.

Al principio creí que se trataba de un problema de teoría de colas, pero cuando por fin pude comprenderlo en su totalidad (porque el enunciado se las traía), lo oriente simplemente como un problema de restricciones en grafos.

Vayamos al tema del enunciado: 

  • Se nos daba el número de ciudades siento todas ellas independientes en sus conexiones, y siendo subproblemas distintos. Hasta aquí todo bien.

     

    Se nos explicaba que había 2 tipos distintos de carreteras, cada una con un tipo de velocidad distinta. Vale.

     

    Ahora llega la primera duda que me surgió: se especificaba que las carreteras tenían uno o dos carriles unidireccionales "Each road may have one or two lanes", sin embargo en los ejemplos de prueba se daban carreteras de 1,2 o 3 carriles.

     

    Después se nos explicaba el concepto de intersección, pero a mi gusto era un poco superficial la explicación que se daba, y a continuación se nos explicaba que las carreteras estaban definidas "desde-hasta", es decir por un origen y un final, pudiendo ser estos una intersección o una ciudad. Aquí la verdad que me costo un poco hasta que pude comprender plenamente el formato de los archivos de entrada.

     

    Por último lo que más claro quedaba era que los coches median 4metros con 1metro entre cada uno de ellos. Por lo tanto 5metros por cada coche.

     

El problema ahora nos pedía que calcularamos el número de coches que podían viajar desde la ciudad de origen hasta AwesomeVille en una hora.


La cantidad de vehículos viene dada por la suma de las carreteras entrantes, y estas se ven limitadas por la carretera de menor velocidad que surte de coches a dicha carretera. Por tanto de primeras podríamos utilizar una búsqueda en profundidad recursiva y de este modo ir a través de las distintas intersecciones y calcular el cual es el limitante.


Pero este tipo de aproximación se torna extremadamente ineficiente (exponencial con respecto al número de nodos).


Desde mi punto de vista este problema encaja bien con un problema de satisfacción de restricciones, y en mi caso lo implementé como un grafo con propagación de restricciones. Este es mi código:


/* C++ HEADERS */
#include <iostream>
#include <vector>

using namespace std;

/*------------------ IMPLEMENTACION MEDIANTE UN GRAFO CON PROPAGACI�N DE RESTRICCIONES ------------------*/



class Edge{

    private:

    public:

        int origin;
        int destination;
        int vel;
        int lanes;

};


int main(){

    int C; //Number of cities
    cin >> C;

    for(int c=0; c<C; c++){

        string cityName;
        cin >> cityName;

        int S; //Maximum speed for roads
        int D; //Maximum speed for dirt roads

        cin >> S;
        cin >> D;

        int I; //Intersection numbers
        int R; //Road numbers

        cin >> I;
        cin >> R;

        vector<int> inter(I,0); //Guardamos el flujo de trafico entrante

        vector<Edge> edges;
        edges.reserve(R);

        string origin, destination, type, lanesNum;

        //Leemos cada una de las carreteras del mapa
        for(int r=0; r<R; r++){

                Edge edge;

                cin >> origin;
                cin >> destination;
                cin >> type;
                cin >> lanesNum;

                if(type == "normal")
                    edge.vel = S;
                else
                    edge.vel = D;

                edge.lanes = stoi(lanesNum);

                if(origin == "AwesomeVille"){
                    edge.origin = -1;
                }else if(origin == cityName){
                    edge.origin = -2;
                }else{
                    edge.origin = stoi(origin);
                }

                if(destination == "AwesomeVille"){
                    edge.destination = -1;
                }else if(destination == cityName){
                    edge.destination = -2;
                }else{
                    edge.destination = stoi(destination);
                }

                edges.push_back(edge);
        }


        //Calculamos ahora el valor total de entrada de todas las intersecciones
        for(int r=0; r<R; r++){
            if(edges[r].destination >= 0){ //Tiene como destino una interseccion
                if(edges[r].origin == -2){ //Si dicha arista parte de la ciudad visitante
                    inter[edges[r].destination] += edges[r].vel * edges[r].lanes;
                }
            }
        }

        //Iteramos mientra haya cambios en la propagacion de las restricciones
        bool modificado=true;
        while(modificado){
            modificado = false;
            for(int r=0; r<R; r++){
                if(edges[r].destination >= 0){ //Tiene como destino una interseccion
                    if(edges[r].origin >= 0){ //Y parte de una interseccion
                        int add = min(edges[r].vel * edges[r].lanes, inter[edges[r].origin]);
                        int ant = inter[edges[r].destination];
                        inter[edges[r].destination] = max(add, inter[edges[r].destination]);
                        if(ant != inter[edges[r].destination] )
                            modificado = true;
                    }
                }
            }
        }



        long long velTotal = 0;
        for(int r=0; r<R; r++){

            //Solo tenemos en cuenta las carreteras con destino a "AwesomeVille"
            if(edges[r].destination == -1){

                //Si es una carretera directa
                if(edges[r].origin == -2){
                    velTotal += edges[r].vel * edges[r].lanes;

                //En caso contrario vendrá limitada por la intersección
                }else{
                    velTotal += min(inter[edges[r].origin] , edges[r].vel * edges[r].lanes);
                }
            }

        }

        velTotal *= 1000; //Las medidas de los coches en metros

        //cout << "\n\nvelTotal = " << velTotal << endl;
        cout << cityName << " " << velTotal/5 << endl; //5 metros = 4metros coche + 1metro separación
    }

    return 0;
}

De esta forma las restricciones (en este caso la velocidad limitante) se progagan a las intersecciones vecinas hasta que no hay más cambios sobre el grafo.


Importante ver también que aquellas carreteras cuya dirección de circulación es contrario a la ciudad de destino, ni siquiera las tengo en cuenta, dado que no afectan al resultado del problema los coches que vienen de AwesomeVille.

Esta era otra de las dudas que me surgían, si el trafico entrante en las intersecciones bloqueaba al tráfico en la dirección contraria de la intersección (de ahí lo de la teoría de colas).


Después ya simplemente nos queda dividir el número total de "ancho de banda" de las carreteras por 5 metros, que es la longitud de un coche con su respectiva separación para obtener el resultado pedido.


Y con esto me  encontraba en el desafio 10 con todos los desafios completados hasta el momento. Pero aún no era consciente de la pesadilla que me esperaba...


Desafio 10 http://programacioncompetitiva.blogspot.com.es/2014/05/tuenti-challenge-2014-5-parte.html