The Final Stand - Sconfiggi l'orda di zombi


25

introduzione

Sei solo su un'isola. Il resto dell'umanità è morto ( probabilmente a causa del bug nel codice di user12345 ). L'orda pirata zombi ha raggiunto la tua isola e sono infiniti. È tempo di calciare il culo o masticare la gomma da masticare, e siete tutti fuori di gomma da masticare.

Problema

Il nostro scenario del giorno del giudizio è descritto da 2 numeri interi su una sola riga me n. Sulla tua isola ci sono avamposti numerati in modo univoco da 1 a m. Le seguenti nlinee contengono ciascuno tre numeri interi, x, y, e z, separati da uno spazio. xe ysono gli ID univoci di due avamposti, ed zè il numero di zombi che si incontreranno sul percorso tra di loro.

Quando percorri un sentiero, perdi zmunizioni e uccidi gli zzombi. Se percorri di nuovo lo stesso percorso, sfortunatamente incontrerai lo stesso numero di zombi. Tutti gli avamposti generano +1 munizioni ogni volta che percorri un percorso. Inizi all'avvio con 100 munizioni 1. Tutti gli avamposti iniziano con 0 munizioni. Muori immediatamente se non esiste un percorso per il quale le tue munizioni sono maggiori del numero di zombi su quel percorso e il resto delle tue munizioni viene convertito in uccisioni. Questa è la tua posizione finale.

Scrivi un programma che genera il numero massimo di zombi che puoi uccidere per un determinato scenario. Se riesci a uccidere un numero infinito di zombi, semplicemente emetti x.

Esempio di input

5 6
1 2 4
2 3 4
3 1 4
2 4 10
2 5 10
1 1 50

Esempio di output

x

ipotesi

  • Un percorso sarà tra due avamposti validi. Vale a dire 1 <= x/ y<=m
  • Se un percorso tra xe ynon è elencato, non può essere percorso
  • Un percorso è bidirezionale
  • 1 m<<= 100
  • 1 n<<= 500
  • L'input deve essere fornito tramite stdin, letto da un file o accettato come unico argomento per il programma e deve seguire esattamente il formato dell'esempio
  • L'autonomia del tuo programma può essere arbitrariamente grande ma deve essere decisamente finita

Vince il codice con il minor numero di caratteri!


Ogni avamposto diverso da 1inizia con 0 munizioni? Il grafico non è direzionale?
Peter Taylor,

