这是一道很有意思的图论题,分析本问题之前必须先理解一笔画问题的原理,不难扩展出以下定理:
若一个连通无向图的奇顶点个数为n
(1) 当n为0时,图可以一笔画(且可以构造一个回路)
(2) 其他情况,图可以n/2笔画(注意n一定为偶数)
对于多联通分量的无向图,每个联通分量单独计算k笔画的情况,最后累加即可
对于本题,将线段看成图的边,端点和交点看成图的顶点,画n遍转换为每条边重复出现至n次,而:笔抬起的次数=笔画数-1,这样题目就转化为下面这样的问题:
给定一个图,求最少可以几笔画完,每条边不能重复也不能遗漏
模型建好了,但是在编程计算的过程中还有以下难点:
1. 如何处理互有覆盖(overlap)的线段?
题目里已经很清楚的指出覆盖的区域也应该只被画一次,所以必须对这种情况进行处理:将所有有覆盖的线段合并成一个更长的线段,以保证没有任意两条线段再重叠,详见代码。
2. 如何划分出各个联通子图?
按照定理,必须先划分出联通子图分别求解才能计算出总的笔数,不能默认大图一定联通。可以按照基本的图的数据结构来计算,求出所有的交点和端点,判断点和点之间的连接情况,再对图进行一个搜索按连通性对节点进行划分,可想而知这样会相当繁琐。考虑到原图的基本元素就是线段,若划分出了线段也就划分出了联通图,所以可以根据线段的相交情况进行划分,这里需要用到一个数据结构并查集
3. 如何计算每个联通子图的奇顶点的个数?
同上,用基本的图模型来计算是相当繁琐的,考虑到交点的生成受到线段相交性(线段相交的情况)的影响,可以进行如下分析:
两条线段相交只有三种情况:十字交(╋)、丁字交(┳)、端点交(┏)
已知图G和线段L,L不在G中,G的奇顶点个数为odd(G)。
若把L加入到G,L与G的每一次相交都会给odd(G)带来如下变化:╋+0,┳+0,┏-2,同时odd(G)还要加上L的基础量2,即:
( egin{equation} egin{split} odd(G+L) &= 2 + odd(G) + sum_{相交x} f(x) end{split} end{equation} )
( f(x) = -2(端点交) or 0(其他) )
实际上,在2中用并查集求联通分量的时候就可以一并把奇顶点个数计算出来,但在配合并查集的时候上述公式需要做一点变换,易推,详见代码。

1 class UnionSet: 2 def __init__(self, segs): 3 self.gs = [Graph(seg) for seg in segs] 4 5 def union(self, x, y, q): 6 i = self.find(x) 7 j = self.find(y) 8 if i != j: 9 self.gs[i].extend(self.gs[j]) 10 self.gs[i].oddNodeCount += q + self.gs[j].oddNodeCount 11 self.gs.pop(j) 12 else: 13 self.gs[i].oddNodeCount += q 14 15 def find(self, x): 16 for i in range(0, len(self.gs)): 17 if x in self.gs[i]: 18 return i 19 raise Exception() 20 21 def graphs(self): 22 return self.gs 23 24 25 class Graph(list): 26 27 def __init__(self, seg): 28 list.__init__([]) 29 self.oddNodeCount = 2 30 self.append(seg) 31 32 def penCount(self, n): 33 if n % 2 == 0: 34 return 1 35 if self.oddNodeCount == 0: 36 return 1 37 else: 38 return int(self.oddNodeCount / 2) 39 40 41 class Segment: 42 def __init__(self, segStr=None, point=None): 43 if segStr: 44 sp = segStr.split(' ') 45 self.x1 = int(sp[0]) 46 self.y1 = int(sp[1]) 47 self.x2 = int(sp[2]) 48 self.y2 = int(sp[3]) 49 else: 50 self.x1 = point[0] 51 self.y1 = point[1] 52 self.x2 = point[2] 53 self.y2 = point[3] 54 55 if self.x1 == self.x2: 56 self.isHor = False 57 if self.y1 > self.y2: 58 temp = self.y1 59 self.y1 = self.y2 60 self.y2 = temp 61 else: 62 self.isHor = True 63 if self.x1 > self.x2: 64 temp = self.x1 65 self.x1 = self.x2 66 self.x2 = temp 67 68 def cross(self, seg): 69 if self.isHor == seg.isHor: 70 return 4 71 if self.isHor: 72 seg1 = self 73 seg2 = seg 74 else: 75 seg1 = seg 76 seg2 = self 77 78 if seg1.x1 <= seg2.x1 <= seg1.x2 and seg2.y1 <= seg1.y1 <= seg2.y2: 79 if seg2.x1 in [seg1.x1, seg1.x2] and seg1.y1 in [seg2.y1, seg2.y2]: 80 return 3 81 elif seg2.x1 in [seg1.x1, seg1.x2] or seg1.y1 in [seg2.y1, seg2.y2]: 82 return 2 83 else: 84 return 1 85 86 else: 87 return 4 88 89 def isoverlap(self, seg): 90 if self.isHor != seg.isHor: 91 return False 92 if self.isHor and self.y1 == seg.y1: 93 return self.x1 <= seg.x1 <= self.x2 or self.x1 <= seg.x2 <= self.x2 or seg.x1 <= self.x1 <= seg.x2 or seg.x1 <= self.x2 <= seg.x2 94 elif not self.isHor and self.x1 == seg.x1: 95 return self.y1 <= seg.y1 <= self.y2 or self.y1 <= seg.y2 <= self.y2 or seg.y1 <= self.y1 <= seg.y2 or seg.y1 <= self.y2 <= seg.y2 96 97 def overlap(self, seg): 98 x1 = min(self.x1, seg.x1, self.x2, seg.x2) 99 x2 = max(self.x1, seg.x1, self.x2, seg.x2) 100 y1 = min(self.y1, seg.y1, self.y2, seg.y2) 101 y2 = max(self.y1, seg.y1, self.y2, seg.y2) 102 return Segment(point = (x1,y1,x2,y2)) 103 104 class PenLift: 105 def _combineSegments(self, segments): 106 ss = [Segment(s) for s in segments] 107 i = 0 108 while i < len(ss): 109 j = i + 1 110 while j < len(ss): 111 if ss[i].isoverlap(ss[j]): 112 ss[i] = ss[i].overlap(ss[j]) 113 ss.pop(j) 114 else: 115 j = j + 1 116 i = i + 1 117 return ss 118 119 def numTimes(self, segments, n): 120 # 线段排重 121 ss = self._combineSegments(segments) # 合并过的线段 122 123 # 划分连通图, 直接通过线段计算(并查集) 124 # 计算每个连通图点的总度数 125 u = UnionSet(ss) 126 127 for i in range(0, len(ss)): 128 for j in range(i+1, len(ss)): 129 result = ss[i].cross(ss[j]) 130 if result == 1: 131 #十字交 132 u.union(ss[i], ss[j], 0) 133 elif result == 2: 134 #丁字交 135 u.union(ss[i], ss[j], 0) 136 elif result == 3: 137 #端点交 138 u.union(ss[i], ss[j], -2) 139 else: 140 #不相交 141 pass 142 143 gs = u.graphs() # 子图 144 145 # 每个连通图分别计算 146 sum = 0 147 for g in gs: 148 sum += g.penCount(n) 149 return sum - 1
代码错了一个点:test case 74,原因不明。以上思路基本应该是正确的,不知道是不是哪种特殊情况没考虑到。