Codeforces 573C Bear and Drawing

题意

你要在两条平行直线之间画一棵有n个节点的树,树必须是可平面的(意思就是不能有两条链发生相交的情况)。给出你若干个连边(他们已经组成一棵树),问你是否可能画出符合要求的树。

思路

通过小数据找规律,可以要想把一棵树的安排在两条平行直线上,这棵树需要满足以下几个条件:

  • 首先我们在树上找到一条主链
  • 凡是在主链上的点,它的度数没有限制
  • 与主链直接相连的点,它的度数最大为3
  • 剩下的点,度数最大为2

知道了这些,接下来就是实现的问题了。这里有一种很聪明的实现方法。

  • 首先,统计所有节点的度数。
  • 在每一个叶子节点都发动一次DFS,在DFS过程中若遇到的一切度数小于等于2的点,进入,并做删除标记,否则返回。这一步的目的是清除掉所有叶子节点所属的链(不能直接修改度数,否则会导致删掉整棵树)。
  • 重新统计每个节点的度数,因为刚才只是做标记,没有改变度数。
  • 接下来对于所有曾经为3度现在为1度的点打删除标记,这种点是与主链直接相连的点。
  • 重新统计每个点的度数。
  • 现在每个节点的度数都理应小于3了,因为我们刚才去掉了叶子节点所在的链,如果这棵树正确,一定会删到主链上。接下来又去掉了曾经3度删完1度的节点,这相当于完全去掉了支链。这个时候整棵树理应只剩一条链了,不可能再有度数大于2的节点了。

如果理解有困难,不妨尝试画图帮助理解。

代码

 

POJ 2386 Lake Counting / HDU 1241 Oil Deposits

题意

在一片空地上,出现了若干水洼,对于一个有水的格子,他的八连通格子(上下左右左上左下右上右下)都可以看成和他连通形成一个大水洼。现在给你空地上水的配布,你要数出有多少个水洼。

思路

典型的“水洼问题”,利用DFS解决连通性问题是十分方便的。本题只要能够正确实现DFS就可以通过了,实现DFS的要点主要是正确地使用标记和正确的递归写法。对于DFS的实作原理不太理解的话请先思考树的先序遍历,可以参考我写的这篇文章

代码

 

初步认识图和暴力搜索

[toc]
图(Graph)作为一种非常常见且有效的模型和抽象手段,不仅是在竞赛中,乃至整个CS的学习和应用过程中都有着举足轻重的地位,尽早掌握一些关于图的概念也能辅助我们更好地分析手边的问题。本文介绍一些关于图的最基本概念和实际应用上的一些基本技术。

图的基本知识

在这里我给出这样一个问题:你站在1上,1能走到3,1能走到2也能从2走回来,7能走到5也能从5走回来,7能走到6也能从6走回来,2和5和6互相连通,1自己能走到自己,3能走到4.那么你都能走到那些点呢?

你可能很自然的会想到把我上面说的那一坨给画成一个“路线图”或“地图”,这样只要一看这个图就可以一目了然了,而接下来要说的计算机/数学中的“图”就是这样的一种抽象方式。

来和“图”桑见一面

图的定义很简单,给你几个圆,再把圆和圆之间拿线连起来,就可以称为图了。

定义: 由若干个不同的顶点与连接其中某些顶点的边组成的图形称为图。

g01
An Example of Graph

上面这个就是一个典型的图了,这个图反映的就是我上文中说的那些关系,图里绿色的圆,称为“节点/顶点(Node/Vertex)“,为了接下来叙述的方便,我给每一个节点都挂上了一个编号,在实际应用中,节点上能够挂载任何你需要的信息。圆与圆之间连接着的线,不管长成什么样,都称为“边(Edge)”,边上面也可以挂载信息。

我们发现了,上图中边中,有的是拿直线段表示的,这种边称为“无向边”,比如上面图中的2号点和5号点的连边,这样的边表示“一端(2号点)和另外一端(5号点)可以经由此边互相到达”。‘

而3号点和4号点之间的连边上面有一个箭头,方向是3号到4号,这样的边称为“有向边”,含义是“你只能从一端(3号)经由此边到达另一端(4号),但是并不能反过来”。这样看来,其实无向边可以看成两条方向互反的有向边,这一条性质非常有用。

