最后一站-击败僵尸部落


25

介绍

你一个人在岛上。人类的其余部分已死(可能是由于user12345的代码中的错误)。僵尸海盗部落已经到达您的岛屿,他们永无止境。是时候踢屁股或咀嚼泡泡糖了,而您全都没有泡泡糖了。

问题

我们的世界末日场景由一行中的2个整数m和来描述n。在您的岛上,前哨站的编号从1到唯一m。以下n各行包含三个整数,xy,和z,用空格隔开。xy两个前哨的唯一ID,并且z是将它们之间的道路上可能遇到的僵尸的数量。

当您沿着小路行驶时,您会丢失z弹药并杀死z僵尸。不幸的是,如果再次走同一条路线,您将遇到相同数量的僵尸。每次旅行时,所有前哨站都会产生+1枚弹药。您从前哨站1的弹药开始。所有前哨站的弹药从0开始。如果不存在弹药大于该路径上的僵尸数量的路径,则您会立即死亡,剩余的弹药将被转换为杀戮。这是你的最终立场。

编写一个程序,输出在给定情况下可以杀死的最大僵尸数量。如果您可以杀死无限数量的僵尸,只需输出x

输入示例

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

示例输出

x

假设条件

  • 路径将位于两个有效的前哨站之间。也就是说1 <= x/ y<=m
  • 如果未列出x和之间的路径y,则无法通过
  • 路径是双向的
  • 1 m<<== 100
  • 1 n<<= 500
  • 输入必须通过stdin提供,从文件中读取或作为程序的唯一参数被接受,并且它必须完全遵循示例的格式
  • 程序的运行时可能会很大,但必须是有限的

字符最少的代码胜出!


除了1以0的弹药开始以外,每个前哨基地是否都存在?图是无方向的吗?
彼得·泰勒

2
通过有一个测试用例来抢占某些类别的错误,这可能也是有用的,在该用例中,有一个周期不会耗尽弹药,但无法及时到达。(我要补充一点,我不相信,目前的测试用例是正确的:在我看来,该周期1->1成本49弹药,以及周期1->2->3->1费用3弹药从长远来看。
彼得·泰勒

@PeterTaylor我不得不撤回我的两条评论,因为看来我使示例成为双向的。因此,让我重新开始-所有路径都是双向的,所有前哨均以0开始。该示例现在应该可以工作。
Rainbolt 2014年

@Rusher:很好的例子!我花了45个步骤向自己展示了它确实是无限可持续的。我们是否可以假设所有前哨站都可以到达,还是要我们处理前哨站与主图断开连接的情况?
Claudiu 2014年

1
嗯...因此,从A到B的每一步,每个哨所都会“生成”弹药,并保持在那里,直到您参观为止。
Tobia

Answers:


14

爪哇(不那么怪异:8415 5291 3301)

好。基本上,我很尴尬,没有人提交解决方案。因此,几天前我开始尝试解决此问题,b / c很棒。。单击该链接以通过GitHub观看我的进度。

编辑

新的求解器版本,更加“实用”,具有MT0标识的更正循环检查器。它还支持快速转发路由,可通过更改VM可用的内存量来进行调整。最新的BIG编辑:意识到我还有其他一些小的索引错误和过早的优化,导致未能考虑很多类型的胜利。所以,这是固定的,请小心。新版本既小又小。对于我们的参考路线,java -Xmx2GB ZombieHordeMin技巧非常好(请注意,这将需要一段时间)。

酷的事实

令人着迷的是,许多解决方案的长度为24,而我的求解器发现了一个与MT0不同的解决方案,但在原理上相同,不同之处在于它访问与相连的其他前哨站开始1。迷人!完全抵制人类的直觉,但完全有效。

解决方案要点

所以这是我的。它(部分)打了高尔夫球,b / c是一个指数的,几乎是蛮力的求解器。我使用IDDFS(迭代深入深度优先搜索)的算法,所以它不跳过一个伟大的一般解算器,所以它解决了两个任择议定书的问题,即部分:

  • 如果找到获胜路线(无限僵尸),则输出“ x”。
  • 如果所有路径都以死亡结尾(有限的僵尸),则输出被杀死的最大数量的僵尸。

给它足够的能力,内存和时间,它将做到这一点,甚至是慢死地图。我花了更多时间改进此求解器,尽管可以做更多的事情,但现在情况有所好转。我还将MT0的建议整合到了最佳的无限僵尸解决方案上,并从我的获胜者中删除了一些过早的优化,这些优化阻止了以前的版本无法找到它,现在我确实找到了与上述MT0非常相似的解决方案。

其他一些亮点:

  • 如前所述,使用IDDFS查找最短的获胜路线。
  • 由于它是DFS的核心,因此它还将发现每条路线是否以我们英雄的死亡而结束,并就大多数丧尸丧生而言跟踪“最佳”路线。死一个英雄!
  • 我已经对该算法进行了测试,以使其更有趣地观看出于高尔夫目的而观看《已移除》。按照指向github的链接之一查看未发布的版本。
  • 整个过程中也有很多评论,请随时根据我的方法重新实现您自己的解决方案,或者告诉我应该如何做!
  • 内存自适应路由快速转发
    • 直到可用的系统内存,都将跟踪未导致死亡的“结束路由”。
    • 使用奇特的路由压缩和解压缩例程,可以恢复IDDFS先前迭代的进度,以防止重新发现所有先前访问的路由。
    • 作为故意的附加奖励,充当死路路线的选择。死端路由不会存储,在IDDFS的未来深度中也不会再次访问。

求解器的历史

  • 我尝试了一堆单步向前的算法,虽然对于非常简单的场景它们可以工作,但最终它们会变得平淡无奇。
  • 然后,我尝试了两步式的前瞻算法,这是令人不满意的。
  • 然后,当我意识到这种方法可以简化为DFS时,我便开始进行n步前瞻,但是DFS远不止于此……更优雅。
  • 在构建DFS时,我想到IDDFS将确保(a)找到最佳的HERO(死亡)路线或(b)第一个获胜周期。
  • 事实证明,构建一个胜利周期检查器很容易,但是在获得一个可以证明是成功的检查器之前,我不得不经历几次非常非常错误的迭代。
  • 考虑到MT0的获胜路径,消除了三行过早的优化,这使我的算法无视了。
  • 添加了自适应路由缓存算法,该算法将使用您提供的所有内存,以防止IDDFS调用之间不必要的工作重做,并且还将死端路由剔除到内存限制。

(行进的)代码

转到代码(在此处在此处获取非高尔夫版本):

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);}}

