poj 2159||hdu 1533 Going Home
最小费用最大流
或KM算法求二分图最优匹配
KM算法
//KM算法:想在网上找一些证明,可惜都看不懂,还对KM很陌生··· // //建图,矮人为x部,house为y部,x部的点顶标初始化为该 //点跟y部连边的边权值.y不顶标为0。由于这题要求的是最小值所 //以把边权加负号,KM求得的最优匹配加负号就相当于最小值了, //求完备匹配的话就进行n次匹配,for过去,每次从迭代变量开始匹配, //若匹配不成功则对x部和y部顶标进行松弛。Hungary时,标记x部的点, //要先判断x部顶标加上y部顶标要等于边权值时才标记y部点, //然后看y部点是否已匹配,跟普通匈牙利一样。 //顶标和大于边权值(不可能小于),用一个松弛数组保存y部点最小的 //松弛量即顶标和减去边权值。若匹配没成功则在松弛数组中找出最小 //的做为松弛量,把刚才hungary时有遍历到的x部点顶标减去松弛量, //有遍历到的y部点加上松弛量,这里y部点其实是已匹配成功的点,所以 //更新完顶标后已匹配的点对的顶标和还是等于边权值;没遍历到的y部点 //对应的松弛数组的值要减去松弛量,然后继续hungary // // //注意: //1、先判断x部顶标加上y部顶标要等于边权值时才标记y部点,表示该y部点 //有遍历到 //2、没遍历到的y部点对应的松弛数组的值要记得减去松弛量 //3、KM对x部点逐个遍历进行匈牙利前要对松弛数组slack初始化为INF, //匹配一个点只能初始化一次,不是每次hungary都要初始化, //不管hungary有没有成功只初始化一次。遍历下一个点时才可以再初始化 // //还有就是要先保证完备匹配,即有x1->y1, x1->y2, x2->y1这三条边,权值分别为 //5,1,1 这时x1 先跟y1匹配,然后匹配x2时,先找到y1。而x1找到y2时 //发现顶标和不等于边权值,所以松弛量为lx[x1] + ly[y1] - w[x1][y1]= 5+0-1=4 //然后顶标lx[x1]=1,lx[x1]=1,ly[y1]=0,ly[y2]=4 //再次hungary匹配结果(x1,y2),(x2,y1),虽然权值和比较小,但我们是要先确保 //完备匹配 #define in freopen("in.txt", "r", stdin); #include <stdio.h> #include <string.h> #define INF (1<<30) #define N 105 struct Point { int x, y; }man[N], house[N]; int map[N][N], lx[N], ly[N]; int slack[N]; //y部的松弛量 int right[N]; //y部匹配到的x部某个点的下标 bool visx[N], visy[N]; int abs(int num) { return num >= 0 ? num : -num; } bool hungary(int x, int n) { visx[x] = true; for(int i = 0; i < n; ++i) { if(visy[i] == true) continue; int slk = lx[x] + ly[i] - map[x][i]; if(slk == 0) //要当x 与 y的顶标和等于 边权值时 { //表示这两点可以匹配, visy[i] = true; if(right[i] == -1 || hungary(right[i], n)) { right[i] = x; return true; } }//当x 与 y的顶标和 大于(不可能小于)边权时, else //就更行y部该点的最小松弛量 slack[i] = slack[i] > slk ? slk : slack[i]; } return false; } int KM(int n) { for(int i = 0; i < n; ++i) //要找n次增广路,一次匹配一对,总的n对 { for(int j = 0; j < n; ++j) slack[j] = INF; //不知道为什么 这里要初始化松弛量 while(1) { for(int j = 0; j < n; ++j) visx[j] = visy[j] = false; if(hungary(i, n) == true) break; int slk = INF; for(int j = 0; j < n; ++j) if(visy[j] == false && slk > slack[j]) slk = slack[j]; //找到最小松弛量 for(int j = 0; j < n; ++j) { if(visx[j] == true) //x部有遍历到的要减去松弛量 lx[j] -= slk; if(visy[j] == true) //y部有遍历到的要加上松弛量 ly[j] += slk; //注意若之前hungary时顶标和大于边权值的 else //y部点是当做没有遍历的,这样该顶标就不会被更改(其实 slack[j] -= slk;//就是有匹配成功的y部就是有遍历到的) } //更改后的x部的顶标加上y部的顶标就可能等于边权值了 } //而原本匹配的y部也有更改顶标所以加上x不的顶标还是等于边权值 } //所以这样就有可能多匹配一对 int ans = 0; for(int i = 0; i < n; ++i) ans += map[right[i]][i]; printf("%d\n", -ans); } int main() { int row, col; while(scanf("%d%d", &row, &col), row||col) { int n_man = 0, n_house = 0; for(int i = 0; i < row; ++i) { getchar(); for(int j = 0; j < col; ++j) { char ch = getchar(); if(ch == 'm') { man[n_man].x = i; man[n_man++].y = j; } if(ch == 'H') { house[n_house].x = i; house[n_house++].y = j; } } } for(int i = 0; i < n_man; ++i) { right[i] = -1; lx[i] = -INF; ly[i] = 0; for(int j = 0; j < n_house; ++j) { //因为这题要求的是最小值,所以在把边权值变为负的 //求出最大值输出时加个负号就是答案了 map[i][j] = -(abs(man[i].x - house[j].x) + abs(man[i].y - house[j].y)); //把x部的顶标设为该点与y部连边中的最大值 lx[i] = lx[i] > map[i][j] ? lx[i] : map[i][j]; } } KM(n_man); } return 0; }
最小费用最大流
//最小费用最大流,是费用优先考虑,然后才是最大流 //直接spfa找到sink的最小费用就可以,流量根据容量建反向边就会 //自己调整,挺神奇的,也不知道这样理解对还是错··· #define infile freopen("in.txt", "r", stdin); #include <stdio.h> #include <string.h> #include <queue> using namespace std; #define N 20010 struct POINT { int x, y; }man[N], house[N]; struct EDGE { int from, to, cap, cost, next; }edge[2*N]; //有反向边 int eid, n; int head[N], fa[N], dis[N]; bool vis[N]; int abs(int num) { return num > 0 ? num : -num; } void add_edge(int from, int to, int cap, int cost) { edge[eid].from = from; edge[eid].to = to; edge[eid].cap = cap; edge[eid].cost = cost; edge[eid].next = head[from]; head[from] = eid++; edge[eid].from = to; edge[eid].to = from; //建反向边 edge[eid].cap = 0; edge[eid].cost = -cost; edge[eid].next = head[to]; head[to] = eid++; } bool spfa(int now) { memset(vis, false, sizeof(vis)); memset(dis, -1, sizeof(dis)); queue<int>que; que.push(now); vis[now] = true; dis[now] = 0; while(!que.empty()) { now = que.front(); que.pop(); for(int i = head[now]; i != -1; i = edge[i].next) { int to = edge[i].to; if(edge[i].cap > 0 && (dis[to] == -1 || dis[to] - dis[now] > edge[i].cost)) { dis[to] = dis[now] + edge[i].cost; fa[to] = i; if(vis[to] == false) { que.push(to); vis[to] = true; } } } vis[now] = false; } if(dis[20005] == -1) return false; return true; //有找到路在这返回true,不是一找到sink就返回 } void max_flow() { int ans = 0; while(spfa(0)) { for(int i = 20005; i != 0; i = edge[fa[i]].from) { edge[fa[i]].cap -= 1; //下标从0开始,奇数为逆向边 edge[fa[i]^1].cap += 1; //所以偶数异或完刚好是奇数 } ans += dis[20005]; //累加最小费用 } printf("%d\n", ans); } int main() { infile int row, col; while(scanf("%d%d", &row, &col), row||col) { eid = 0; memset(head, -1, sizeof(head)); int n_man = 0, n_house = 0; for(int i = 0; i < row; ++i) { getchar(); for(int j = 0; j < col; ++j) { char ch = getchar(); if(ch == 'm') { man[++n_man].x = i; man[n_man].y = j; } if(ch == 'H') { house[++n_house].x = i; house[n_house].y = j; } } } n = n_man; for(int i = 1; i <= n_man; ++i) { add_edge(0, i, 1, 0); //设source为0,source到人 add_edge(i+n_man, 20005, 1, 0); //设sink为20005,house 到汇点 for(int j = 1; j <= n_house; ++j) { add_edge(i, n_man+j, 1, abs(man[i].x - house[j].x) + abs(man[i].y - house[j].y)); } } max_flow(); } return 0; }