2
Probabilmente sarebbe anche utile prevenire una determinata classe di bug avendo un caso di test in cui esiste un ciclo che non esaurisce le munizioni ma che non può essere raggiunto in tempo. (Aggiungo che non sono convinto che l'attuale caso di test sia corretto: mi sembra che il ciclo 1->1costi 49 munizioni e il ciclo 1->2->3->1costi 3 munizioni a lungo termine.
Peter Taylor

@PeterTaylor Ho dovuto ritirare entrambi i miei commenti perché sembra che ho reso l'esempio bidirezionale . Consentitemi quindi di ricominciare: tutti i percorsi sono bidirezionali e tutti gli avamposti iniziano con 0. L'esempio ora dovrebbe funzionare.
Rainbolt,

@Rusher: bell'esempio! Mi ci sono voluti 45 passaggi per dimostrare a me stesso che è davvero infinitamente sostenibile. Possiamo presumere che tutti gli avamposti saranno raggiungibili o vuoi che gestiamo il caso in cui ci sono avamposti scollegati dal grafico principale?
Claudiu,

1
Ahhh ... Quindi per ogni passaggio dalla A alla B, ogni avamposto "genera" delle munizioni e la mantiene lì fino a quando non la visiti.
Tobia,

Risposte:


14

Java ( meno grottesco: 8415 5291 3301)

Ok. Fondamentalmente, sono imbarazzato che nessuno abbia presentato una soluzione. Quindi qualche giorno fa ho iniziato a provare a risolvere questo problema, b / c è fantastico. . Segui quel link per vedere i miei progressi su di esso tramite GitHub.

modificare

Nuova versione del solutore, molto più "giocata a golf", con correttore di cicli corretto identificato da MT0. Supporta inoltre percorsi di avanzamento rapido, regolabili modificando la quantità di memoria disponibile per la VM. Ultima modifica GRANDE : mi sono reso conto che avevo alcuni altri piccoli errori nell'indice e ottimizzazioni premature, che hanno provocato l'incapacità di considerare un numero piuttosto elevato di tipi di vincite. Quindi è stato risolto, con attenzione. La nuova versione è sia più piccola che inferiore. Per il nostro percorso di riferimento, java -Xmx2GB ZombieHordeMinfa il trucco abbastanza bene (attenzione, ci vorrà del tempo).

Factoide freddo

In una svolta affascinante, ci sono MOLTE soluzioni alla lunghezza 24, e il mio risolutore trova una diversa da quella di MT0, ma identica in linea di principio, tranne per il fatto che inizia visitando gli altri avamposti collegati 1. Affascinante! Totalmente contro l'intuizione umana, ma perfettamente valida.

Aspetti salienti della soluzione

Quindi ecco il mio. È (parzialmente) golfato, b / c è un risolutore esponenziale, quasi a forza bruta. Uso un algoritmo IDDFS (prima ricerca approfondita della profondità di approfondimento), quindi è un ottimo solutore generale che non salta, quindi risolve entrambe le parti della domanda del PO, vale a dire:

  • Se viene trovata una rotta vincente (infiniti zombi), genera 'x'.
  • Se tutte le rotte finiscono con la morte (zombi finiti), genera il maggior numero di zombi uccisi.

Dagli abbastanza potenza, memoria e tempo, e farà proprio questo, anche le mappe a morte lenta. Ho trascorso un po 'più di tempo a migliorare questo risolutore, e mentre si può fare di più, ora è un po' meglio. Ho anche integrato i consigli di MT0 sulla migliore soluzione di zombi infiniti e ho rimosso diverse ottimizzazioni premature dal mio win-checker che ha impedito alla versione precedente di trovarla, e ora in effetti trovo una soluzione molto simile a quella di MT0 descritta.

Alcuni altri punti salienti:

  • Come accennato, utilizza un IDDFS per trovare il percorso vincente più breve possibile.
  • Dal momento che è al centro di un DFS, scoprirà anche se ogni rotta termina con la morte del nostro eroe e tiene traccia della rotta "migliore" in termini di maggior parte degli zombi uccisi. Muori un eroe!
  • Ho strumentato l'algoritmo per rendere più interessante la visione di Removed per scopi di golf. Segui uno dei link a github per vedere la versione non giocata.
  • Ci sono anche molti commenti, quindi sentiti libero di ri-implementare la tua soluzione sulla base del mio approccio o mostrami come dovrebbe essere fatto!
  • Avanzamento rapido del percorso adattivo alla memoria
    • Fino alla memoria di sistema disponibile, terrà traccia delle "rotte finali" che non hanno provocato la morte.
    • Utilizzando una routine di compressione e decompressione del percorso elaborata, i progressi da una precedente iterazione di IDDFS vengono ripristinati per impedire la riscoperta di tutti i percorsi visitati precedenti.
    • Come bonus laterale intenzionale, funge da abbattimento di un vicolo cieco. Le rotte dei vicoli ciechi non vengono memorizzate e non verranno mai più visitate nelle profondità future di IDDFS.

Storia del risolutore

  • Ho provato un sacco di algoritmi look-ahead in un solo passaggio, e mentre per scenari molto semplici avrebbero funzionato, alla fine sono rimasti piatti.
  • Poi ho provato un algoritmo look-ahead in due passaggi, che era ... insoddisfacente.
  • Ho quindi iniziato a costruire un look -head n-step, quando ho riconosciuto che questo approccio è riducibile a DFS, ma DFS è molto ... più elegante.
  • Durante la costruzione del DFS, mi è venuto in mente che IDDFS avrebbe assicurato (a) di trovare la migliore rotta HERO (morte) o (b) il primo ciclo vincente.
  • Scoprire che costruire una pedina del ciclo di vittoria è facile, ma ho dovuto passare diverse iterazioni molto sbagliate prima di arrivare a una pedina di successo.
  • Factored nel percorso di vittoria di MT0 per rimuovere tre linee di ottimizzazione prematura che hanno reso cieco il mio algoritmo.
  • Aggiunto un algoritmo adattivo di memorizzazione nella cache del percorso che utilizzerà tutta la memoria che gli viene fornita per evitare inutili ripetizioni del lavoro tra le chiamate IDDFS e elimina i percorsi senza uscita fino ai limiti della memoria.

Il codice (golf)

Passa al codice (scarica la versione non controllata qui o qui ):

import java.util.*;public class ZombieHordeMin{int a=100,b,m,n,i,j,z,y,D=0,R,Z,N;int p[][][];Scanner in;Runtime rt;int[][]r;int pp;int dd;int[][]bdr;int ww;int[][]bwr;int[][]faf;int ff;boolean ffOn;public static void main(String[]a){(new ZombieHordeMin()).pR();}ZombieHordeMin(){in=new Scanner(System.in);rt=Runtime.getRuntime();m=in.nextInt();N=in.nextInt();p=new int[m+1][m+1][N+1];int[]o=new int[m+1];for(b=0;b<N;b++){i=in.nextInt();j=in.nextInt();z=in.nextInt();o[i]++;o[j]++;D=(o[i]>D?o[i]:D);p[i][j][++p[i][j][0]]=z;if(i!=j)p[j][i][++p[j][i][0]]=z;D=(o[j]>D?o[j]:D);}m++;}void pR(){r=new int[5000][m+3];r[0][0]=a;Arrays.fill(r[0],1,m,1);r[0][m]=1;r[0][m+1]=0;r[0][m+2]=0;ww=-1;pp=dd=0;pR(5000);}void pR(int aMD){faf=new int[D][];ff=0;ffOn=true;for(int mD=1;mD<=aMD;mD++){System.out.printf("Checking len %d\n",mD);int k=ffR(0,mD);if(ww>-1){System.out.printf("%d x\n",ww+1);for(int win=0;win<=ww;win++)System.out.printf(" %d:%d,%d-%d",win,bwr[win][0],bwr[win][1],bwr[win][2]);System.out.println();break;}if(k>0){System.out.printf("dead max %d kills, %d steps\n",pp,dd+1);for(int die=0;die<=dd;die++)System.out.printf(" %d:%d,%d-%d",die,bdr[die][0],bdr[die][1],bdr[die][2]);System.out.println();break;}}}int ffR(int dP,int mD){if(ff==0)return pR(dP,mD);int kk=0;int fm=ff;if(ffOn&&D*fm>rt.maxMemory()/(faf[0][0]*8+12))ffOn=false;int[][]fmv=faf;if(ffOn){faf=new int[D*fm][];ff=0;}for(int df=0;df<fm;df++){dS(fmv[df]);kk+=pR(fmv[df][0],mD);}fmv=null;rt.gc();return kk==fm?1:0;}int pR(int dP,int mD){if(dP==mD)return 0;int rT=0;int dC=0;int src=r[dP][m];int sa=r[dP][0];for(int dt=1;dt<m;dt++){for(int rut=1;rut<=p[src][dt][0];rut++){rT++;r[dP+1][0]=sa-p[src][dt][rut]+r[dP][dt];for(int cp=1;cp<m;cp++)r[dP+1][cp]=(dt==cp?1:r[dP][cp]+1);r[dP+1][m]=dt;r[dP+1][m+1]=rut;r[dP+1][m+2]=r[dP][m+2]+p[src][dt][rut];if(sa-p[src][dt][rut]<1){dC++;if(pp<r[dP][m+2]+sa){pp=r[dP][m+2]+sa;dd=dP+1;bdr=new int[dP+2][3];for(int cp=0;cp<=dP+1;cp++){bdr[cp][0]=r[cp][m];bdr[cp][1]=r[cp][m+1];bdr[cp][2]=r[cp][0];}}}else{for(int chk=0;chk<=dP;chk++){if(r[chk][m]==dt){int fR=chk+1;for(int cM=0;cM<m+3;cM++)r[dP+2][cM]=r[dP+1][cM];for(;fR<=dP+1;fR++){r[dP+2][0]=r[dP+2][0]-p[r[dP+2][m]][r[fR][m]][r[fR][m+1]]+r[dP+2][r[fR][m]];for(int cp=1;cp<m;cp++)r[dP+2][cp]=(r[fR][m]==cp?1:r[dP+2][cp]+1);r[dP+2][m+2]=r[dP+2][m+2]+p[r[dP+2][m]][r[fR][m]][r[fR][m+1]];r[dP+2][m]=r[fR][m];r[dP+2][m+1]=r[fR][m+1];}if(fR==dP+2&&r[dP+2][0]>=r[dP+1][0]){ww=dP+1;bwr=new int[dP+2][3];for(int cp=0;cp<dP+2;cp++){bwr[cp][0]=r[cp][m];bwr[cp][1]=r[cp][m+1];bwr[cp][2]=r[cp][0];}return 0;}}}dC+=pR(dP+1,mD);if(ww>-1)return 0;}for(int cp=0;cp<m+3;cp++)r[dP+1][cp]=0;}}if(rT==dC)return 1;else{if(ffOn&&dP==mD-1)faf[ff++]=cP(dP);return 0;}}int[]cP(int dP){int[]cmp=new int[dP*2+3];cmp[0]=dP;cmp[dP*2+1]=r[dP][0];cmp[dP*2+2]=r[dP][m+2];for(int zip=1;zip<=dP;zip++){cmp[zip]=r[zip][m];cmp[dP+zip]=r[zip][m+1];}return cmp;}void dS(int[]cmp){int[]lv=new int[m];int dP=cmp[0];r[dP][0]=cmp[dP*2+1];r[dP][m+2]=cmp[dP*2+2];r[0][0]=100;r[0][m]=1;for(int dp=1;dp<=dP;dp++){r[dp][m]=cmp[dp];r[dp][m+1]=cmp[dP+dp];r[dp-1][cmp[dp]]=dp-lv[cmp[dp]];r[dp][m+2]=r[dp-1][m+2]+p[r[dp-1][m]][cmp[dp]][cmp[dP+dp]];r[dp][0]=r[dp-1][0]+r[dp-1][cmp[dp]]-p[r[dp-1][m]][cmp[dp]][cmp[dP+dp]];lv[cmp[dp]]=dp;}for(int am=1;am<m;am++)r[dP][am]=(am==cmp[dP]?1:dP-lv[am]+1);}}

Ottieni il codice da github qui, per tenere traccia di tutte le modifiche che apporto. Ecco alcune altre mappe che ho usato.

Esempio di output

Esempio di output per soluzione di riferimento:

    $ java -d64 -Xmx3G ZombieHordeMin > reference_route_corrected_min.out
    5 6 1 2 4 2 3 4 3 1 4 2 4 10 2 5 10 1 1 50
    Checking len 1
    Checking len 2
    Checking len 3
    Checking len 4
    Checking len 5
    Checking len 6
    Checking len 7
    Checking len 8
    Checking len 9
    Checking len 10
    Checking len 11
    Checking len 12
    Checking len 13
    Checking len 14
    Checking len 15
    Checking len 16
    Checking len 17
    Checking len 18
    Checking len 19
    Checking len 20
    Checking len 21
    Checking len 22
    Checking len 23
    Checking len 24
    25 x
     0:1,0-100 1:3,1-97 2:1,1-95 3:2,1-94 4:5,1-88 5:2,1-80 6:4,1-76 7:2,1-68 8:1,1-70 9:2,1-68 10:1,1-66 11:2,1-64 12:1,1-62 13:2,1-60 14:1,1-58 15:2,1-56 16:1,1-54 17:2,1-52 18:1,1-50 19:2,1-48 20:1,1-46 21:2,1-44 22:1,1-42 23:2,1-40 24:1,1-38

Leggi l'output del percorso in questo modo step:: source, route-to-get-here- ammo. Quindi nella soluzione sopra, lo leggeresti come:

  • Al passo 0, all'avamposto 1con munizioni 100.
  • Al passaggio 1, usa il percorso 1per arrivare all'avamposto 3con munizioni finali97
  • Al passaggio 2, usa il percorso 1per arrivare all'avamposto 1con munizioni finali95
  • ...

Note di chiusura

Quindi, spero di aver reso la mia soluzione più difficile da battere, ma PER FAVORE PROVA! Usalo contro di me, aggiungi qualche elaborazione parallela, una migliore teoria dei grafi, ecc. Un paio di cose che immagino potrebbero migliorare questo approccio:

  • "ridurre" in modo aggressivo i loop per eliminare la ricostruzione inutile man mano che l'algoritmo avanza.
    • Un esempio: nel problema di esempio, considera i loop 1-2-3 e le altre permutazioni come "un passo", in modo da poter fare il nostro ciclo di fine ciclo più rapidamente.
    • Ad esempio, se ci si trova nel nodo 1, è possibile (a) passare a 2, (b) passare a 1, (c) passare 1-2-3 come un passo e così via. Ciò consentirebbe a un risolto di ripiegare la profondità in larghezza, aumentando il numero di percorsi a una profondità particolare ma accelerando notevolmente il tempo di soluzione per i cicli lunghi.
  • abbattere percorsi morti. La mia attuale soluzione non "ricorda" che un determinato percorso è senza uscita e deve riscoprirlo ogni volta. Sarebbe meglio tenere traccia del primo momento in un percorso in cui la morte è certa e non progredire mai oltre. fatto...
  • se attento, è possibile applicare l'abbattimento della rotta morta come abbattimento della sotto-rotta. Ad esempio, se 1-2-3-4 provoca sempre la morte e il risolutore sta per testare la rotta 1-3-1-2-3-4, dovrebbe immediatamente smettere di scendere quella via poiché si garantisce che finirà nella delusione. Sarebbe comunque possibile calcolare il numero di uccisioni, con un po 'di matematica attenta.
  • Qualsiasi altra soluzione che scambia memoria per tempo o consente di evitare aggressivamente le seguenti rotte senza uscita. fatto anche questo!

Bella risposta! Chi ha bisogno di giocare a golf con il proprio codice quando sono gli unici a poter risolvere il problema? Sono motivato ora a scrivere la mia soluzione, quindi ci lavorerò su.
Rainbolt

Eccellente, era quello che speravo potesse fare. Sentiti libero di prendere in prestito / rubare qualcosa dalla mia risposta che ritieni utile! Anche se ovviamente spero che altre persone oltre a me stesso e OP cercheranno di risolvere: P
ProgrammatoreDan

Sono stato eliminato e ho iniziato a minimizzare il tuo codice. Se pensavi che la tua risposta fosse grottesca prima, dai un'occhiata a: tny.cz/17ef0b3a . Sono ancora in corso lavori.
Rainbolt

Haha, sei stato davvero distratto. Sembra buono (appropriatamente orribile per il code-golf? Sai cosa intendo) finora!
Programmatore

@Rusher Qualche fortuna finora? Ho alcune idee per miglioramenti che sto preparando, tra cui una tecnica di compressione della rappresentazione del percorso e un modo per avanzare rapidamente attraverso percorsi già elaborati (fino a un certo punto).
Programmatore:

2

Alcune note astratte su una soluzione

Se avrò tempo lo convertirò in un algoritmo ...

Per un dato grafico Gesiste quindi un sotto-grafico collegato G'che contiene la città 1. Se c'è una soluzione infinito allora esisterà un collegato sub-grafico G''di G'che contiene Vcittà e Ppercorsi.

I percorsi Pdi G''possono essere partizionati in modo tale da {p}contenere un percorso che ha quindi un costo minimo di tutti i percorsi in Ped P/{p}è tutti gli altri percorsi (che formano un albero di spanning o possibilmente un ciclo). Se assumiamo che pnon è un bordo loop (che collega le due estremità alla stessa città), allora si collegherà due città ( v1e v2) ed ha il costo cdi munizioni allora (il sopravvissuto) può quindi traversata da v1a v2e indietro a un costo totale di 2cmunizioni e questo aumenterà le munizioni in tutte le città di 2 (per un aumento totale 2|V|all'interno G''- alcune delle quali saranno state raccolte da v1e v2).

Se si viaggia da v1a v2e di nuovo a v1multipla ( m) volte e poi prendere un viaggio da v1lungo i bordi P/{p}per visitare tutte le città diverse v1e v2prima di tornare a v1e questo richiede npercorsi per raggiungere (dove |P/{p}| ≤ n ≤ 2|P/{p}|dato che si dovrebbe mai bisogno di percorrere un cammino più di due volte) con un costo di ke le città otterranno 2m|V|munizioni (di nuovo alcune delle quali saranno state raccolte durante l'attraversamento).

Alla luce di tutto ciò, puoi dire se una soluzione infinita è potenzialmente possibile se il costo k + 2mcè uguale o inferiore alla ricompensa totale 2(m+n)|V|.

C'è un'ulteriore complessità al problema in quanto:

  • potrebbe essere necessario viaggiare dalla città di partenza 1alla {p}prima iterazione e tenere conto di questo costo; e
  • devi anche assicurarti che i me nsiano abbastanza bassi da non esaurire le munizioni prima di poter superare la prima iterazione poiché la prima iterazione avrà un costo maggiore rispetto alle iterazioni successive).

Ciò porta a una soluzione neutrale in termini di costi a 24 percorsi nell'esempio nella domanda (i numeri sono le città visitate):

1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,2,4,2,5,2,3, ... and repeat ...

Una piccola cosa da aggiungere: potrebbe essere necessario prendere in considerazione i bordi in loop con un costo di 1, poiché tali bordi da soli formano una condizione vincente se è possibile raggiungerli in tempo.
Rainbolt
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.