有的时候,在两个节点之间,可能有着多条无向边或者是相同方向的有向边,这种情况,称为“重边”。

然后再看1号点,我们发现他自己居然和自己连了一条边,这样的边称为“自环”。

而看一下2号、5号、6号和7号,我们发现这四个节点,和他们之间的边共同组成了一个“环”,如果我们以一个方向沿着环行走,恐怕是一辈子也出不去了-_-#。组成环的边可以是无向的,也可以是有向的,沿着一系列的有向边行走,最后走回到了起点,这些有向边就组成了“有向环”。

给图挂载信息

我们先假设这样一种问题情境:你自驾去了一个旅游区,现在正在景区门口盘算着旅游的路线,你知道景区里面没办法加油,所以你已经加满油了,你的油箱容量是K个单位,现在给了你景区的平面图,上面记载了两个景点之间连接着的最近道路的距离(已经被折算成了通过一次要消耗的油量)和你来景区之前调查好的每一个景点的美丽程度。你需要设计一种路线,使得可以用有限的K个单位的燃料,游玩的景点的美丽程度之和最大(你不用考虑游玩结束没油怎么回景区的问题,因为你的保险公司提供免费的拖车服务)。

这个问题就是图论派上用场的时候了,我在这里随便举了一组实例:

g02

 

景区大门和每一个景点,都可以抽象为图上的一个顶点,而每一条道路都可以抽象为图上的一条边。

我们知道每一个景点都有一个“美丽程度”,这个美丽程度就体现在上面这个图里面每一个节点里填着的数字,我们把这种称为“点权”。

每一条道路,都有一个“耗油量”,体现在图上就是边上的数字就是“边权”。

所谓“权(Weight)”,其实就是一种定量的评价标准,怎么样理解附挂在图上的权,是我们自己决定的。

而图上附挂的信息又不仅仅是权值,理论上我们可以给图的任何部分挂载任何我们想要的信息,试想一下,如果我们给每一个节点挂载一个列表,上边记载了从这个可以到达的全部节点的编号,会怎么样?如果我们给每一条边挂载一个数对,上面记载了边的两端所连接的节点的序号,会怎么样?

而接下来要讲的“图的存储”,其实就是几种“挂载信息”的方式。

图的常见存储方式

计算机是不长眼睛的,你也不可能指望它看到上面我们画出来的圆和线就能解决问题。因此我们要把手中的图通过一种计算机可以理解的方式进行转化并存储才行。

常见的存储方式有四种:“邻接矩阵”、“邻接表”、“前向星”、“链式前向星”。他们各有各的特点,其中“前向星”和“链式前向星”其实是邻接表的实现方式,这两种里面我们只需要学习“链式前向星”就可以了。

邻接矩阵

首先,为了方便起见,我们要把图中每一个节点都编号,在这里我们编号1~n。接下来创建一个n*n的全是0的矩阵(或者其他你设定的用来表示无法联通的值),然后我们挨个处理图中的每一条边,对于一条有向边,从a号点指向b号点,那么我们就把矩阵的a行b列修改为1,表示从a到b可以通过。对于一条无向边,连通着a号点和b号点,那么我们就把矩阵的a行b列、b行a列都修改为1,因为无向边可以看成两条方向互反的有向边。

如果你想给边挂边权,怎么办?很简单,矩阵的数值就可以当成边权来用,不过如果你的权值和矩阵中你设定的标记冲突,你就要单独开一个数组来记录了。

如果你想给点挂点权,也需要单独开一个数组来记录。

邻接矩阵的特点是查询一个点到另外一个点之间是否有边和这条边的信息,O(1)就可以查到。但是如果我们要询问一个点连出/连入的所有边(也就是所谓的“遍历”),就必须要检查邻接矩阵的一整行/一整列,这样复杂度就是O(n)。同时,因为是使用数组保存,如果图里有重边,你最终只能保存最后保存的那一条边,也就是会丢失信息,因此一旦有无法忽略的重边,就一定不可以使用邻接矩阵。

看到这里你可能觉得邻接矩阵有点傻,那你可就错了。求多源最短路的Floyd-Warshall算法,求传递闭包的Floyd-Warshall算法,有限状态自动机的状态转移方法数等等都是要使用邻接矩阵的。

邻接表(STL vector实现)

还记得上文的“如果我们给每一个节点挂载一个列表,上边记载了从这个可以到达的全部节点的编号,会怎么样”吗?其实这就是邻接表的一种实现方式。

