图论
图论给我的感觉就是知识点特别多、各种概念、对于初学者来说是个不小的挑战、
先从最基本的图的遍历说起、图的广度和深度优先遍历、也就是图上的bfs和dfs、
说的搜索、必然要考虑图的存储结构、最直接的是邻接矩阵存储、比较常用的是邻接表、还有一些别的存储结构、比如十字链表等等、
当然、上面说的显式存储结构、图的搜索不一定要先把图先建立好、也可以在搜索的过程再建立部分图、这种是隐式存储、一般不常用、在需要节约存储空间的时候才会考虑、
下面以两道题为例:
题目链接:click here
// 黑白算法之图的遍历 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 1<<31 const int maxn = 1005; char mpt[maxn][maxn]; void dfs(int x, int y) { if (mpt[x][y] != '#') return; mpt[x][y] = '.'; dfs(x + 1, y); dfs(x - 1, y); dfs(x, y + 1); dfs(x, y - 1); } int main() { int n, m; while(scanf("%d%d", &n, &m) != EOF) { for (int i = 1; i <= n; i++) scanf("%s", mpt[i] + 1); int ans = 0; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if(mpt[i][j] == '#') { dfs(i, j); ans++; } } } printf("%d\n", ans); } //system("pause"); return 0; }
题目链接:HDU 1728
// 黑白算法之图的遍历 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 1<<31 const int maxn = 1005; char mpt[maxn][maxn]; int vis[maxn][maxn]; int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0}; int n, m, k; struct node { int x, y, step; }; bool judge(int x, int y) { if (x >= 1 && x <= n && y >= 1 && y <= m && mpt[x][y] == '.') return true; return false; } bool bfs(int k, int sx, int sy, int ex, int ey) { queue<node> q; node now; now.x = sx; now.y = sy; now.step = 0; vis[now.x][now.y] = 1; q.push(now); while(!q.empty()) { now = q.front(); q.pop(); if(now.step > k)continue; for (int i = 0; i < 4; i++) { node next; next.x = now.x + dir[i][0]; next.y = now.y + dir[i][1]; next.step = now.step + 1; while(true) { if (!judge(next.x, next.y)) break; if (next.x == ex && next.y == ey) return true; if (!vis[next.x][next.y]) { q.push(next); vis[next.x][next.y] = 1; } next.x += dir[i][0]; next.y += dir[i][1]; } } } return false; } int main() { int t; scanf("%d", &t); while(t--) { memset(vis, 0, sizeof(vis)); scanf("%d%d", &n, &m); int sx, sy, ex, ey; for (int i = 1; i <= n; i++) { scanf("%s", mpt[i] + 1); } scanf("%d%d%d%d%d", &k, &sy, &sx, &ey, &ex); bool flag = bfs(k, sx, sy, ex, ey); if (flag) printf("yes\n"); else printf("no\n"); } system("pause"); return 0; }
然后是拓扑排序、
拓扑排序其实就是在一个有向无环图中、从第一个入度为0的点开始、然后按照逻辑先后顺序将整个图的点排列出来、
题目链接:HDU 1285 感觉代码越来越飘逸了、、、
// 黑白算法之拓扑排序 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 1<<31 const int maxn = 505; bool mpt[maxn][maxn]; int lev[maxn]; vector<int> v[maxn]; priority_queue<int, vector<int>, greater<int> > q; void topo(int n) { for (int i = 1; i <= n; i++) { if (!lev[i]) q.push(i); } int flag = 0; while(!q.empty()) { int now = q.top(); q.pop(); if (flag) printf(" %d", now); else printf("%d", now); flag++; for (int i = 0; i < v[now].size(); i++) { int next = v[now][i]; lev[next]--; if (!lev[next]) q.push(next); } } if (flag != n) { printf("这个图有环、并没有拓扑排序\n"); } } int main() { int n, m; while(scanf("%d%d", &n, &m) != EOF) { memset(mpt, 0, sizeof(mpt)); for (int i = 1; i <= m; i++) { int a, b; scanf("%d%d", &a, &b); mpt[a][b] = 1; } for (int i = 1; i <= n; i++) { v[i].clear(); for (int j = 1; j <= n; j++) { if (mpt[i][j]) { v[i].push_back(j); lev[j]++; } } } topo(n); printf("\n"); } system("pause"); return 0; }
接下来就是最小生成树了、其实只需要掌握两种最小生成树的算法就可以了、
对于稠密图来说prim算法是一个不错的选择、对于稀疏图来说kruskal算法更nice、
prim算法和dijkstra算法很像、换句话说、两者是基于同样的贪心本质来实现的、
先写一遍prim算法
题目链接:HDU 1863
// 黑白算法之最小生成树 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x3f3f3f3f const int maxn = 505; int mpt[maxn][maxn]; int vis[maxn]; int val[maxn]; void Prim(int n) { memset(vis, 0, sizeof(vis)); for (int i = 1; i <= n; i++) val[i] = mpt[1][i]; int sum = 0; vis[1] = 1; for (int i = 1; i < n; i++) { int minx = inf; int k = 0; for (int j = 1; j <= n; j++) { if (!vis[j] && val[j] < minx && val[j]) { minx = val[j]; k = j; } } vis[k] = 1; sum += minx; if(k == 0) break; for (int j = 1; j <= n; j++) { if (!vis[j] && mpt[k][j] < val[j] && mpt[k][j]) val[j] = mpt[k][j]; } } if (sum >= inf) printf("?\n"); else printf("%d\n", sum); } int main() { int n, m; while(scanf("%d%d", &m, &n) != EOF, m) { memset(mpt, inf, sizeof(mpt)); for (int i = 1; i <= m; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); mpt[a][b] = mpt[b][a] = c; } Prim(n); } system("pause"); return 0; }
kruskal算法的本质是以边贪心、用到的并查集这种数据结构来优化、并查集除了用来合并关系、对于删边之后判断关系、还可以离线+逆向的方式来做、
// 黑白算法之最小生成树 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x3f3f3f3f const int maxn = 505; int p[maxn]; struct node { int x, y, val; }edge[maxn]; int num; bool cmp(node a, node b) { if (a.val < b.val) return true; else return false; } int find(int x) { if (x == p[x]) return x; else return p[x] = find(p[x]); } int merge(int x, int y) { int fx = find(x); int fy = find(y); if (fx != fy) { p[fx] = fy; num++; return 1; } return 0; } int main() { int n, m; while(scanf("%d%d", &m, &n) != EOF, m) { int sum = 0; num = 0; for (int i = 1; i <= m; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); edge[i].x = a; edge[i].y = b; edge[i].val = c; } sort(edge + 1, edge + m + 1, cmp); for (int i = 1; i <= n; i++) p[i] = i; for (int i = 1; i <= m; i++) { int flag = merge(edge[i].x, edge[i].y); if (flag) sum += edge[i].val; } if (num == n - 1) printf("%d\n", sum); else printf("?\n"); } system("pause"); return 0; }
当让最小生成树还有许多扩展、比如次小生成树、最优比率生成树、K度限生成树等等、留给读者自行思考、
其中比较重要的是生成树计数类问题、可以学习周冬的《生成树的计数及其应用》
然后就是最短路、其实最短路算法看似挺多、但有用的不过那两三种、而且这类题目陈题实在太多、所以已经没有什么创新空间了、
下面就介绍三种比较有用的最短路算法、dijkstra、SPFA、Floyd
DIJ算法本身比较简单、但是由于算法效率问题、在大部分情况下远没有SPFA算法优秀、
不过DIJ+堆优化、也就是用优先队列来优化过后的DIJ在性能上不比SPFA差多少、复杂度是O(nlogn)、
而且DIJ有一个SPFA永远无法触及的优势就是路径比较、比如在求解最短路树的时候、题目链接:click here
下面给一份DIJ+堆优化的代码
题目链接:HDU 2544
// 黑白算法之最小生成树 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x3f3f3f3f const int maxn = 505; int dist[maxn]; struct node { int x, val; node(int a, int b) {x = a; val = b;} bool operator < (const node & a) const { if (val == a.val) return x < a.x; else return val > a.val; } }; vector<node> v[maxn]; void Dijkstra(int s, int n) { for (int i = 1; i <= n; i++) dist[i] = inf; dist[s] = 0; priority_queue<node> q; q.push(node(s, dist[s])); while(!q.empty()) { node now = q.top(); q.pop(); for (int i = 0; i < v[now.x].size(); i++) { node next = v[now.x][i]; if (dist[next.x] > now.val + next.val) { dist[next.x] = now.val + next.val; q.push(node(next.x, dist[next.x])); } } } printf("%d\n", dist[n]); } int main() { int n, m; while(scanf("%d%d", &n, &m) != EOF, n + m) { for (int i = 1; i <= n; i++) v[i].clear(); for (int i = 1; i <= m; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); v[a].push_back(node(b, c)); v[b].push_back(node(a, c)); } Dijkstra(1, n); } system("pause"); return 0; }
SPFA算法是一个非常神奇的算法、超乎寻常的执行效率、以及对负权边的处理和负环的处理都是那么得心应手、可以说SPFA是最短路算法的精华、
你可以什么都不会、但是不能不会写SPFA、
SPFA算法的复杂度是O(K*E)、在一般情况下、K的值为2~3左右、由此可以看出、SPFA更适用于稀疏图、
所以除了网格型的图、SPFA的实际运行效率的都是相当惊人的、
下面给一份SPFA的代码、当然SPFA还可以加入一些优化、但是效率已经得不到太大的提高、毕竟卡常数的出题人不是好的出题人、
题目链接:HDU 3790 最开始写的太飘逸、连自己都晕了、然后换个种写法、简单易懂、、
// 黑白算法之最短路 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x7fffffff const int maxn = 1005; int dist[maxn]; int c[maxn]; int vis[maxn]; struct node { int x, len, cost; node(int a, int b, int c) {x = a; len = b; cost = c;} }; vector<node> v[maxn]; void SPFA(int s, int t, int n) { memset(vis, 0, sizeof(vis)); for (int i = 1; i <= n; i++) { dist[i] = inf; c[i] = inf; } dist[s] = 0; c[s] = 0; queue<node> q; q.push(node(s, 0, 0)); vis[s] = 1; while(!q.empty()) { node now = q.front(); q.pop(); vis[now.x] = 0; for (int i = 0; i < v[now.x].size(); i++) { node next = v[now.x][i]; if (dist[next.x] > next.len + dist[now.x]) { dist[next.x] = next.len + dist[now.x]; c[next.x] = next.cost + c[now.x]; if (!vis[next.x]) { vis[next.x] = 1; q.push(next); } } if (dist[next.x] == next.len + dist[now.x]) { if (c[next.x] > next.cost + c[now.x]) c[next.x] = next.cost + c[now.x]; } } } printf("%d %d\n", dist[t], c[t]); } int main() { int n, m; while(scanf("%d%d", &n, &m) != EOF, n + m) { for (int i = 1; i <= n; i++) v[i].clear(); for (int i = 1; i <= m; i++) { int a, b, c, d; scanf("%d%d%d%d", &a, &b, &c, &d); v[a].push_back(node(b, c, d)); v[b].push_back(node(a, c, d)); } int s, t; scanf("%d%d", &s, &t); SPFA(s, t, n); } system("pause"); return 0; }
Floyd算法其实就是个动态规划、一般情况下都没用、不过可以用来辅助求解关系传递闭包和求最小环、
最小环也是很老的考点了、其实就是Floyd扩展一下、感觉没什么太大的用处、
还是给一份Floyd的代码、HDU 2544
// 黑白算法之最短路 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x7ffffff const int maxn = 1005; int dp[maxn][maxn]; void init(int n) { for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dp[i][j] = inf; for (int i = 1; i <= n; i++) dp[i][i] = 0; } void floyd(int n) { for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]); } } } } int main() { int n, m; while(scanf("%d%d", &n, &m) != EOF, n + m) { init(n); for (int i = 1; i <= m; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); dp[a][b] = dp[b][a] = c; } floyd(n); printf("%d\n", dp[1][n]); } system("pause"); return 0; }
差分约束系统、最短路衍生出来的一类经典的问题、
其实就是根据松弛原理、将不等式转化为最短路的来做、具体的内容看论文《树与图的完美结合--浅析差分约束系统》
下面我贴一份差分约束的代码、用SPFA实现的、
题目链接:ZOJ 1508
// 黑白算法之差分约束 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x7ffffff const int maxn = 50005; int dist[maxn]; int vis[maxn]; int cnt[maxn]; struct node{ int x, val; node(int a, int b) {x = a; val = b;} }; vector<node> v[maxn]; int SPFA(int s, int t) { memset(vis, 0, sizeof(vis)); memset(dist, 0x3f, sizeof(dist)); memset(cnt, 0, sizeof(cnt)); queue<int> q; q.push(s); vis[s] = 1; dist[s] = 0; cnt[s]++; while(!q.empty()) { int now = q.front(); q.pop(); vis[now] = 0; if (cnt[now] > s + 1) return -1; for (int i = 0; i < v[now].size(); i++) { node next = v[now][i]; if (dist[next.x] > dist[now] + next.val) { dist[next.x] = dist[now] + next.val; if (!vis[next.x]) { vis[next.x] = 1; q.push(next.x); cnt[next.x]++; } } } } return -dist[t - 1]; } int main() { int n; while(scanf("%d", &n) != EOF) { int maxx = 0, minx = 50005; for (int i = 0; i < maxn; i++) v[i].clear(); for (int i = 1; i <= n; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); if (a < minx) minx = a; if (b > maxx) maxx = b; v[b].push_back(node(a - 1, -c)); } for (int i = minx; i <= maxx; i++) { v[i].push_back(node(i - 1, 0)); v[i].push_back(node(i + 1, 1)); } int ans = SPFA(maxx, minx); printf("%d\n", ans); } //system("pause"); return 0; }
最短路还有一个扩展是求第K短路、属于A*启发式搜索、将在搜索部分讲解、
两外还有最短路计数问题、只需要在最短路算法中添加DP计数部分即可
然后就是图的强连通分量(SCC)部分、求图的强连通分量有线性的算法tarjan算法、求图的SCC其实就是将一个有向带环图转换成一个DAG图、
因为很多方法都可以在DAG图上线性的实现、比如图的遍历、动态规划之类的、
tarjan算法最主要的运用还是在缩点、可以将相对复杂的关系简化、
给一份tarjan算法的代码、HDU 1269
// 黑白算法之tarjan算法 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <queue> #include <algorithm> using namespace std; #define inf 0x7ffffff const int maxn = 10005; vector<int> v[maxn]; int low[maxn]; int dfn[maxn]; int vis[maxn]; int que[maxn]; int cnt, top, id; void init() { memset(dfn, 0, sizeof(dfn)); memset(vis, 0, sizeof(vis)); cnt = id = 0; top = -1; } void tarjan(int s) { low[s] = dfn[s] = ++id; que[++top] = s; vis[s] = 1; for (int i = 0; i < v[s].size(); i++) { int next = v[s][i]; if (!dfn[next]) { tarjan(next); low[s] = min(low[s], low[next]); } else if (vis[next]) low[s] = min(low[s], dfn[next]); } if (low[s] == dfn[s]) { cnt++; int next; do { next = que[top--]; vis[next] = 0; }while(s != next); } } int main() { int n, m; while(scanf("%d%d", &n, &m) != EOF, n + m) { init(); for (int i = 1; i <= n; i++) v[i].clear(); for (int i = 1; i <= m; i++) { int a, b; scanf("%d%d", &a, &b); v[a].push_back(b); } for (int i = 1; i <= n; i++) { if (!dfn[n]) tarjan(i); } if (cnt == 1) printf("Yes\n"); else printf("No\n"); } system("pause"); return 0; }
接下来是图论中最神奇的部分、网络流、
网络流中一个重要的关系:最大流 = 最小割
网络流有几种优秀的算法、dinic、sap、isap、
下面给出DINIC算法的模板、
// 黑白算法之网络流 #include <cstdio> #include <cstring> #include <queue> #include <algorithm> using namespace std; #define N 5005 #define INF 0x3fffffff int n, m, k; int level[N]; struct node { int to, next, cost; }edge[N * 10]; int head[N]; int t_head[N]; int s, e; void init() { k = 0; memset(head, -1, sizeof(head)); } int bfs(int s, int t) {//对顶点进行标号、找出层次图 memset(level, 0, sizeof(level)); queue<int> q; q.push(s); level[s] = 1; while (!q.empty()) { int now = q.front(); q.pop(); for (int i = head[now]; i != -1; i = edge[i].next) { int y = edge[i].to; if (!level[y] && edge[i].cost > 0) { level[y] = level[now] + 1; q.push(y); } } } return level[t] != 0;//汇点是否在层次图中 } int dfs(int s, int cp) {//在层次图中寻找增广路进行增广 int flow = 0, temp; int t; if (s == e || cp == 0)return cp; for (;t_head[s] + 1; t_head[s] = edge[t_head[s]].next) { int y = edge[t_head[s]].to; if (level[s] + 1 == level[y]) { temp = dfs(y, min(cp, edge[t_head[s]].cost)); if (temp > 0) { edge[t_head[s]].cost -= temp; edge[t_head[s] ^ 1].cost += temp; flow += temp; cp -= temp; if(cp == 0) break; } } } return flow; } int dinic() { int ans = 0, flow = 0; while (bfs(s, e)) {//汇点不在层次图中、算法终止 for (int i = 0; i <= e; i++) t_head[i] = head[i]; ans += dfs(s, INF); } return ans; } void add(int x, int y, int val) { edge[k].to = y; edge[k].cost = val; edge[k].next = head[x]; head[x] = k++; edge[k].to = x; edge[k].cost = 0; edge[k].next = head[y]; head[y] = k++; } int A[105][205]; int main() { int t; scanf("%d%d", &n, &m); init(); int sum1 = 0; int sum2 = 0; s = 0, e = 2 * n + 1; for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); sum1 += x; add(s, i, x); } for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); sum2 += x; add(n + i, e, x); //自身到自身 add(i, n + i, INF); } for (int i = 1; i <= m; i++) { int a, b; scanf("%d%d", &a, &b); add(a, n + b, INF); add(b, n + a, INF); } int ans = dinic(); if (sum1 != sum2) { printf("NO\n"); return 0; } if(sum1 != ans) { printf("NO\n"); return 0; } else { printf("YES\n"); memset(A, 0, sizeof(A)); for (int i = 1; i <= n; i++) { for (int j = head[i]; j != -1; j = edge[j].next) { A[i][edge[j].to - n] = edge[j^1].cost; } } for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { printf("%d ", A[i][j]); } printf("\n"); } } return 0; }
网络流的精华在于建图、从最基本的拆点、拆边到奇偶建图、平面图转最短路等等才是网络流的神奇所在、
建议学习论文《网络流建模汇总》
网络流的扩展流量有上下界的网络流、需要构造一个伴随网络、
还有最小费用最大流和最大费用最大流等等、
下面给出最小费用最大流模板、求最大费用只需要取相反数、结果取相反数即可、
// 黑白算法之费用流 const int MAXN = 1010; const int MAXM = 10010; const int INF = 0x3f3f3f3f; struct Edge { int to, next, cap, flow, cost; }edge[MAXM]; int head[MAXN], tol; int pre[MAXN], dis[MAXN]; bool vis[MAXN]; int N;//节点总个数,节点编号从0 ~ N-1 void init(int n) { N = n; tol = 0; memset(head, -1, sizeof(head)); } void addedge(int u, int v, int cap, int cost){ edge[tol].to = v; edge[tol].cap = cap; edge[tol].cost = cost; edge[tol].flow = 0; edge[tol].next = head[u]; head[u] = tol++; edge[tol].to = u; edge[tol].cap = 0; edge[tol].cost = -cost; edge[tol].flow = 0; edge[tol].next = head[v]; head[v] = tol++; } bool spfa(int s,int t){ queue<int> q; for (int i = 0; i < N; i++) { dis[i] = INF; vis[i] = false; pre[i] = -1; } dis[s] = 0; vis[s] = true; q.push(s); while(!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u];i != -1;i = edge[i].next) { int v = edge[i].to; if (edge[i].cap > edge[i].flow && dis[v] > dis[u] +edge[i].cost) { dis[v] = dis[u] + edge[i].cost; pre[v] = i; if (!vis[v]) { vis[v] = true; q.push(v); } } } } if (pre[t] == -1) return false; else return true; } //返回的是最大流,cost存的是最小费用 int minCostMaxflow(int s, int t, int &cost) { int flow = 0; cost = 0; while(spfa(s,t)) { int Min = INF; for (int i = pre[t]; i != -1 ; i = pre[edge[i^1].to]) { if (Min > edge[i].cap - edge[i].flow) Min = edge[i].cap - edge[i].flow; } for (int i = pre[t];i != -1;i = pre[edge[i^1].to]) { edge[i].flow += Min; edge[i^1].flow -= Min; cost += edge[i].cost*Min; } flow += Min; } return flow; }
二分图是图论一个非常有趣的分支、可以很好地提高抽象模型与建图能力、同时算法思想也比较吸引人、
判断二分图:交叉染色法(奇偶染色)、如果图中存在奇环即表示非二分图、
最小点覆盖 = 最大匹配
最大点独立集 = 顶点数 - 最大匹配
有向图最小路径覆盖 = 原图顶点数 - 拆点后最大匹配
最小边覆盖 = 最大点独立集
二分图最大匹配匈牙利算法、复杂度O(V * E)
//黑白算法之匹配 邻接矩阵实现 复杂度O(VE) const int MAX_N = 510; int uN, vN;//匹配两端的点数 int g[MAX_N][MAX_N]; int match[MAX_N]; bool used[MAX_N]; bool dfs(int u) { for (int v = 0; v < vN; v++) { if (g[u][v] && !used[v]) { used[v] = true; if (match[v] == -1 || dfs(match[v])) { match[v] = u; return true; } } } return false; } int hungary() { int res = 0; memset(match, -1, sizeof(match)); for (int u = 0; u < uN; u++) { memset(used, false, sizeof(false)); if (dfs(u)) res++; } return res; } //邻接表实现 int V;//顶点数 vector<int> G[MAX_V];//邻接表 int match[MAX_V];//所匹配的顶点 bool used[MAX_V];//DFS中用到的访问标记 //向图中增加一条连接u和v的边 void add_edge(int u, int v) { G[u].push_back(v); G[v].push_back(u); } //通过DFS寻找增广路 bool dfs(int v) { used[v] = true; for (int i = 0; i < G[v].size(); i++) { int u = G[v][i], w = match[u]; if (w < 0 || !used[w] && dfs(w)) { match[v] = u; match[u] = v; return true; } } return false; } //求解二分图的最大匹配 int bipartite_matching() { int res = 0; memset(match, -1, sizeof(match)); for (int v = 0; v < V; v++) { if (match[v] < 0) { memset(used, 0, sizeof(used)); if (dfs(v)) { res++; } } } return res; }
KM算法主要用于求解最大(小)权完美匹配问题、在求非2完美匹配时需要添加边从而使其转化为完美匹配、
KM算法还可以解形似二分图的不等式组、复杂度O(V^3)
二分图最大权匹配 复杂度O(nx*nx*ny) 若求最小权匹配、可将权值取相反数、结果取相反数 点的编号从0开始 const int N = 310; const int INF =0x3f3f3f3f; int nx, ny;//两边的点数 int g[N][N]; int linker[N], lx[N], ly[N];//y中各点匹配状态、x/y中的点标号 int slack[N]; bool visx[N], visy[N]; bool DFS(int x) { visx[x] = true; for (int y = 0; y < ny; y++) { if (visy[y])continue; int tmp = lx[x] + ly[y] - g[x][y]; if (tmp == 0) { visy[y] = true; if (linker[y] == -1 || DFS(linker[y])) { linker[y] = x; return true; } } else if(slack[y] > tmp) slack[y] = tmp; } return false; } int KM() { memset(linker, -1, sizeof(linker)); memset(ly, 0, sizeof(ly)); for (int i = 0; i < nx; i++) { lx[i] = -INF; for (int j = 0; j < ny; j++) if (g[i][j] > lx[i]) lx[i] = g[i][j]; } for (int x = 0; x < nx; x++) { for (int i = 0; i < ny; i++) slack[i] = INF; while (true) { memset(visx, false, sizeof(visx)); memset(visy, false, sizeof(visy)); if (DFS(x)) break; int d = INF; for (int i = 0; i < ny; i++) if (!visy[i] && d > slack[i]) d = slack[i]; for (int i = 0; i < nx; i++) if (visx[i]) lx[i] -= d; for (int i = 0; i < ny; i++) { if (visy[i]) ly[i] += d; else slack[i] -= d; } } } int res = 0; for (int i = 0; i < ny; i++) if (linker[i] != -1) res += g[linker[i]][i]; return res; } int main() { int n; while(scanf("%d", &n) != EOF) { for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) scanf("%d", &g[i][j]); nx = ny =n; printf("%d\n", KM()); } return 0; }
二分图多重匹配的一般做法就是给每个点开一个vector记录这个点匹配了哪些点、
//黑白算法之多重匹配 #include <stdio.h> #include <string.h> #include <algorithm> using namespace std; int n,m; int num[15]; int mpt[100005][15]; int vis[15]; int use[15]; int dp[15][100005]; int hungary(int x) { for (int i = 1; i <= m; i++) { if (vis[i] == 0 && mpt[x][i] == 1) { vis[i] = 1; if (use[i] < num[i]) { //满足条件 dp[i][use[i]++] = x; return 1; } //不满足则寻找增广路 for (int j = 0; j < use[i]; j++) { //看能否回溯一个出去 if (hungary(dp[i][j])) { dp[i][j] = x; return 1; } } } } return 0; } int main() { while(scanf("%d%d", &n, &m) != EOF) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { scanf("%d", &mpt[i][j]); } } for (int i = 1; i <= m; i++) scanf("%d", &num[i]); int ans = 0; memset(use, 0, sizeof(use)); for (int i = 1; i <= n; i++) { memset(vis, 0, sizeof(vis)); if (!hungary(i)) { ans = 1; break; } } if (ans == 0) printf("YES\n"); else printf("NO\n"); } return 0; }
一般图匹配问题、解法是带花树算法、复杂度是O(V^3)
2-sat问题即2判定性问题、问题描述基本上是存在多个二元组、某些组中的一个元素与另一组中的某一个元素存在矛盾、现在要求每一个
二元组中选择一个元素、并且使选出来的元素间不存在矛盾关系、
假如X1与Y1有矛盾、则我们连两条有向边X1-->Y2、Y1-->X2、然后求强连通、如果X1和Y2在同一个强连通分量中则无解、
如果要求解选择方案的话需要进行拓扑排序、不过如果是求最小字典序的解有更简单的做法、
图论中还有各种繁多复杂的内容、如混合欧拉图回路、最大密度子图等等、 需要自己不断的积累学习、
最后、图论就总结到这里了、
----- 黑白是一种态度

浙公网安备 33010602011771号