巨人和鬼
一组n个巨人正与n个鬼进行战斗,每个巨人的武器是一个质子炮, 它可以把一串质子流射中鬼而把鬼消灭。质子流沿直线行进,在击中鬼时就终止。巨人决定采取下述策略。他们寻找鬼配对,以形成n个巨人─鬼对,。然后每个巨人同时向他选取的鬼射出一串质子流。我们知道,让质子流互相交叉是很危险的。因此巨人选择的配对方式应该使质子流都不会交叉。假定每个巨人和每个鬼的位置都是平面上的一个固定点,并且没有三个位置共线, 求一种配对方案。
自己分析:采用分治方法,寻找中间界限,将大区间问题分成左右2个子区间,并同理递归求解。可能有很多种配对的方法,但只需找一种即可。此种方法必能找一种,因为每次找的都是成立的,能保证左右两边都能找到配对的。
算法分析:
我们设P1..Pn为巨人的固定点;Pn+1..P2n为鬼的固定点。我们采取分治采取分治策略寻找序列[Pp..Pr]中的配对方案(初始时[Pp..Pr]为[P1..P2n]):
在[Pp..Pr]中找出一个最低位置(Y坐标值最小)的一个点P0,如果这样的点有多个,则选取最左边的点为P0,P0与Pp交换。然后将其余点[Pp+1..Pr]按相对 Pp的极角递增的顺序排列。显然Pp与其余点Pp+1..Pr之间的任何线段是不会交叉的。我们从Pp开始寻找一个巨人和鬼成对的最小子区间[Pp..Pi](p≤i≤r)。若该子区间仅剩一个元素,配对结束;否则巨人(鬼)Pp与鬼(巨人)Pi配对。这样使得尚未配对的巨人和鬼分布在两个子区间[Pp+1..Pi-1],[Pi+1..Pr]。继续按上述分治策略分别递归求解[Pp+1..Pi-1]和[Pi+1..Pr]。
如上图,以点P1,将其他点按相对P1的极角递增排序。然后从P2开始顺序地找一个最短的配对序列P1-P6(鬼和巨人的个数要相等,这样才能一一配对。P1-P6是3个鬼,3个巨人)。怎样求分割线P1P6呢?是给鬼和巨人一个标志,设鬼为-1,巨人为1,从P2开始找时,逐渐累加,直到为1时停止,表明鬼和人的个数相等,如P1到P2时:2个鬼(-1-1=-2),继续到P3:2鬼1巨人(-1-1+1=-1),P4:3鬼1巨人(-1-1+1-1=-2),P5:3鬼2巨人(-1-1+1-1+1=-1),P6:3鬼3巨人(-1-1+1-1+1+1=0),此时鬼和巨人的个数相等,则分割线为P1P6,将P1-P8分割成(P2-P5)和(P7-P8)。再递归对(P2-P5)和(P7-P8)按同样的方法分治求解。
上面求分割线P1P6的参考代码如下:
m = List[p].k; i = p;//其中p为区间[p,r]的起点,鬼的k=-1,巨人的k=1.m为巨人、鬼个数累积和
{求巨人和魔鬼成对的最小子区间list[p..i]}
While ( m != 0)//当m=0时,表明找到了一个最短的鬼和巨人个数相等(即可配对)的子区间
{ ++i;
m += List[i].k;//k不断累加,从p到最终满足条件的i
}
下面是程序题解(摘自《ACM程序设计培训教程 吴昊》第15章 凸包问题中的案例2 巨人和鬼P232):
Program Giants_And_Monsters;
Const
Maxn = 100;
Type
Node = Record
k : Integer; {k=1:巨人;k=-1:魔鬼}
x, y : Real{坐标}
End;
Var
N, i : Integer;{魔鬼和巨人的对数,辅助变量}
F : Text;{文件变量}
List, Lt : Array [1..2 * Maxn] of Node;{点集,辅助点集}
p0 : Node;{最低位置点}
Function Comp(Var p1, p2 : Node):Boolean;
{计算(P1-P0)*(P2-P0)的叉积值。若值为正(相对于P0来说,P2的极角大于P1的极角 ) 返回true;否则返回false}
Begin
If (p1.x-p0.x)*(p2.y-p0.y) - (p1.y-p0.y)*(p2.x-p0.x) > 0
Then Comp := True
Else Comp := False
End;
Procedure Merge(p, q, r : Integer);
{将两个已按极角递增顺序排好序的子序列list[p..q]和list[q+1..r]合并排序成一个序列list[p..r]}
Var
i, j, t : Integer;
Begin
t := p; i := p; j := q + 1;
While t <= r Do Begin
If (i <= q) And ((j > r) Or Comp(List[i], List[j])) Then
Begin
Lt[t] := List[i]; Inc(i)
End
Else Begin
Lt[t] := List[j]; Inc(j)
End;{else}
Inc(t)
End;{while}
For i := p to r Do List[i] := Lt[i]
End;{merge}
Procedure Merge_Sort(p, r : Integer);
Var
q : Integer;
Begin
If p <> r Then Begin
q := (p + r - 1) div 2;{计算中间下标q}
Merge_Sort(p, q);{对子序列list[p..q]递归排序}
Merge_Sort(q + 1, r);{对子序列list[q+1..r]递归排序}
Merge(p, q, r){合并两个排好序的子序列}
End{then}
End;{merge_sort}
Procedure Swap(Var a, b : Node);{交换a和b两点}
Var
t : Node;
Begin
t := a; a := b; b := t
End;
Procedure Out_pos(Var p : Node);{输出p点的X和Y坐标}
Begin
Write(p.x:8:2, p.y:8:2)
End;
Procedure Pick(p, r : Integer);
Var
i, m : Integer;
Begin
If p < r Then Begin
m := p;{求出Y坐标值最小的点或具有Y最小值的数个点中最左边的点m}
For i := p to r Do
If (List[i].y < List[m].y) Or (List[i].y = List[m].y)
And (List[i].x < List[m].x) Then m := i;
Swap(List[p], List[m]);{m点与p点交换并设为P0}
p0 := List[p];
Merge_Sort(p + 1, r);{对list[p+1..r]按极角递增的顺序排序}
m := List[p].k; i := p;
{求巨人和魔鬼成对的最小子区间list[p..i]}
Repeat
Inc(i);
m := m + List[i].k
Until m = 0;
Out_pos(List[p]);{list[p]和list[i]配对}
Out_Pos(List[i]);
Writeln;
Pick(p+1, i - 1);{递归搜索子序列list[p+1..i-1]中配对情况}
Pick(i + 1, r){递归搜索子序列list[i+1..r]中配对情况}
End{then}
End;{pick}
Begin
Assign(F, 'INPUT.DAT');{输入文件名串与文件变量连接}
Reset(F);{文件读准备}
Readln(F, N);{读入巨人和鬼的对数}
For i := 1 to N Do Begin{读入N个巨人的位置}
Readln(F, List[i].x, List[i].y);
List[i].k := 1
End;
For i := N + 1 to 2 * N Do Begin{读入N个魔鬼的位置}
Readln(F, List[i].x, List[i].y);
List[i].k := -1
End;
Pick(1, 2 * N)
End.{main}