我们依然要给每一个节点编号,不过这时更多的是为了方便了(如果不编号,就可能要借助指针)。对于每一个点,我们给它挂上一个vector,在这里我给他起名叫to。然后我们还是挨个处理原图中的每一条边。对于一条从a到b的有向边,我们只要对a中的to.push_back(b)就可以了。对于无向边,因为无向边可以看成两条方向互反的有向边,所以就是a.to.push_back(b)和b.to.push_back(a)两步。

在这里我用了个结构体,这是为了方便我们挂载信息的。如果我们想挂点权,直接在Node里面多来一个int什么的就好,如果想挂边权,就再来一个vector,与to同步push_back对应边的权值。

邻接表非常常用,首先他是以点为主体,边是作为点的信息存在的,这一点很符合我们的思考规律。其次因为边采用vector保存,所以不用担心重边丢失的问题。邻接表逊色于邻接矩阵的地方恐怕就只有查询两个点之间是否有边了,在邻接表里面你必须遍历整个当前点上的to才能判断是不是能和另外一个点相连。但相对的,如果图中的点连接的边不多的话(这时候的图可以称为“疏松图”,反之,“稠密图”),邻接矩阵下的O(n)的遍历在邻接表下就快多了。

链式前向星(静态建表的邻接表实现)

上文中我们还提到了“如果我们给每一条边挂载一个数对,上面记载了边的两端所连接的节点的序号,会怎么样?”,其实链式前向星就是这种思路。

在这里假定你已经很明白链表的工作方式了,链式前向星其实就是图上所有边按照某种特殊的方式组成的链表。因为比较难理解,我先上代码。

使用上,还是先编号,然后挨个边处理,直接调用addEdge()方法就可以,查询一个节点所连出的边是,使用上面代码里iteration()里的方法,利用一个略微奇怪的for循环就可以办到。

我们不妨直接从代码的实现来分析链式前向星的工作原理,结构体里面的head是每一个节点在容器(数组)中所对应的第一条边的位置,next是每一条边在容器中所对应的同一起点的下一条边的位置,to则是真正的存储某一条边是指向哪一点的。

再看加边操作,注意到方法里的q是静态变量,这一点非常重要,q的作用是指示当前存储边的容器的末端(注意“末端”指的是并不是最后一个元素,而是其后的空地)的位置,相当于STL迭代器的end(),每一次加边的时候,要在to的末端写入新加边的信息,然后通过head[_from]提取出起点最近加的一条边的位置,然后我们要把新加的边的next指向那一条边,最后修改head,使得最近添加的边变更为新边,同时末端向后移动以供下一次添加之用。

理解了这样的加边方法,查询一个点连出的边的方法也就很好理解,要查询一个点的连出边,我们要先查head,知道这个点最近添加的那条边在哪里(查询结果在这里是j),然后比这条边早一些添加的就是next[j],再早一点的就是next[next[j]],更早一点的是next[next[next[j]]],再早一点的是……,就这样我们一直往时间添加时间更早的边查,直到查到空节点(用来标记链表结束)。

可以发现,链式前向星相对于邻接表的vector实现,常数更小。同时如果想直接遍历每一条边,而不考虑顺序,可以直接遍历to数组(当然,为了知道每条边的起点,你可能还需要建一个from数组),这就使得我们可以从边入手解决问题。因为是邻接表,我们也可以以点为中心来考虑问题。基本可以说链式前向星在各方面都优于邻接表的vector实现。

如果你不用数组,而是用指针来实现,那就是“前向星”了。

暴力搜索

在讲搜索之前,我们还要再来一点关于图的话题,那就是“树”,当然不是种出来的树,而是一种特别一点的图,不过就像你在大街上看到的各种树一样,能被称为一棵“树”的图一定要满足以下几种特征:

  • 每两个点之间有且仅有一条路径
  • 没有环,而且只要再添加一条边,就一定会出现环
  • 只要去掉一条边,就无法保证第一条性质
  • 只要增加一个点,为了保证第一条性质就必须还要增加一条边
An Example of Tree
An Example of Tree

如果只有光秃秃的一个节点,其实也可以称为树,树上的边的方向性也没有要求,可以有向也可以无向,当有多棵“树”同时存在的时候,这些互相没有相交的树共同组成的图也被称作“森林”。