从github获取代码,以跟踪我所做的任何更改这是我使用过的其他一些地图。

输出例子

参考解决方案的示例输出:

    $ 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

读取路由输出,如下所示step::sourceroute-to-get-here- ammo。因此,在上述解决方案中,您将其读取为:

  • 在步枪0,在前哨1与弹药100
  • 在步骤中1,使用路线13终点弹药到达前哨站97
  • 在步骤中2,使用路线11终点弹药到达前哨站95
  • ...

结束语

因此,我希望我的解决方案更难被击败,但是请尝试!对我使用它,添加一些并行处理,更好的图论,等等。我认为有几点可以改进这种方法:

  • 随着算法的发展,积极地“减少”循环以消除不必要的重读。
    • 一个示例:在示例问题中,将循环1-2-3和其他排列视为“一步”,以便我们可以更快地结束循环。
    • 例如,如果您在节点1上,则可以(a)转到2,(b)转到1,(c)第一步经过1-2-3,依此类推。这将使解决方案能够将深度折叠成宽度,增加特定深度处的路线数量,但可以大大加快长周期的求解时间。
  • 剔除死路。我当前的解决方案不能“记住”某条路线是死路一条,而每次都必须重新发现它。最好在确定死亡的路线上尽早追踪,并且永远不要超越死亡。做过这个...
  • 如果谨慎,可以将死路线剔除应用为子路线剔除。例如,如果1-2-3-4总是会导致死亡,而求解器将要测试路径1-3-1-2-3-4,则它应立即停止沿该路径下降,因为这样可以保证结束感到失望 通过一些仔细的数学运算,仍然有可能计算出击杀次数。
  • 任何其他将内存换为时间的解决方案,或允许积极避免遵循死胡同路线的解决方案。也做到了!

好答案!当他们是唯一可以解决问题的人时,谁需要宣传自己的密码?我现在有动力编写自己的解决方案,所以我将继续努力。
Rainbolt 2014年

太好了,那是我希望这样做的结果。随意从我的答案中借用/窃取您认为有用的任何内容!尽管我当然希望其他人,但不仅仅是我和OP会尝试解决:P
ProgrammerDan

我陷入困境,开始精简您的代码。如果您之前认为自己的回答很怪诞,请查看以下内容:tny.cz/17ef0b3a。仍在进行中。
Rainbolt 2014年

哈哈,您确实的确陷入了困境。到目前为止看起来不错(对于代码高尔夫球来说,这太可怕了?您知道我的意思)!
程序员

@Rusher到目前为止有运气吗?我已经提出了一些我正在酝酿的改进建议,包括路由表示压缩技术以及一种快速处理已经处理过的路由的方法。
ProgrammerDan

2

解决方案的一些抽象说明

如果我有时间,我会将其转换为算法...

对于给定的图,G则存在一个G'包含Town 的连接子图1。如果有一个无限溶液,然后将存在一个连接的子图G''G'含有V城镇和P路径。

路径PG''可被分割,使得{p}包含具有在所有路径的最小然后成本的路径PP/{p}为所有其它路径(形成生成树或可能是周期)。如果我们假设p不是(连接两端同一个镇),那么它将连接两个镇(一个循环的边缘v1v2),并具有成本c弹药,那么你(幸存者)然后遍历从v1v2和回的总成本2c弹药并且这将使所有城镇的弹药增加2(总共增加2 2|V|G''-其中一些是从v1和收集的v2)。

如果您从旅行v1v2和回v1多(m)次,然后走了一趟,从v1沿边缘P/{p}访问所有比其他城镇v1v2返回之前v1,这需要n路径来实现(在那里|P/{p}| ≤ n ≤ 2|P/{p}|,因为你永远不应该需要遍历的路径更两次),费用为k,城镇将获得2m|V|弹药(同样,其中一些会在穿越过程中被收集)。

有了所有这些,您就可以判断出如果成本k + 2mc等于或低于总报酬,是否有无限可能解决2(m+n)|V|

这个问题还有一个额外的复杂性:

  • 您可能需要从起始城镇1{p}第一次迭代,并且需要将此费用考虑在内;和
  • 您还需要确保mn足够低,以确保在第一次迭代之前不会用完弹药,因为第一次迭代的成本要高于后续迭代。

这导致问题中的示例采用24条路径成本中性解决方案(数字是所访问的城镇):

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 ...

要增加的一小件事-您可能必须考虑以1的成本循环边缘,因为如果您能够及时到达边缘,则仅这些边缘会构成获胜条件。
Rainbolt 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.