6.5 竞赛题目选讲

例题6-17 看图写树(Undraw the Trees, UVa 10562)

你的任务是将多叉树转化为括号表示法。如图6-16所示,每个结点用除了“-”、“|”和空格的其他字符表示,每个非叶结点的正下方总会有一个“|”字符,然后下方是一排“-”字符,恰好覆盖所有子结点的上方。单独的一行“#”为数据结束标记。

图6-16 样例输入与输出

【分析】

直接在二维字符数组里递归即可,无须建树。注意对空树的处理,以及结点标号可以是任意可打印字符。代码如下:


#include<cstdio>
#include<cctype>
#include<cstring>
using namespace std;

const int maxn = 200 + 10;
int n;
char buf[maxn][maxn];

//递归遍历并且输出以字符buf[r][c]为根的树
void dfs(int r, int c) {
  printf("%c(", buf[r][c]);
  if(r+1 < n && buf[r+1][c] == '|') { //有子树
    int i = c;
    while(i-1 >= 0 && buf[r+2][i-1] == '-') i——; //找"————"的左边界
    while(buf[r+2][i] == '-' && buf[r+3][i] != '\0') {
      if(!isspace(buf[r+3][i])) dfs(r+3, i); //fgets读入的"\n"也满足isspace( )
      i++;
    }
  }
  printf(")");
}

void solve( ) {
  n = 0;
  for(;;) {
    fgets(buf[n], maxn, stdin);
    if(buf[n][0] == '#') break; else n++;
  }
  printf("(");
  if(n) {
    for(int i = 0; i < strlen(buf[0]); i++)
      if(buf[0][i] != ' ') { dfs(0, i); break; }
  }
  printf(")\n");
}

int main( ) {
  int T;
  fgets(buf[0], maxn, stdin);
  sscanf(buf[0], "%d", &T);
  while(T——) solve( );
  return 0;
}

例题6-18 雕塑(Sculpture, ACM/ICPC NWERC 2008, UVa12171)

某雕塑由nn≤50)个边平行于坐标轴的长方体组成。每个长方体用6个整数x0y0z0xyz表示(均为1~500的整数),其中x0为长方体的顶点中x坐标的最小值,x表示长方体在x方向的总长度。其他4个值类似定义。你的任务是统计这个雕像的体积和表面积。注意,雕塑内部可能会有密闭的空间,其体积应计算在总体积中,但从“外部”看不见的面不应计入表面积。雕塑可能会由多个连通块组成。

【分析】

设想有一个三维坐标范围均为1~500个三维网格,如果一开始就把输入的n个长方体“画”到网格里,接下来就可以抛开那些长方体,只在网格中进行统计了。

还记得floodfill吗?它不仅能求出连通块的个数,还能准确地找出每个连通块各由哪些方格组成。虽然本题的研究对象是三维空间中的长方体,但丝毫不影响floodfill的作用,唯一的区别就是每个格子的相邻格子从二维情形的4个增加到了三维情形的6个。

本题的麻烦之处在于雕塑中间可能有封闭区域,甚至还有可能相互嵌套,看上去很复杂。但其实可以从反面思考:不考虑雕塑本身,而考虑“空气”。在网格周围加一圈“空气”(目的是为了让所有空气格子连通),然后做一次floodfill,就可以得到空气的“内表面积”和体积。这个表面积就是雕塑的外表面积,而雕塑体积等于总体积减去空气体积。

但还有一个大问题:空间占用。坐标为1~500的整数,一共需要5003=1.25*108个单元。在第5章的例题“城市正视图”中介绍了离散化法,在这里它再次派上用场:每个维度最多只有2n≤100个不同的坐标,因此可以把500*500*500的网格离散化成100*100*100,单元格的数目降为原来的1/125。在floodfill时直接使用离散化后的新网格,但在统计表面积和体积时则需要使用原始坐标。

例题6-19 自组合(Self-Assembly, ACM/ICPC World Finals 2013, UVa 1572)