g04
An Example of Forest

自然界的树都是有根的,同样的,有时候为了分析问题的便利,我们也会选取树上的某个节点作为“根节点”。选取了根节点的树这时候也称为“有根树”。

选取了根之后,伴随之而来的还有节点与节点之间的“辈分关系”,我们把根视为辈分最大的祖先,他连接的所有点都是它的“孩子”,而对于他连接着的所有点来说他就是这些点的“父亲”。这样的关系对于所有的节点都是一样的,我们注意到了,在一棵树上,每个节点可以有好多孩子,但是只能有一个父亲,而对于没有孩子的节点,我们对他还有一个称呼叫“叶子节点”,对于有同一个父亲的若干个节点,他们互称“兄弟节点”。

Relation on a Tree
Relation on a Tree

细心的你也发现了,上面这幅图从上面到下面颜色是越来越深的,没错,我想借此来表达有根树的“深度”概念。对于根部,他的“辈分”最大,但是他的“深度”是最小的,父亲的深度始终是比孩子节点的深度大1的。

理解了上面的这些概念,我们就可以开始说“暴力搜索”了。

我们说的“搜索”是什么

我们对搜索的认识,恐怕都是来源于“百度”、“谷歌”,也就是“从所有的信息中找出我们需要的信息的这一过程”,而我们现在接下来要说的“搜索”虽然感觉上还是和你键入几个关键字然后敲回车有一定区别的,但是实质上其实是一样的,不过现在这个过程要交给你亲自去实现。

试想这么几个问题,给你一个表示各个景点之间连通关系的图,现在让你找13号点在哪里,你如何“系统地”完成这个寻找过程?给你一个迷宫,里面有好多墙壁,现在要你找出脱出迷宫的最短路线,你又该如何“系统地”完成这一过程?更直接地,给你一棵树,制定一个根部,你能“系统地”把这棵树的亲子关系和深度都算出来吗?

这些问题,对于你来说可能就是瞟一眼那样简单,不过你知道计算机可是没有眼睛的,也没法扫视全局,因此“系统性”的算法就显得非常重要,也就是说你要有一种“套路”,而不是随便去瞟一眼这么简单。

接下来我们要介绍两种最为常见的搜索“套路”,深度优先搜索和广度优先搜索。在学习这些之前,我已经假定你理解了我上面所讲的的概念,以及递归(调用栈)队列的知识。

深度优先搜索(Depth First Search)

理解深度优先搜索(DFS)的最佳途径就是从“树的先序遍历(DLR)”开始,所谓“遍历”就是把树的每一个节点都走一遍。树的先序遍历,是指对于给定的一棵有根树,从这棵树的根部开始,不断尝试进入儿子节点(下潜),直到进入了叶子节点,然后才会返回到上一层,继续查找下一个儿子节点进入,通过不断重复这一过程,直到所有的节点都被进入并最终返回根部。简单的说就是:不断尝试探底,不走重复的路。下面举个例子来解说:

在这里我给出了一棵树,并且设定好了根部,同时因为我们的起点在根部,我把根部标记为1.

g06
Example Tree to Show DLR

根节点(1号节点),连接了两个儿子节点,按照上文所述的思想,我们优先访问最左面的那个,我们把它标记为2,并进入这个节点。

g07

 

进入2号节点之后,我们顺着刚才的思路,不断下潜,最后我们到达了叶子节点4号。

g08

 

现在我们发现没有可以用的孩子节点了,那么我们就要回到当前节点的父亲那里去,也就是3号节点。在3号节点,我们发现了除了已访问的4号节点之外的另外一个儿子,我们把它标记为5号节点,并进入。

g10

我们在5号节点又遇到了4号一样的情况,没有儿子,那样的话我们就要返回父亲节点3号。当我们返回3号,我们发现儿子已经走了个遍了,这个时候我们又要返回父亲节点2号,在2号同样没有可以没走过的儿子了,我们返回到2号的父亲节点1号。

g11

等我们回到了一号节点,我们又可以找到新的没有走过的儿子了,于是我们按照刚才的调子,进入这个儿子,这个节点已经是我们走过的第6个新节点了,给他标号6,然后我们又找到了他的儿子,进入,标号7。

g12

7号节点又是叶子节点,于是我们返回到他的父亲6号,然后发现还有一个没走过的儿子,于是进入,标号8。

