//1380K 47MS G++
#include <stdio.h>
#include <string.h>
#define MAX 510
int N;
int K;
int Vset1[MAX]; // row
int Vset2[MAX]; // column
int G[MAX][MAX]; // 0: no asteriod, 1: asteriod
int assignedFlag[MAX];
int waitingMove[MAX];
char getPair(int beginId) {
// printf("getPair %d\n", beginId);
for (int i = 1; i <= N; i++) { // find its adjancey
if (G[beginId][i] > 0) { // if connected
if (!assignedFlag[i]) { // if not assigned to other yet
Vset1[beginId] = i;
Vset2[i] = beginId;
assignedFlag[i] = 1;
return 1;
} else { // assigned to other before, try to move other to another
waitingMove[beginId] = 1; // beginId is waiting for move
// if i's owner is not waitting move, try move it.
if (!waitingMove[Vset2[i]] && getPair(Vset2[i])) { // if move others OK
Vset1[beginId] = i;
Vset2[i] = beginId;
assignedFlag[i] = 1;
return 1;
} else { // if move other failed, try next connected Node
continue;
}
}
}
}
return 0;
}
void getMinShot() {
int shotTime = 0;
for (int i = 1; i <= N; i++) {
memset(waitingMove, 0, sizeof(waitingMove));
if (getPair(i)) {
shotTime++;
}
}
printf("%d\n", shotTime);
}
int main() {
while (scanf("%d %d", &N, &K) != EOF) {
memset(Vset1, 0, sizeof(Vset1));
memset(Vset2, 0, sizeof(Vset2));
memset(G, 0, sizeof(G));
memset(assignedFlag, 0, sizeof(assignedFlag));
for (int i = 0; i < K; i++) {
int x;
int y;
scanf("%d %d", &x, &y);
G[x][y] = 1;
}
getMinShot();
}
}
二分图的基础应用题。
不过对于第一次用的我来说,光是转化就费了一番周折,
一开始是想这样划分两个点集:
第一个点集V1是坐标系中每一行和每一列(分别代表着一次shot的位置)
第二个点集V2是坐标系中的每一个asteroid,
之间的连线也就是边则代表着在此行/列shot,边另一端的的ateroid就可以被消灭。
但是这样划分以后,却发现不管是二分图的最小点覆盖还是最小边覆盖,都不能得到题目想要的解,
因为题目要的解是一个边的集合S :边的起点都在V1,而终点都在V2,并且v2中所有的点都能被S的边所覆盖,同时要求S在V1中覆盖的点最少。
这样当然最小点/边覆盖都不行,因为这两者会覆盖所有的边/点.
后来查了下才发现自己想偏了,应该这样划分两个点集:
V1代表每一行的shot
V2代表每一列的shot
而每一个asteroid(x , y)则是连接 y行 与 x列的的边, 该边代表着此asteriod被消灭。
这样,所有asteriod都被消灭就是指此二分图的所有边的集合S,
而要求的最少shot则是覆盖所有边的最小点集, 这样就转化成了最小点覆盖了(=二分图的最大匹配)。
之前用过二分图,果然威力还是比较强大的,顺便总结一下二分图最大匹配的匈牙利算法:
http://blog.csdn.net/dark_scope/article/details/8880547
这个总结的非常好,虽然没讲原理和证明,但是对于算法流程足够了。
首先,要有两个数组V1和V2分别代表着之前提到的二分图的两个点集。
首先遍历V1的每个Node i,做这个操作: getPair(i):
根据图G的连通信息来遍历于i邻接的V2中的点,
如果这样的点集T,那么遍历点T的每个点j:
case1: 如果j之前没有被分配给其他V1的点,那么就将j分配给i,更新相应的数组V1[i] = j, V1[j] = i, 直接返回1代表成功
case2: 如果j之前已经被分配给其他点k了,那么做这样的尝试:
为k在它的V2邻接点中找一个点分配给它,这时候就是一个递归调用getPair(k)了
这个递归调用会有一个隐含问题,就是无限死锁递归,最终segment fault , 比如 i -> getPair(k), k-> getPair(i),那么就绝对无限递归了。
因此,每次在对Node i进行配对时,要搞一个flag数组F,F[i] 标示 i 是否正在等待其他的V1的点k腾出位置, 所以要在上面的操作加一个判断,
如果F[k]标示在之前的递归调用 k 就已经在等待别的V1的点腾位置,这时候如果继续getPair(k), 那么就会无限调用了,所以要打破死锁,直接返回0
标示不能腾出位置来,如果能腾,那么更新相应的V1[i] 和V2[j],直接返回1代表成功。
如果根本没有邻接点或者找不到能和i配对的V2点,直接return 0代表失败。
对于V1的每个点i,getPair()返回成功就把最大匹配的值加1.
最后返回最大匹配值就可以了,最小覆盖点的点数量和它相等.
二分图的原理和证明还需要好好看看,光知道怎么写在遇到变种题和转化难度高的题时还是很吃力的.