Skip to content

Creare un'IA con vernice Flood

Talía, un membro di questo team, ci ha fatto il favore di scrivere questo tutorial perché conosce perfettamente l'argomento.

Soluzione:

C# - 2.098.382 passi

Ho provato molte cose, la maggior parte di loro fallisce e non funziona affatto, fino a poco tempo fa. Ho ottenuto qualcosa di abbastanza interessante da postare una risposta.

C'è sicuramente modo di migliorare ulteriormente. Penso che scendere sotto i 2M passi potrebbe essere possibile.

Ci sono voluti circa 7 hours per generare i risultati. Ecco un file txt con tutte le soluzioni, nel caso qualcuno voglia controllarle: results.zip

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace FloodPaintAI
{
    class Node
    {   
        public byte Value;             //1-6
        public int Index;              //unique identifier, used for easily deepcopying the graph
        public List Neighbours;  
        public List> NeighboursPositions; //used by BuildGraph() 

        public int Depth;    //used by GetSumDistances() 
        public bool Checked; // 

        public Node(byte value, int index)
        {
            Value = value;      
            Index = index;          
        }

        public Node(Node node)
        {           
            Value = node.Value; 
            Index = node.Index;                     
        }
    }

    class Board
    {
        private const int SIZE = 19;
        private const int STARTPOSITION = 9;

        public Node Root;         //root of graph. each node is a set of contiguous, same color square
        public List Nodes;  //all nodes in the graph, used for deep copying

        public int EstimatedCost; //estimated cost, used by A* Pathfinding
        public List Solution;

        public Board(StreamReader input)
        {                   
            byte[,] board = new byte[SIZE, SIZE];
            for(int j = 0 ; j < SIZE ; j++)
            {
                string line = input.ReadLine();
                for(int i = 0 ; i < SIZE ; i++)         
                {                                       
                    board[j, i] = byte.Parse(line[i].ToString());
                }               
            }
            Solution = new List();
            BuildGraph(board);  
        }

        public Board(Board boardToCopy)
        {               
            //copy the graph            
            Nodes = new List(boardToCopy.Nodes.Count);
            foreach(Node nodeToCopy in boardToCopy.Nodes)
            {
                Node node = new Node(nodeToCopy);
                Nodes.Add(node);
            }

            //copy "Neighbours" property
            for(int i = 0 ; i < boardToCopy.Nodes.Count ; i++)
            {
                Node node = Nodes[i];
                Node nodeToCopy = boardToCopy.Nodes[i];

                node.Neighbours = new List(nodeToCopy.Neighbours.Count);
                foreach(Node neighbour in nodeToCopy.Neighbours)
                {
                    node.Neighbours.Add(Nodes[neighbour.Index]);
                }
            }

            Root = Nodes[boardToCopy.Root.Index];
            EstimatedCost = boardToCopy.EstimatedCost;          
            Solution = new List(boardToCopy.Solution);            
        }

        private void BuildGraph(byte[,] board)
        {                       
            int[,] nodeIndexes = new int[SIZE, SIZE];
            Nodes = new List();

            //check how much sets we have (1st pass)
            for(int j = 0 ; j < SIZE ; j++)
            {
                for(int i = 0 ; i < SIZE ; i++)         
                {               
                    if(nodeIndexes[j, i] == 0) //not already visited                    
                    {
                        Node newNode = new Node(board[j, i], Nodes.Count);                      
                        newNode.NeighboursPositions = new List>();
                        Nodes.Add(newNode);

                        BuildGraphFloodFill(board, nodeIndexes, newNode, i, j, board[j, i]);
                    }
                }       
            }

            //set neighbours and root (2nd pass)
            foreach(Node node in Nodes)
            {
                node.Neighbours = new List();
                node.Neighbours.AddRange(node.NeighboursPositions.Select(x => nodeIndexes[x.Item2, x.Item1]).Distinct().Select(x => Nodes[x - 1]));
                node.NeighboursPositions = null;                
            }
            Root = Nodes[nodeIndexes[STARTPOSITION, STARTPOSITION] - 1];            
        }

        private void BuildGraphFloodFill(byte[,] board, int[,] nodeIndexes, Node node, int startx, int starty, byte floodvalue)
        {
            Queue> queue = new Queue>();
            queue.Enqueue(new Tuple(startx, starty));

            while(queue.Count > 0)
            {
                Tuple position = queue.Dequeue();
                int x = position.Item1;
                int y = position.Item2;

                if(x >= 0 && x < SIZE && y >= 0 && y < SIZE)
                {
                    if(nodeIndexes[y, x] == 0 && board[y, x] == floodvalue)
                    {
                        nodeIndexes[y, x] = node.Index + 1;

                        queue.Enqueue(new Tuple(x + 1, y));
                        queue.Enqueue(new Tuple(x - 1, y));
                        queue.Enqueue(new Tuple(x, y + 1));
                        queue.Enqueue(new Tuple(x, y - 1));                                           
                    }               
                    if(board[y, x] != floodvalue)
                        node.NeighboursPositions.Add(position);                         
                }       
            }
        }

        public int GetEstimatedCost()
        {       
            Board current = this;

            //copy current board and play the best color until the end.
            //number of moves required to go the end is the heuristic
            //estimated cost = current cost + heuristic
            while(!current.IsSolved())
            {
                int minSumDistance = int.MaxValue;
                Board minBoard = null;

                //find color which give the minimum sum of distance from root to each other node
                foreach(byte i in current.Root.Neighbours.Select(x => x.Value).Distinct())
                {
                    Board copy = new Board(current);
                    copy.Play(i);                   

                    int distance = copy.GetSumDistances();                  

                    if(distance < minSumDistance)
                    {
                        minSumDistance = distance;
                        minBoard = copy;
                    }
                }
                current = minBoard;
            }           
            return current.Solution.Count;
        }

        public int GetSumDistances()
        {
            //get sum of distances from root to each other node, using BFS
            int sumDistances = 0;           

            //reset marker
            foreach(Node n in Nodes)
            {
                n.Checked = false;                                  
            }

            Queue queue = new Queue();
            Root.Checked = true;
            Root.Depth = 0; 
            queue.Enqueue(Root);

            while(queue.Count > 0)
            {
                Node current = queue.Dequeue();                             
                foreach(Node n in current.Neighbours)
                {
                    if(!n.Checked)          
                    {                                   
                        n.Checked = true;                                               
                        n.Depth = current.Depth + 1;
                        sumDistances += n.Depth;            
                        queue.Enqueue(n);   
                    }               
                }
            }
            return sumDistances;
        }       

        public void Play(byte value)            
        {
            //merge root node with other neighbours nodes, if color is matching
            Root.Value = value;
            List neighboursToRemove = Root.Neighbours.Where(x => x.Value == value).ToList();
            List neighboursToAdd = neighboursToRemove.SelectMany(x => x.Neighbours).Except((new Node[] { Root }).Concat(Root.Neighbours)).ToList();

            foreach(Node n in neighboursToRemove)
            {
                foreach(Node m in n.Neighbours)
                {
                    m.Neighbours.Remove(n);
                }
                Nodes.Remove(n);
            }   

            foreach(Node n in neighboursToAdd)
            {
                Root.Neighbours.Add(n);         
                n.Neighbours.Add(Root); 
            }           

            //re-synchronize node index
            for(int i = 0 ; i < Nodes.Count ; i++)
            {
                Nodes[i].Index = i;
            }           
            Solution.Add(value);
        }

        public bool IsSolved()
        {           
            //return Nodes.Count == 1;
            return Root.Neighbours.Count == 0;  
        }           
    }

    class Program
    {       
        public static List Solve(Board input)
        {
            //A* Pathfinding            
            LinkedList open = new LinkedList();       
            input.EstimatedCost = input.GetEstimatedCost();
            open.AddLast(input);            

            while(open.Count > 0)
            {                       
                Board current = open.First.Value;
                open.RemoveFirst();

                if(current.IsSolved())
                {
                    return current.Solution;                
                }
                else
                {
                    //play all neighbours nodes colors
                    foreach(byte i in current.Root.Neighbours.Select(x => x.Value).Distinct())
                    {                       
                        Board newBoard = new Board(current);
                        newBoard.Play(i);           
                        newBoard.EstimatedCost = newBoard.GetEstimatedCost();   

                        //insert board to open list
                        bool inserted = false;
                        for(LinkedListNode node = open.First ; node != null ; node = node.Next)
                        {                               
                            if(node.Value.EstimatedCost > newBoard.EstimatedCost)
                            {
                                open.AddBefore(node, newBoard);
                                inserted = true;
                                break;
                            }
                        }       
                        if(!inserted)
                            open.AddLast(newBoard);                                                 
                    }   
                }   
            }
            throw new Exception(); //no solution found, impossible
        }   

        public static void Main(string[] args)
        {                   
            using (StreamReader sr = new StreamReader("floodtest"))
            {   
                while(!sr.EndOfStream)
                {                               
                    List boards = new List();
                    while(!sr.EndOfStream && boards.Count < 100)
                    {
                        Board board = new Board(sr);                        
                        sr.ReadLine(); //skip empty line
                        boards.Add(board);
                    }                                           
                    List[] solutions = new List[boards.Count];                                          
                    Parallel.For(0, boards.Count, i => 
                    {                               
                        solutions[i] = Solve(boards[i]); 
                    });                                         
                    foreach(List solution in solutions)
                    {
                        Console.WriteLine(string.Join(string.Empty, solution));                                             
                    }       
                }               
            }
        }
    }
}