g13

8是叶子节点,返回6,6没有没走过的儿子,返回1,1没有没走过的儿子。好了,任务完成了!

g14

以上就是“树的先序遍历”,是不是很简单?而DFS其实就可以理解成先序遍历在各种其他情境的推广,比如各种“图的遍历”、迷宫搜索(可以转化为图)、找出联通块(水洼问题)等等。通过思考我们也发现了,DFS实现起来的最佳方式就是递归,递归可以完美的还原我们的“下潜(发动递归)”和“返回(函数返回)”动作。下面我给出使用DFS遍历图的实现,使用了vector邻接表,所有访问到的节点,均会在标记数组vis中被标true。

如果你还是一头雾水,不妨试着将上面我做演示的那棵树建立成图,然后手动模拟一边这份实现的执行过程,就能理解了(前提是你必须知道递归的原理)。

一旦你发动了dfs(a),那么在搜索执行完毕之后,所有与a号节点直接或间接连通的点都会被在vis中标记,所以只要对这份实现稍加改动,我们就可以解决“水洼问题”了。而要想解决“迷宫出路”这种问题,我们还要稍微动下脑筋,思考一下vis这个标记数组应该怎样活用,怎样利用vis让dfs达到我们想要的效果(在这份实现中,一旦一条路被走过,就无法再次走过,而在迷宫中,我们很有可能要走之前走过的道路,这样我们就要适时地复原vis标记)。

广度优先搜索(Breadth First Search)

相对于DFS,广度优先搜索(BFS)也许更加直观一点,我们还是从“树的遍历”说起。对于一棵给定的有根树,我们依然从根开始入手,不过这次我们建立了一个队列,队列存储的是“状态”,最开始队列是空的,要开始搜索,我们要给他加入“当前在根节点”这一状态,之后我们不断拉取队伍的队首,然后分别把我们进入了各个儿子的状态逐一加入队列,直到队列清空,这个时候我们一定把每一个“在XX节点”的状态都经历过了,所以我们是走遍了整棵树。

我在这里依然用在DFS里的那棵树来举例子,图下面的方括号,表示队列的变化,左半方括号表示队列头,右半方括号表示队列尾。

最开始,我们的队列里只有1号状态,我们给它拿出来,然后发现他有两个儿子,于是分别创建进入2号儿子的2号状态和进入3号儿子的3号状态,并将他们加入队列尾端。

g15
[1] -> [2,3]
接下来,我们取出队列头的2号状态,我们发现它只有一个儿子,我给他标号4,于是把“进入4号节点”的4号状态加入队列。

[2,3] -> [3,4]
[2,3] -> [3,4]
接下来,取出队列头的3号状态,我们发现它有两个儿子,在这里标号为5和6,于是借此生成5号和6号状态,加入队列。

[3,4] -> [4,5,6]
[3,4] -> [4,5,6]
之后,取出队列头的四号状态,发现在这个状态所在的4号节点有两个儿子,在这里标号7和8,同时据此创建状态并加入队列。

[][]
[4,5,6] -> [5,6,7,8]
最终,队列弹出的5,6,7,8号状态均是在叶子节点的状态,无法创建新状态,队列就此清空,我们完成了遍历任务!来看一下我们整个BFS的全貌吧:

State tree of the whole BFS process
State tree of the whole BFS process

相信你已经可以基本理解BFS的工作方式了。

显然光会个遍历树是不够的,将上面所讲述的思考方式拓展一下,BFS也可以完成“图的遍历”,“迷宫最短路”等问题,下面我会给出一种用于遍历图的BFS实现,图的存储方式采用vector实现的邻接表。

 

选择适合的搜索方式

一招鲜显然并不能吃遍天,DFS,BFS中的任何一个都不能应付全部的场合。DFS使用递归实现,因此当搜索深度比较大的时候就存在巨大的栈溢出风险,而实用模拟栈实现的DFS又十分复杂,得不偿失,因此当栈空间不足以支持DFS时,必须换用BFS。BFS相对于DFS,实现相对比较繁琐,如果使用STL queue的话,会带来一些常数上的劣势(这种劣势在SPOJ等黑心OJ上十分明显),模拟队列又可能会占用大量内存。因此我们需要根据问题的实际来考虑使用哪种搜索方式。