nn≤40000)种边上带标号的正方形。每条边上的标号要么为一个大写字母后面跟着一个加号或减号,要么为数字00。当且仅当两条边的字母相同且符号相反时,两条边能拼在一起(00不能和任何边拼在一起,包括另一条标号为00的边)。

假设输入的每种正方形都有无穷多种,而且可以旋转和翻转,你的任务是判断能否组成一个无限大的结构。每条边要么悬空(不和任何边相邻),要么和一个上述可拼接的边相邻。如图6-17(a)所示是3个正方形,图6-17(b)所示边是它们组成的一个合法结构(但大小有限)。

 

  (a)     (b)  

图6-17 自组合正方形

【分析】

本题看上去很难下手,但不难发现“可以旋转和翻转”是一个很有意思的条件,值得推敲。“无限大结构”并不一定能铺满整个平面,只需要能连出一条无限长的“通路”即可。借助于旋转和翻转,可以让这条“通路”总是往右和往下延伸,因此永远不会自交。这样一来,只需以某个正方形为起点开始“铺路”,一旦可以拼上一块和起点一样的正方形,无限重复下去就能得到一个无限大的结构。

可惜这样的分析仍然不够,因为正方形的数目n很大。进一步分析发现:实际上不需要正方形本身重复,而只需要边上的标号重复即可。这样问题就转化为:把标号看成点(一共只有A+~Z+,A-~Z-这52种,因为00不能作为拼接点),正方形看作边,得到一个有向图。则当且仅当图中存在有向环时有解。只需要做一次拓扑排序即可。

例题6-20 理想路径(Ideal Path, NEERC 2010, UVa1599)

给一个n个点m条边(2≤n≤100000,1≤m≤200000)的无向图,每条边上都涂有一种颜色。求从结点1到结点n的一条路径,使得经过的边数尽量少,在此前提下,经过边的颜色序列的字典序最小。一对结点间可能有多条边,一条边可能连接两个相同结点。输入保证结点1可以达到结点n。颜色为1~109的整数。

【分析】

首先回顾一下第3章中介绍的“字典序”。对于字符串来说,字典序就是在字典里的顺序。例如,ab在cd的前面,cde在a的后面,abcd在abcde的前面。这个定义可以扩展到序列:序列(1, 2)在(3, 4, 5)的前面,(4, 5, 6)在(4, 5)的后面。

抛开字典序不谈,本题只是一个普通的最短路问题,可以用BFS解决。但是之前的“记录父结点”的方法已经不适用了,因为这样打印出来的路径并不能保证字典序最小。怎么办呢?

事实上,无须记录父结点也能得到最短路,方法是从终点开始“倒着”BFS,得到每个结点i到终点的最短距离d[i],然后直接从起点开始走,但是每次到达一个新结点时要保证d值恰好减少1(如有多个选择则可以随便走),直到到达终点。可以证明(想一想,为什么):这样走过的路径一定是一条最短路。

有了上述结论,本题就不难解决了:直接从起点开始按照上述规则走,如果有多种走法,选颜色字典序最小的走;如果有多条边的颜色字典序都是最小,则记录所有这些边的终点,走下一步时要考虑从所有这些点出发的边。聪明的读者应该已经看出来了:这实际上是又做了一次BFS,因此时间复杂度仍为O(m)。其实本题也可以只进行一次BFS,不过要从终点开始逆向进行,有兴趣的读者可以自行研究。

本题非常重要,强烈建议读者编写程序。

例题6-21 系统依赖(System Dependencies, ACM/ICPC World Finals 1997, UVa506)

软件组件之间可能会有依赖关系,例如,TELNET和FTP都依赖于TCP/IP。你的任务是模拟安装和卸载软件组件的过程。首先是一些DEPEND指令,说明软件之间的依赖关系(保证不存在循环依赖),然后是一些INSTALL、REMOVE和LIST指令,如表6-1所示。

