Google Code Jam 2013 ... con obstáculos
07:30
Me dispongo a participar en el Google Code Jam 2013 donde los obstáculos son mi mujer y mis dos hijos, lo que lo hace muuuucho más interesante. ;-) Llegaré hasta donde mis circunstancias me dejen llegar.
Para el que no conozca de qué va la cosa el Google Code Jam es un concurso de resolución de problemas que organiza Google todos los años.
Iré poniendo mis soluciones por aquí pero no las publicaré hasta mañana por motivos obvios.
Leyendo los problemas
Dado que mi intención no es ganar sino ir pasando de ronda tengo que hacer una primera lectura de los problemas para hacerme una idea de su dificultad e ir haciéndolos de más fácil a más difícil. 07:58 He tardado media horita en leer los cuatro problemas y entenderlos.Problem A. Tic-Tac-Toe-Tomek
Este problema consiste en, dado un tablero de Tic-Tac-Toe en el que hay una ficha "comodín" que cuenta para los dos, saber cuál de los dos ha ganado, empatado o si todavía no ha acabado la partida. Las fichas de los oponentes son 'X' y 'O'. El comodín es la 'T'. Se entiende viendo el ejemplo:Input 6 XXXT .... OO.. .... XOXT XXOO OXOX XXOO XOX. OX.. .... .... OOXX OXXX OX.T O..O XXXO ..O. .O.. T... OXXX XO.. ..O. ...O Output Case #1: X won Case #2: Draw Case #3: Game has not completed Case #4: O won Case #5: O won Case #6: O wonSe resuelve buscando 4 símbolos iguales alineadas en el tablero. Se puede hacer recorriendo los bordes izquierdo e inferior trazando diagonales, horizontales y verticales.
Problem B. Lawnmower
Se trata de comprobar que en un área dada podemos pasar de un lado a otro sin que cambie el valor del número con el que se empezó. También se entiende bien con el ejemplo:Input 3 3 3 2 1 2 1 1 1 2 1 2 5 5 2 2 2 2 2 2 1 1 1 2 2 1 2 1 2 2 1 1 1 2 2 2 2 2 2 1 3 1 2 1 Output Case #1: YES Case #2: NO Case #3: YESEste problema es primo hermano del anterior descartando las diagonales. Se puede resolver recorriendo la parte lateral e inferior, vemos que número hay y vamos moviéndonos hasta llegar al final o hasta que cambie el número.
Problem C. Fair and Square
Nos dan un rango de números y tenemos que recorrer todo el rango y decir cuántos de esos números que son palíndromos tienen una raíz cuadrada que también es palíndroma. También se entiende en el ejemplo:Input 3 1 4 10 120 100 1000 Output Case #1: 2 Case #2: 0 Case #3: 2Hay que recorrer el rango y comprobar lo que he dicho arriba. Este es muy fácil.
Problem D. Treasure
Te dan una llave al principio. Con esa llave puedes abrir otra puerta en la que puedes encontrar otras llaves o ninguna. Si encuentras más llaves puedes abrir más puertas. Se trata de conseguir ir por todas las puertas sin dejar ninguna sin abrir. Cada llave se puede usar una sola vez. Se entiende bien en el enunciado:Chest Number | Key Type To Open Chest | Key Types Inside --------------+--------------------------+------------------ 1 | 1 | None 2 | 1 | 1, 3 3 | 2 | None 4 | 3 | 2Puedes abrir todas las puertas si vas en orden 2, 1, 4 y 3. Si empiezas por abrir primero el 1 te quedas sin llaves y no puedes seguir abriendo puertas. La entrada de ejemplo sería esta:
Input 3 1 4 1 1 0 1 2 1 3 2 0 3 1 2 3 3 1 1 1 1 0 1 0 1 0 1 1 2 1 1 1 Output Case #1: 2 1 4 3 Case #2: 1 2 3 Case #3: IMPOSSIBLEEste es un pelín más difícil que el resto. Se puede resolver de forma recursiva probando diferentes caminos como si fuera el típico problema del salto del caballo.
Orden de resolución
8:20 Me he tirado 20 minutos escribiendo lo anterior. :-/ Hay dos casos parecidos y dos distintos. De los distintos el último es el menos fácil así que iré a por el distinto fácil y luego mientras doy el desayuno a los niños pensaré los otros dos: 1.- Problem C. Fair and Square 2.- Problem A. Tic-Tac-Toe-Tomek 3.- Problem B. Lawnmower 4.- Problem D. TreasureProblem C. Fair and Square
11:30 Tras dar de desayunar a los niños, vestirles, hacer camas ... terminé de resolver el problema. Resolverlo para pasar con "Small input" es sencillo, lo difícil es resolver los casos en los que hay muchas entradas. Una posible solución al problema es:#!/usr/bin/python import math import time def isPalindrome(number): # Palindrome means that it is simetric strNumber=str(number) lenStrNumber=len(strNumber) isPalindrome=1 i=-1 j=lenStrNumber while i<(lenStrNumber-1) and isPalindrome==1: i=i+1 j=j-1 if strNumber[i]!=strNumber[j]: isPalindrome=0 return isPalindrome def isPerfectSquare(number): # Perfect square means that its decimal is 0 perfectSquare=0 if str(math.sqrt(number)).split('.')[1]=="0": perfectSquare=1 else: perfectSquare=0 return perfectSquare # Open input file inputF=open('C-large-1.in','r') outputF=open('C-large-1.out','w') # Get number of cases line="" while len(line)<1: line=inputF.readline() NoC=int(line) # Read all cases and solve it case=0 while case < NoC: case=case+1 print "Case: "+str(case) line=inputF.readline() while len(line)<1: line=inputF.readline() caseLine=line rangeBegins=int(caseLine.split()[0]) rangeEnds=int(caseLine.split()[1]) # All input data in rangeBegins, rangeEnds and case NumberIsFairAndSquare=0 caseTest=rangeBegins-1 while caseTest < rangeEnds: # Test if caseTest is a Fair and perfect Square caseTest=caseTest+1 # Perfect square means that its decimal is 0 if isPerfectSquare(caseTest): perfectSquare=int(math.sqrt(caseTest)) if ((isPalindrome(caseTest)==1) and (isPalindrome(perfectSquare)==1)): NumberIsFairAndSquare=NumberIsFairAndSquare+1 print "Found FairAndSquare: "+str(caseTest)+" with perfectSquare: "+str(perfectSquare) outputF.write("Case #"+str(case)+": "+str(NumberIsFairAndSquare)+"\n") "+str(rangeBegins)+"-"+str(rangeEnds)+" is "+str(NumberIsFairAndSquare) inputF.close() outputF.close()Con este caso he pagado varias novatadas:
- No hacer buenas pruebas con la salida que se envía a Google. Me equivoqué y tuve que arreglar el formato de salida en los 4 minutos que te dan de plazo.
- No programar pensando en la eficiencia. El "que funcione como sea" que he hecho yo aquí no sirve. En el caso con pocas entradas sólo tardó un segundo en ejecutarse cuando te dan 4 minutos. Con muchas entradas te dan 8 minutos pero mi programa tarda bastante más.
- No desarrollar pensando en los cores de la máquina: el problema se puede dividir para lanzar cuatro procesos que se lanzarían uno en cada core y como tengo 4 iría 4 veces más rápido.
- No desarrollar pensando en castings innecesarios ni en condiciones de bucles simples. En el código que he puesto arriba ya he quitado algunas cosas rematadamente mal hechas pero antes en cada if se hacía un casting para pasar de string a entero.
- Elegir mal la forma de resolver el problema. Hacer una raíz cuadrada es varias órdenes de magnitud más lento que elevar al cuadrado. Hubiera sido mucho más rápido hacer una caché de cuadrados palíndromos almacenada en un diccionario que lo que yo he hecho.
- Mala elección del lenguaje de programación. C y C++ son más rápidos. Quizás no sea necesario usarlos para pasar pero seguro que me ahorraría bastante tiempo de ejecución.
Problem B. Lawnmower
15:29 Estuve pensando en el problema en el parque mientras vigilaba a los niños y me ha parecido más difícil de lo que pensaba en un principio ya que se trata de poner la altura de la máquina al máximo y cortar todo el césped. Luego para cada altura hay que ir celda por celda intentando cortar el césped en horizontal y si no se puede en vertical. Si no se puede en horizontal ni en vertical es que no se puede. 00:30 Al final he desistido de terminar el código por falta de tiempo dejando sin hacer cutRow y cutColum. Lo que me ha dado tiempo a escribir es lo siguiente:#!/usr/bin/python import sys import pprint import time def getNumberOfCases(inputF): line="" NoC=0 while len(line) < 1: line=inputF.readline() NoC=int(line) return NoC getLawn(inputF): line="" dimY=0 dimX=0 lawn=[] while len(line)<1: line=inputF.readline() dimY=int(line.split()[0]) dimX=int(line.split()[1]) for y in range(dimY): line=inputF.readline() #print "line "+str(y)+": "+line lawn.append(line.split()) # Convert all data to int for i in range(len(lawn)): for j in range(len(lawn[0])): lawn[i][j]=int(lawn[i][j]) return lawn def maxHeight (lawn, x1, x2, y1, y2): maximum=0 #print "x1="+str(x1) #print "x2="+str(x2) #print "y1="+str(y1) #print "y2="+str(y2) x=x1 y=y1 while y<=y2: x=0 while x<=x2: maximum=max(maximum, lawn[x][y]) #print "x="+str(x) #print "y="+str(y) x=x+1 y=y+1 return int(maximum) def minHeight (lawn, x1, x2, y1, y2): minimum=999 x=x1 y=y1 while y<=y2: x=0 while x<=x2: minimum=min(minimum, lawn[x][y]) x=x+1 y=y+1 return int(minimum) def cutColumn (lawn, column, height): possible=1 # Aquí me quedé return possible,vLawn def cutRow (vLawn, x, y, height, lawn): possible=1 # Aquí me quedé return possible,vLawn def possibleLawn (lawn): height=0 possible=1 # Wanted: # 11111 # 12221 # 12321 # 12321 # 11111 # # Step 1 Step 2 Step 3 # 33333 22222 11111 # 33333 22222 12221 # 33333 22322 12321 # 33333 22322 12321 # 33333 22222 11111 # # Look for higher value and cut all lawn # Look for value-1 and cut lawn without touch higher values -> try vertical and horizontal cut maxH=maxHeight(lawn, 0, len(lawn)-1, 0, len(lawn[0])-1) minH=minHeight(lawn, 0, len(lawn)-1, 0, len(lawn[0])-1) # Create a virgin lawn with the same dimensions and set all to maxH vLawn=lawn for i in range(len(vLawn)): for j in range(len(vLawn[0])): vLawn[i][j]=maxH height=maxH while((possible==1) and (height>minH)): height=height-1 # For each position in lawn minimum=min(minimum, lawn[x][y]) y=len(vLawn) x=len(vLawn[0]) while (y>=0) and (possible==1): x=len(vLawn[0]) while (x>=0) and (possible==1): # if height desired in lawn is higher than height cut row to height possible,vLawn=cutRow (vLawn, x, y, height, lawn) if possible==0: # if cannot cut row, cut column possible,vLawn=cutColumn (vLawn, x, y, height, lawn) # else -> impossible x=x+1 y=y+1 return possible if __name__=='__main__': # Open input and output files inputF=open(sys.argv[1],'r') outputF=open(sys.argv[2],'w') NoC=getNumberOfCases(inputF) # Read all cases and solve them case=0 while case < NoC: case=case+1 print "Case: "+str(case) lawn=getLawn(inputF) #pprint.pprint(lawn) #time.sleep(5) possible=possibleLawn(lawn) if (possible==1): outputF.write("Case #"+str(case)+": YES\n") else: outputF.write("Case #"+str(case)+": NO\n") # Close input and output files inputF.close() outputF.close()
Consejos para afrontar un Google Code Jam para geeks casados con hijos y poco tiempo
Al final no he podido terminar por falta de tiempo pero he sacado unas conclusiones para volver a intentarlo el año que viene, eso sí, sin intención de ganar los 10000$:- Lo primero es que los problemas no son fáciles. Tampoco son difíciles si lo que se quiere es que funcionen con entradas pequeñas. Cualquier persona que haya hecho los tres primeros años de Ingeniería (o Ingeniería Técnica) en Informática es capaz de resolver los problemas en tiempo para las entradas pequeñas. No debería llevar más de una hora frente a la pantalla para cada problema.
- El Google Code Jam comienza a la 1 de la mañana hora española. Hay que leerse los problemas a la 1 de la mañana e irse a la cama a la 1:30 con los problemas en la cabeza para que nuestro cerebro piense por nosotros mientras dormimos. Los problemas se pueden resolver el día siguiente.
- Tener varias funciones ya hechas. Hay que saber que el formato de entrada siempre es el mismo: número de casos en la primera línea y datos en las siguientes. Nos ahorraremos por lo menos una horas si ya las tenemos hechas y probadas.
- Aprovecha el tiempo que no estás delante del ordenador. El tiempo que vas a tener para picar código es muy reducido así que llévate en el móvil el enunciado del problema que vas a resolver y piensa sobre él mientras vistes niños, les das de desayunar o les llevas al parque para que al estar frente a la pantalla puedas utilizar el 100% del tiempo escribiendo código.