一般来说,DFS在求连通性问题上具有十分的优势(Tarjan SCC/DCC算法均基于DFS),而BFS在求最短路问题上占上风(Dijkstra/SPFA算法均基于BFS),掌握好这两种算法是学好图论的基础。如果你够细心的话,你会发现我们在DFS的同时给树进行了编号,那个编号其实叫做“DFI(DFS序)”,这个东西有着一些神奇的应用。同时DFS和BFS又可以升级为一种考虑问题的思想,当然这些高级一些的问题就需要我们在练习的时候慢慢体会了。

 

本文系hahaschool原创,同时作为Tic ACM Club讲稿,未经许可禁转载。

 

15多校2-9/HDU 5308 I Wanna Become A 24-Point Master

更新

根据出题人的回复,问题应该已经修复了,本文代码中的PATCH部分可以忽略。关于原来出现的问题的证明也已经失效,敬请留意。

题意

给出一个数字n,问你如果给你n个n,你要怎么样才能利用加减乘除运算给算成24呢?注意,没个数字只能使用一次,所有的数字必须都要用到。你要按照题里面介绍的特别的表示方法把你的解法表示出来。除法是可以出分数的,因为是SPJ,应该可以过。

思路

我个人不太认同出题人题解里面给出的手算4~15的所有情况,对于较大的数再机智得直接消除凑24,因为手算的太多了。

我的思路是对于比较小的情况,即n <= 27的时候,可以直接利用dfs找出答案。因为题目给出的表示方法其实意外地适合dfs的搜索过程,我们不断的枚举下一个操作,即从当前没用过的数里面选出两个,然后尝试加减乘除,每一次尝试就是改变当前数组尾部的值,同时做好标记进入下一层。只要搜索成功,我们就可以在dfs的退出过程中将路径保存下来,开一个stack,把这些个结果压进去, 输出的时候弹出来,顺序就正好是正序了。

对于比较大的情况,事情就变得非常简单,假如说现在的n = k,那么只要找出24个相同的数k,加到一起,就是24 * k了,然后再额外拿一个k,做一下商,就得到了24,对于多出来的数,我们可以找两个k做一下差得0,然后用得到的0去乘上下一个多余的数,最后剩下一个0,和刚才的24加一下,多余的数也消耗掉了。

注意点

做这个题的时候发生了两处失误,首先是dfs的时候,范围设置的不正确,两层的循环都应该是从1开始的,原来外层从1开始而内层却是从i+1开始(i是外层的循环变量),这样显然丢解了,而且搜索速度也拖慢了。

其次是题目中要求每个数只可以用一次,结果我偷懒把一个0跟好多哥数字去乘了,这样肯定就错了。

然后dfs应该采取一些必要的优化,比如说跳过重复的区段(剪枝),如果不做的话可能超时。如果实在没办法,打表也可以,反正才24种情况,并不会被拒绝提交。

然而还是没过

折腾了一下午后发现很有可能这个题的评测器在实现上有漏洞,通过排(feng)除(kuang)法(jiao),发现有3组数据没有通过评测器,但是确实是正确的。

如果数据确实不符合题意,请与我跟进,谢谢。

代码

为了过,最后没有办法特殊对待给上面三组数据打了补丁。

标准答案里边很豪放地用了分数,正常来讲评测器应该是接受分数的,但不知道为什么不接受我这个调换位置保证不出现分数的答案了,真的很怪。我自己写了一个检查我自己答案的评测器,没有提供对分数的支持,但是评测器会在发现出现分数的时候报错,因为我的答案是已经调换顺序保证不出分数的了,此外也检查了每个数字的使用情况,保证和题意一致。

从1开始测了1个GB的数据,没发现问题,如果评测器有问题,请跟进,谢谢。

如果你还是不相信我

下面是官方题解:

QQ20150723-1@2x

 

 

虽然很蛋疼,写这么多其实就是为了让这么个玩意别再坑更多的人了。

SGU 101 Domino

题意

给你一堆骨牌,每张骨牌正反两面各有一个0~6范围内的数字,你要做的是确定一种骨牌的排列方式和骨牌的正反似的相邻的两个骨牌的面数字一样。

思路