表6-1 指令说明

 

  指令     说明  
  DEPEND item1 item2 [item3 …]     item1依赖组件item2, item3, …  
  INSTALL item1     安装item1和它的依赖(已安装过的不用重新安装)  
  REMOVE item1     卸载item1和它的依赖(如果某组件还被其他显式安装的组件所依赖,则不能卸载这个组件)  
  LIST     输出所有已安装组件  

在INSTALL指令中提到的组件称为显式安装,这些组件必须用REMOVE指令显式删除。同样地,被这些显式安装组件所直接或间接依赖的其他组件也不能在REMOVE指令中删除。

每行指令包含不超过80个字符,所有组件名称都是大小写敏感的。指令名称均为大写字母。

【分析】

这道题目在概念上并没有什么难点,但是有一些细节问题容易写错。首先,维护一个组件名字列表,这样可以把输入中的组件名全部转化为整数编号。接下来用两个vector数组depend[x]和depend2[x]分别表示组件x所依赖的组件列表和依赖于x的组件列表(即当读到DEPEND x y时要把y加入depend[x],把x加入depend2[y]),这样就可以方便地安装、删除组件,以及判断某个组件是否仍然需要了。

为了区分显式安装和隐式,需要一个数组status[x],0表示组件x未安装,1表示隐式显式安装,2表示隐式安装,则安装组件的代码如下:


void install(int item, bool toplevel) {
  if(!status[item]) {
    for(int i = 0; i < depend[item].size( ); i++)
      install(depend[item][i], false);
    cout << "   Installing " << name[item] << "\n";
    status[item] = toplevel ? 1 : 2;
    installed.push_back(item);
  }
}

删除的顺序相反:首先判断本组件是否能删除,如果可以删除,在删除之后再递归删除它所依赖的组件:


bool needed(int item) {
  for(int i = 0; i < depend2[item].size( ); i++)
    if(status[depend2[item][i]]) return true;
  return false;
}

void remove(int item, bool toplevel) {
  if((toplevel || status[item] == 2) && !needed(item)) {
    status[item] = 0;
    installed.erase(remove(installed.begin( ), installed.end( ), item),
                       installed.end( ));
    cout << "   Removing " << name[item] << "\n";
    for(int i = 0; i < depend[item].size( ); i++)
      remove(depend[item][i], false);
  }
}

例题6-22 战场(Paintball, UVa 11853)

有一个1000×1000的正方形战场,战场西南角的坐标为(0,0),西北角的坐标为(0,1000)。战场上有n(0≤n≤1000)个敌人,第i个敌人的坐标为(xi,yi),攻击范围为ri。为了避开敌人的攻击,在任意时刻,你与每个敌人的距离都必须严格大于它的攻击范围。你的任务是从战场的西边(x=0的某个点)进入,东边(x=1000的某个点)离开。如果有多个位置可以进/出,你应当求出最靠北的位置。输入每个敌人的xiyiri,输出进入战场和离开战场的坐标。

【分析】

本题初看起来比较麻烦,不妨把它简化一下:先判断是否有解,再考虑如何求出最靠北的位置。首先,可以把每个敌人抽象成一个圆,圆心就是他所在位置,半径是攻击范围,则本题变成了:正方形内有n个圆形障碍物,是否能从左边界走到右边界?

   

图6-18 战场示意图

下一步需要一点创造性思维:把正方形战场看成一个湖,障碍物看成踏脚石,如果可以从上边界“走”到下边界,沿途经过的障碍物就会把湖隔成左右两半,相互无法到达,即本题无解;另一方面,如果从上边界走不到下边界,虽然仍然可能会出现某些封闭区域(图6-18中灰色区域),但一定可以从左边界的某个地方到达右边界的某个地方,如图6-18所示。

这样,解的存在性只需一次DFS或BFS判连通即可。如何求出最北的进/出位置呢?方法如下:从上边界开始遍历,沿途检查与边界相交的圆。这些圆和左边界的交点中最靠南边的一个就是所求的最北进入位置,和右边界的最南交点就是所求的最北离开位置。