Ulteriori dettagli su come funziona:

Utilizza l'algoritmo A* Pathfinding.

Ciò che è difficile è trovare un buon heuristic. Se il heuristic sottostima il costo, si comporterà quasi come l'algoritmo di Dijkstra e, a causa della complessità di una tavola 19x19 con 6 colori, funzionerà all'infinito. Se sovrastima il costo, convergerà rapidamente a una soluzione, ma non ne darà affatto di buone (qualcosa come 26 mosse quando ne erano possibili 19). Trovare la soluzione perfetta heuristic che fornisca l'esatto numero di mosse rimanenti per raggiungere la soluzione sarebbe la soluzione migliore (sarebbe veloce e darebbe la migliore soluzione possibile). È (a meno che non mi sbagli) impossibile. In realtà richiede di risolvere prima la scheda stessa, mentre questo è ciò che si sta effettivamente cercando di fare (problema dell'uovo e della gallina)

Ho provato molte cose, ecco quello che ha funzionato per il problema heuristic:

  • Costruisco un grafico della scheda corrente da valutare. Ogni node rappresenta un insieme di quadrati contigui dello stesso colore. Utilizzando questo graphposso facilmente calcolare la distanza minima esatta dal centro a qualsiasi altro nodo. Ad esempio, la distanza dal centro all'alto a sinistra sarebbe 10, perché almeno 10 colori li separano.
  • Per calcolare heuristic : Riproduco la tavola corrente fino alla fine. Per ogni passo, scelgo il colore che minimizza la somma delle distanze dalla radice a tutti gli altri nodi.
  • Il numero di mosse necessarie per raggiungere la fine è il heuristic.

  • Estimated cost (usato da A*) = moves so far + heuristic