这种链接多个物体,物体之间通过公共部分链接,这种题是可以转换成图论题做的。那么这个题应该怎么转换?
我们建立0~6这7个节点,每张骨牌就可以看作是链接骨牌上两个数的边,我们要做的就是找到一个路径使得我们可以“一笔”走过所有的边,其实这个叫欧拉通路。
关于欧拉通路,欧拉回路的更多细节,可以参看http://haha.school/guanyuoulahuiluheoulalujing/
找出欧拉回路的方法就是DFS,但是和以往不同的是我们把处理工作放在下一层返回之后做。同时本题还需要判断欧拉回路是否存在,如果不存在的话要输出无解。

代码

 

HDU 4035 Maze

题意

一个人被丢进了迷宫, 迷宫有n个房间,1号房间作为起点,房间之间以隧道联通,任意两个房间的通路只有一条,每进入一个房间,有k的概率被干掉,然后在1号房间复活,有e的概率直接脱出迷宫,若上述两事件未发生,则这个人会从当前房间的全部隧道中随便选一个进去(即使沿着刚才的路返回)。问你这个人要穿过多少隧道才能拖出迷宫(求期望)。

思路

注意审题,“任意两个房间的通路只有一条” ,这句话告诉我们迷宫可以抽象为一棵树(因为没有环,起点确定所以看作树根),又因为是求期望,所以这是一个树上进行的概率dp。
对于每个节点,我们定义dp数组为dp[i]表示在i号房间脱出迷宫的期望穿洞数。
接下来就是处理dp方程,如果你可以写出来dp方程的话,你会发现这个方程有两种版本,一种是叶子节点,一种是普通节点(其实还有一种根节点,不过这个只是最后出答案用的),列完之后我们会发现dp方程组成环,而且每一个方程式都有着类似的未知项(或者说是格式统一),面对这种情况就可以归纳出方程的一般形式,建立几个系数数组,回代一般形式,确定出系数数组的递推关系式,化环为链。这种情形经常出现在概率dp问题中。
最后需要注意本题有坑点:判断误解的时候,需要直接判断1-A[1](这个是什么请看下面的数学推导)的绝对值是否非常小(分母趋向于0),在这里卡精度,你至少需要1e-9的精度。在这里用其他的方法判断一律是过不了的~

相关数学推导

以下内容引用自http://mlz000.logdown.com/posts/220880-hdu-4035-maze-probability-dp,谢谢!

 

9948CE17-3F1B-4276-A48F-6E220AF5EDE8

 

代码

 

 

ZOJ 3686 A Simple Tree Problem

被这道题玩得相当惨。。。这道题是一个DFS+线段树的题。

首先这个题是在一棵传统树上面进行大规模的区间读写操作,这是线段树的玩法,但是怎么玩呢?
这个时候DFS就派上用场了,我们通过DFS得到DLR遍历,把dfs时进入一个节点是时间记下来,出去一个节点的时间记下来,就可以得到这个节点的子树在DLR遍历中的区间的开始与结束标号,这样就相当于把2维的树降到一维了。
之后得到了一个一维的数组,就可以开线段树,这里涉及区间更新,还要加开懒惰标记。
这种把树压成线然后再搞是一种很常用很好的姿势!

其实这道题卡死我的就是懒惰标记这里。我原来一直在用zkw线段树,但是因为是自下向上操作没有递归,所以我就根本不知道怎么下手写懒惰标记了,鼓捣2天未果,orz各路大神的适配zkw线段树的懒惰标记是给rmq准备的,不适合这个题,没办法研究递归版本了。

递归版本的话,实现懒惰标记的思路就是:
1.更新时,如果发生了目标区间与当前区间相交的情况,首先把当前节点的懒惰标记下传给下一层,然后再去更新下一层,更新完下一层后,用更新好的下一层更新自身。如果目标区间完全包括当前区间,这个时候就不用往下走了,首先更新自己的值,然后更新自己位置的懒惰标记。
2.查询时,如果目标区间和当前区间相交,这时候需要下传当前节点的懒惰标记,然后进入下一层接着查询,如果目标区间完全包括当前区间,直接更新答案。
3.下传时,我们只操作当前节点的子节点,不可以操作当前节点(否则就重复操作啦),注意我们只下传了一层,所以不仅要操作下面一层的值,还要操作下面一层的懒惰标记,同时清空当前节点的懒惰标记。

HDU 2553 N皇后问题

C++作业不好好写的后果。。。

注意必须要用DFS来搜索,逐行搜。

 

Scroll to top