图论

图论给我的感觉就是知识点特别多、各种概念、对于初学者来说是个不小的挑战、

 

先从最基本的图的遍历说起、图的广度和深度优先遍历、也就是图上的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;  
}  
View Code

 

题目链接: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;  
}  
View Code

 

然后是拓扑排序、

拓扑排序其实就是在一个有向无环图中、从第一个入度为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;  
}  
View Code

 

接下来就是最小生成树了、其实只需要掌握两种最小生成树的算法就可以了、

对于稠密图来说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;  
}  
View Code

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;  
}  
View Code

 

当让最小生成树还有许多扩展、比如次小生成树、最优比率生成树、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;  
}  
View Code

 

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;  
}  
View Code

 

 

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;  
}  
View Code

 

差分约束系统、最短路衍生出来的一类经典的问题、

其实就是根据松弛原理、将不等式转化为最短路的来做、具体的内容看论文《树与图的完美结合--浅析差分约束系统》

下面我贴一份差分约束的代码、用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;  
}  
View Code

 

 最短路还有一个扩展是求第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;  
}  
View Code

 

 

接下来是图论中最神奇的部分、网络流、

网络流中一个重要的关系:最大流 = 最小割

网络流有几种优秀的算法、dinic、sap、isap、

下面给出DINIC算法的模板、

 CF  546E

 

// 黑白算法之网络流
#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;
}
View Code

 

网络流的精华在于建图、从最基本的拆点、拆边到奇偶建图、平面图转最短路等等才是网络流的神奇所在、

建议学习论文《网络流建模汇总》

网络流的扩展流量有上下界的网络流、需要构造一个伴随网络、

还有最小费用最大流和最大费用最大流等等、

下面给出最小费用最大流模板、求最大费用只需要取相反数、结果取相反数即可、

// 黑白算法之费用流
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;
}
View Code

 

 

二分图是图论一个非常有趣的分支、可以很好地提高抽象模型与建图能力、同时算法思想也比较吸引人、

判断二分图:交叉染色法(奇偶染色)、如果图中存在奇环即表示非二分图、

最小点覆盖 = 最大匹配

最大点独立集 = 顶点数 - 最大匹配

有向图最小路径覆盖 = 原图顶点数 - 拆点后最大匹配

最小边覆盖 = 最大点独立集

 

二分图最大匹配匈牙利算法、复杂度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;
}
View Code

 

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;
}
View Code

 

二分图多重匹配的一般做法就是给每个点开一个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;
}
View Code

 

一般图匹配问题、解法是带花树算法、复杂度是O(V^3)

 

2-sat问题即2判定性问题、问题描述基本上是存在多个二元组、某些组中的一个元素与另一组中的某一个元素存在矛盾、现在要求每一个

二元组中选择一个元素、并且使选出来的元素间不存在矛盾关系、

假如X1与Y1有矛盾、则我们连两条有向边X1-->Y2、Y1-->X2、然后求强连通、如果X1和Y2在同一个强连通分量中则无解、

如果要求解选择方案的话需要进行拓扑排序、不过如果是求最小字典序的解有更简单的做法、

 

图论中还有各种繁多复杂的内容、如混合欧拉图回路、最大密度子图等等、 需要自己不断的积累学习、

 

最后、图论就总结到这里了、

           -----    黑白是一种态度

posted @ 2015-06-24 18:09  Mzx0821  阅读(355)  评论(0)    收藏  举报