Non è perfetto perché sovrastima leggermente il costo (quindi viene trovata una soluzione non ottimale). In ogni caso è veloce da calcolare e dà buoni risultati.

Sono riuscito a ottenere un enorme miglioramento della velocità utilizzando il grafico per eseguire tutte le operazioni. All'inizio avevo un 2-dimension array. Lo allagavo e ricalcolavo il grafico quando era necessario (ad esempio per calcolare l'euristica). Ora tutto viene fatto usando il grafico, che viene calcolato solo all'inizio. Per simulare le fasi, non è più necessario riempire il grafo, ma unire i nodi. Questo è molto più veloce.

Python - 10.800.000 passi

Come soluzione di riferimento per l'ultimo posto, considerate questa sequenza:

print "123456" * 18

Scorrere tutti i colori n significa che ogni quadrato n sarà garantito dello stesso colore del quadrato centrale. Ogni quadrato è distante al massimo 18 passi dal centro, quindi 18 cicli garantiranno che tutti i quadrati siano dello stesso colore. Molto probabilmente il programma finirà in meno di questo tempo, ma non è obbligatorio fermarsi non appena tutti i quadrati sono dello stesso colore; è solo molto più vantaggioso farlo. Questa procedura costante è di 108 passi per ogni caso di test, per un totale di 10.800.000.

Java - 2.480.714 passi

Ho fatto un piccolo errore prima (ho messo una frase cruciale prima di un ciclo invece che nel ciclo:

import java.io.*;

public class HerjanPaintAI {

    BufferedReader r;
    String[] map = new String[19];
    char[][] colors = new char[19][19];
    boolean[][] reached = new boolean[19][19], checked = new boolean[19][19];
    int[] colorCounter = new int[6];
    String answer = "";
    int mapCount = 0, moveCount = 0;

    public HerjanPaintAI(){
        nextMap();

        while(true){

            int bestMove = nextRound();
            answer += bestMove;
            char t = Character.forDigit(bestMove, 10);
            for(int x = 0; x < 19; x++){
                for(int y = 0; y < 19; y++){
                    if(reached[x][y]){
                        colors[x][y] = t;
                    }else if(checked[x][y]){
                        if(colors[x][y] == t){
                            reached[x][y] = true;
                        }
                    }
                }
            }

            boolean gameOver = true;
            for(int x = 0; x < 19; x++){
                for(int y = 0; y < 19; y++){
                    if(!reached[x][y]){
                        gameOver = false;
                        break;
                    }
                }
            }

            for(int x = 0; x < 19; x++){
                for(int y = 0; y < 19; y++){
                    checked[x][y] = false;
                }
            }
            for(int i = 0; i < 6; i++)
                colorCounter[i] = 0;

            if(gameOver)
                nextMap();
        }
    }

    int nextRound(){
        for(int x = 0; x < 19; x++){
            for(int y = 0; y < 19; y++){
                if(reached[x][y]){//check what numbers are next to the reached numbers...
                    check(x, y);
                }
            }
        }

        int[] totalColorCount = new int[6];
        for(int x = 0; x < 19; x++){
            for(int y = 0; y < 19; y++){
                totalColorCount[Character.getNumericValue(colors[x][y])-1]++;
            }
        }

        for(int i = 0; i < 6; i++){
            if(totalColorCount[i] != 0 && totalColorCount[i] == colorCounter[i]){//all of this color can be reached
                return i+1;
            }
        }

        int index = -1, number = 0;
        for(int i = 0; i < 6; i++){
            if(colorCounter[i] > number){
                index = i;
                number = colorCounter[i];
            }
        }

        return index+1;
    }

    void check(int x, int y){
        if(x<18)
            handle(x+1, y, x, y);
        if(x>0)
            handle(x-1, y, x, y);
        if(y<18)
            handle(x, y+1, x, y);
        if(y>0)
            handle(x, y-1, x, y);
    }

    void handle(int x2, int y2, int x, int y){
        if(!reached[x2][y2] && !checked[x2][y2]){
            checked[x2][y2] = true;
            if(colors[x2][y2] == colors[x][y]){
                reached[x2][y2] = true;
                check(x2, y2);
            }else{
                colorCounter[Character.getNumericValue(colors[x2][y2])-1]++;
                checkAround(x2, y2);
            }
        }
    }

    void checkAround(int x2, int y2){
        if(x2<18)
            handleAround(x2+1, y2, x2, y2);
        if(x2>0)
            handleAround(x2-1, y2, x2, y2);
        if(y2<18)
            handleAround(x2, y2+1, x2, y2);
        if(y2>0)
            handleAround(x2, y2-1, x2, y2);
    }

    void handleAround(int x2, int y2, int x, int y){
        if(!reached[x2][y2] && !checked[x2][y2]){
            if(colors[x2][y2] == colors[x][y]){
                checked[x2][y2] = true;
                colorCounter[Character.getNumericValue(colors[x2][y2])-1]++;
                checkAround(x2, y2);
            }
        }
    }

    void nextMap(){
        moveCount += answer.length();
        System.out.println(answer);
        answer = "";

        for(int x = 0; x < 19; x++){
            for(int y = 0; y < 19; y++){
                reached[x][y] = false;
            }
        }

        reached[9][9] = true;

        try {
            if(r == null)
                r = new BufferedReader(new FileReader("floodtest"));

            for(int i = 0; i < 19; i++){
                map[i] = r.readLine();
            }
            r.readLine();//empty line

            if(map[0] == null){
                System.out.println("Maps solved: " + mapCount + " Steps: " + moveCount);
                r.close();
                System.exit(0);
            }
        } catch (Exception e) {e.printStackTrace();}

        mapCount++;

        for(int x = 0; x < 19; x++){
            colors[x] = map[x].toCharArray();
        }
    }

    public static void main(String[] a){
        new HerjanPaintAI();
    }
}

Ti mostriamo le recensioni e le valutazioni degli utenti



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.