最大流(网络流基础概念+三个算法)

下面是由一道题引发的一系列故事。。。

题目链接 http://poj.org/problem?id=1273

Drainage Ditches
Time Limit: 1000MS
Memory Limit: 10000K
Total Submissions: 68920
Accepted: 26683

Description

Every time it rains on Farmer John's fields, a pond forms over Bessie's favorite clover patch. This means that the clover is covered by water for awhile and takes quite a long time to regrow. Thus, Farmer John has built a set of drainage ditches so that Bessie's clover patch is never covered in water. Instead, the water is drained to a nearby stream. Being an ace engineer, Farmer John has also installed regulators at the beginning of each ditch, so he can control at what rate water flows into that ditch.
Farmer John knows not only how many gallons of water each ditch can transport per minute but also the exact layout of the ditches, which feed out of the pond and into each other and stream in a potentially complex network.
Given all this information, determine the maximum rate at which water can be transported out of the pond and into the stream. For any given ditch, water flows in only one direction, but there might be a way that water can flow in a circle.

Input

The input includes several cases. For each case, the first line contains two space-separated integers, N (0 <= N <= 200) and M (2 <= M <= 200). N is the number of ditches that Farmer John has dug. M is the number of intersections points for those ditches. Intersection 1 is the pond. Intersection point M is the stream. Each of the following N lines contains three integers, Si, Ei, and Ci. Si and Ei (1 <= Si, Ei <= M) designate the intersections between which this ditch flows. Water will flow through this ditch from Si to Ei. Ci (0 <= Ci <= 10,000,000) is the maximum rate at which water will flow through the ditch.

Output

For each case, output a single integer, the maximum rate at which water may emptied from the pond.

Sample Input

5 4
1 2 40
1 4 20
2 4 20
2 3 30
3 4 10

Sample Output

50


         直接看输入输出。输入:n和m,分别代表边的数量和节点数。之后的n行,输入三个数,代表(u,v)边的最大水容量。要求输出,从起点1开始到终点m的最大水流量。那么,与实际相联系,很容易知道,对于每一条路径,都最大只能运输该路径上的最小容量的那条边所能承受的水量。所以,这个问题就是传说中的网络流之最大流问题。

       经过查阅资料,先理清一下基本的概念:

容量网络:设G(V,E),是一个有向网络,在V中指定了一个顶点,称为源点(记为Vs),以及另一个顶点,称为汇点(记为Vt);对于每一条弧<u,v>属于E,对应有一个权值c(u,v)>0,称为弧的容量.通常吧这样的有向网络G称为容量网络.

弧的流量:通过容量网络G中每条弧<u,v>,上的实际流量(简称流量),记为f(u,v);

网络流:所有弧上流量的集合f={f(u,v)},称为该容量网络的一个网络流.

可行流:在容量网络G中满足以下条件的网络流f,称为可行流.

    a.弧流量限制条件:   0<=f(u,v)<=c(u,v);

    b:平衡条件:即流入一个点的流量要等于流出这个点的流量,(源点和汇点除外).

若网络流上每条弧上的流量都为0,则该网络流称为零流.

伪流:如果一个网络流只满足弧流量限制条件,不满足平衡条件,则这种网络流为伪流,或称为容量可行流.(预流推进算法有用)

最大流:在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流.

弧的类型:

a.饱和弧:即f(u,v)=c(u,v);

b.非饱和弧:即f(u,v)<c(u,v);

c.零流弧:即f(u,v)=0;

d.非零流弧:即f(u,v)>0.

链:在容量网络中,称顶点序列(u1,u2,u3,u4,..,un,v)为一条链要求相邻的两个顶点之间有一条弧.

设P是G中一条从Vs到Vt的链,约定从Vs指向Vt的方向为正方向.在链中并不要求所有的弧的方向都与链的方向相同.

a.前向弧:(方向与链的正方向一致的弧),其集合记为P+,

b.后向弧:(方向与链的正方向相反的弧),其集合记为P-.

增广路:

设f是一个容量网络G中的一个可行流,P是从Vs到Vt 的一条链,若P满足以下条件:

a.P中所有前向弧都是非饱和弧,

b.P中所有后向弧都是非零弧.

则称P为关于可行流f 的一条增广路.

沿这增广路改进可行流的操作称为增广.

残留容量:给定容量网络G(V,E),及可行流f,弧<u,v>上的残留容量记为cl(u,v)=c(u,v)-f(u,v).每条弧上的残留容量表示这条弧上可以增加的流量.因为从顶点u到顶点v的流量减少,等效与从顶点v到顶点u的流量增加,所以每条弧<u,v>上还有一个反方向的残留容量cl(v,u)=-f(u,v).

残留网络:设有容量网络G(V,E)及其上的网络流f,G关于f的残留网络记为G(V',E').其中G'的顶点集V'和G中顶点集G相同,V'=V.对于G中任何一条弧<u,v>,如果f(u,v)<c(u,v),那么在G'中有一条弧<u,v>属于E',其容量为c'(u,v)=c(u,v)-f(u,v),如果f(u,v)>0,则在G'中有一条弧<v,u>属于E',其容量为c'(v,u)=f(u,v).残留网络也称为剩余网络.


下面是所有最大流算法的精华部分: 引入反向边

为什么要有反向边呢?

 

我们第一次找到了1-2-3-4这条增广路,这条路上的delta值显然是1。于是我们修改后得到了下面这个流。(图中的数字是容量)

 

这时候(1,2)和(3,4)边上的流量都等于容量了,我们再也找不到其他的增广路了,当前的流量是1。

但这个答案明显不是最大流,因为我们可以同时走1-2-4和1-3-4,这样可以得到流量为2的流。

那么我们刚刚的算法问题在哪里呢?问题就在于我们没有给程序一个”后悔”的机会,应该有一个不走(2-3-4)而改走(2-4)的机制。那么如何解决这个问题呢?回溯搜索吗?那么我们的效率就上升到指数级了。

而这个算法神奇的利用了一个叫做反向边的概念来解决这个问题。即每条边(I,j)都有一条反向边(j,i),反向边也同样有它的容量。

我们直接来看它是如何解决的:

在第一次找到增广路之后,在把路上每一段的容量减少delta的同时,也把每一段上的反方向的容量增加delta。即在Dec(c[x,y],delta)的同时,inc(c[y,x],delta)

我们来看刚才的例子,在找到1-2-3-4这条增广路之后,把容量修改成如下

这时再找增广路的时候,就会找到1-3-2-4这条可增广量,即delta值为1的可增广路。将这条路增广之后,得到了最大流2。

 

那么,这么做为什么会是对的呢?我来通俗的解释一下吧。

事实上,当我们第二次的增广路走3-2这条反向边的时候,就相当于把2-3这条正向边已经是用了的流量给”退”了回去,不走2-3这条路,而改走从2点出发的其他的路也就是2-4。(有人问如果这里没有2-4怎么办,这时假如没有2-4这条路的话,最终这条增广路也不会存在,因为他根本不能走到汇点)同时本来在3-4上的流量由1-3-4这条路来”接管”。而最终2-3这条路正向流量1,反向流量1,等于没有流量。

这就是这个算法的精华部分,利用反向边,使程序有了一个后悔和改正的机会


第一个隆重登场的算法是 EK(Edmond—Karp)算法

感谢http://www.cnblogs.com/zsboy/archive/2013/01/27/2878810.html 的博文,里面有很详细的我要给出的第一个代码的模板和对于该算法的深刻理解

先给出模板(也是为了方便以后自己查阅)

<strong><span style="font-size:12px;">#include <iostream>
#include <queue>
#include<string.h>
using namespace std;
#define arraysize 201
int maxData = 0x7fffffff;
int capacity[arraysize][arraysize]; //记录残留网络的容量
int flow[arraysize];                //标记从源点到当前节点实际还剩多少流量可用
int pre[arraysize];                 //标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中
int n,m;
queue<int> myqueue;
int BFS(int src,int des)
{
    int i,j;
    while(!myqueue.empty())       //队列清空
        myqueue.pop();
    for(i=1;i<m+1;++i)
    {
        pre[i]=-1;
    }
    pre[src]=0;
    flow[src]= maxData;
    myqueue.push(src);
    while(!myqueue.empty())
    {
        int index = myqueue.front();
        myqueue.pop();
        if(index == des)            //找到了增广路径
            break;
        for(i=1;i<m+1;++i)
        {
            if(i!=src && capacity[index][i]>0 && pre[i]==-1)
            {
                 pre[i] = index; //记录前驱
                 flow[i] = min(capacity[index][i],flow[index]);   //关键:迭代的找到增量
                 myqueue.push(i);
            }
        }
    }
    if(pre[des]==-1)      //残留图中不再存在增广路径
        return -1;
    else
        return flow[des];
}
int maxFlow(int src,int des)
{
    int increasement= 0;
    int sumflow = 0;
    while((increasement=BFS(src,des))!=-1)
    {
         int k = des;          //利用前驱寻找路径
         while(k!=src)
         {
              int last = pre[k];
              capacity[last][k] -= increasement; //改变正向边的容量
              capacity[k][last] += increasement; //改变反向边的容量
              k = last;
         }
         sumflow += increasement;
    }
    return sumflow;
}
int main()
{
    int i,j;
    int start,end,ci;
    while(cin>>n>>m)
    {
        memset(capacity,0,sizeof(capacity));
        memset(flow,0,sizeof(flow));
        for(i=0;i<n;++i)
        {
            cin>>start>>end>>ci;
            if(start == end)               //考虑起点终点相同的情况
               continue;
            capacity[start][end] +=ci;     //此处注意可能出现多条同一起点终点的情况
        }
        cout<<maxFlow(1,m)<<endl;
    }
    return 0;
}</span></strong>


EK算法的核心
反复寻找源点s到汇点t之间的增广路径,若有,找出增广路径上每一段[容量-流量]的最小值delta,若无,则结束。
在寻找增广路径时,可以用BFS来找,并且更新残留网络的值(涉及到反向边)。
而找到delta后,则使最大流值加上delta,更新为当前的最大流值。

对于BFS找增广路:

1.         flow[1]=INF,pre[1]=0;

        源点1进队列,开始找增广路,capacity[1][2]=40>0,则flow[2]=min(flow[1],40)=40;

        capacity[1][4]=20>0,则flow[4]=min(flow[1],20)=20;

        capacity[2][3]=30>0,则flow[3]=min(folw[2]=40,30)=30;

        capacity[2][4]=30,但是pre[4]=1(已经在capacity[1][4]这遍历过4号点了)

        capacity[3][4].....

        当index=4(汇点),结束增广路的寻找

        传递回increasement(该路径的流),利用前驱pre寻找路径

路径也自然变成了这样:

2.flow[1]=INF,pre[1]=0;

 源点1进队列,开始找增广路,capacity[1][2]=40>0,则flow[2]=min(flow[1],40)=40;

        capacity[1][4]=0!>0,跳过

        capacity[2][3]=30>0,则flow[3]=min(folw[2]=40,30)=30;

        capacity[2][4]=30,pre[4]=2,则flow[2][4]=min(flow[2]=40,20)=20;

        capacity[3][4].....

        当index=4(汇点),结束增广路的寻找

        传递回increasement(该路径的流),利用前驱pre寻找路径

 图也被改成

  

接下来同理

这就是最终完成的图,最终sumflow=20+20+10=50(这个就是最大流的值)


下面是我用这个方法写的本题的代码


<strong><span style="font-size:12px;">#include <cstdio>
#include <algorithm>
#include <queue>
#include <string.h>
using namespace std;
int const MAX = 1005;
int const inf = 0x3f3f3f3f;
int c[MAX][MAX];//c[u][v]保存容量
int f[MAX][MAX];//f[u][v]保存当前流量
int a[MAX];// a数组在每趟bfs中找到最小路径中最小残余流量的,a数组使个递推数组,a[v]的意思是从源点s到点v的最小残余流量
int p[MAX];//保存前一个点
int n, m;
int bfs(int s, int t)
{
    queue<int> q;
    int flow = 0;
    while(!q.empty())   q.pop();
    memset(f, 0, sizeof(f));
    while(1){
        memset(a, 0, sizeof(a));
        a[s] = inf;//将起始点的最小残余量设为最大
        q.push(s);
        while(!q.empty()){//bfs找到一条最短路,这里的边不代表距离,可以看作每两个点都是单位距离的
            int u;
            u = q.front();
            q.pop();
            for(int v = 1; v <= m; v++){//枚举所有点v <u,v>
                if(!a[v] && c[u][v] > f[u][v]){//a[]可以代替vis[],来判断这个点是否已经遍历过,后面那个条件更是起了关键作用,很巧妙
                    p[v] = u;
                    q.push(v);
                    a[v] = min(a[u], c[u][v] - f[u][v]);//递推
                }
            }
        }
        if(!a[t])   break;//直到最小残余流量为0时,退出
        for(int u = t; u != s; u = p[u]){
            f[p[u]][u] += a[t];
            f[u][p[u]] -= a[t];
        }
        flow += a[t];
    }
    return flow;
}

int main()
{
    while(~scanf("%d %d", &n, &m)){
        memset(c, 0, sizeof(c));
        memset(p, 0, sizeof(p));
        for(int i = 1; i <= n; i++){
            int u, v, w;
            scanf("%d %d %d", &u, &v, &w);
            c[u][v] += w;
        }
        printf("%d\n", bfs(1, m));
    }
    return 0;
}</span></strong>

第二个隆重登场的算法,Ford-Fulkerson算法,简单易懂,老少皆宜


基于邻接矩阵的一个模板

<strong><span style="font-size:12px;">#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
int map[300][300];
int used[300];
int n,m;
const int INF = 1000000000;
int dfs(int s,int t,int f)
{
    if(s == t) return f;
    for(int i = 1 ; i <= n ; i ++) {
        if(map[s][i] > 0 && !used[i]) {
            used[i] = true;
            int d = dfs(i,t,min(f,map[s][i]));
            if(d > 0) {
                map[s][i] -= d;
                map[i][s] += d;
                return d;
            }
        }
    }
}
int maxflow(int s,int t)
{
    int flow = 0;
    while(true) {
        memset(used,0,sizeof(used));
        int f = dfs(s,t,INF);//不断找从s到t的增广路
        if(f == 0) return flow;//找不到了就回去
        flow += f;//找到一个流量f的路
    }
}
int main()
{
    while(scanf("%d%d",&m,&n) != EOF) {
        memset(map,0,sizeof(map));
        for(int i = 0 ; i < m ; i ++) {
            int from,to,cap;
            scanf("%d%d%d",&from,&to,&cap);
            map[from][to] += cap;
        }
        cout << maxflow(1,n) << endl;
    }
    return 0;</span>
}</strong>

下面是我用vector写的Ford-Fulkerson算法的本题代码

<span style="font-size:12px;"><strong>#include <cstdio>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;
int const inf = 0x3f3f3f3f;
int const MAX = 300;
struct Node
{
    int to;  //与这个点相连的点
    int cap; //以这个射出的边的容量
    int rev; //这个点的反向边
};
vector<Node> v[MAX];
bool used[MAX];

void add_node(int from, int to, int cap)//重边情况不影响
{
    v[from].push_back((Node){to, cap, v[to].size()});
    v[to].push_back((Node){from, 0, v[from].size() - 1});
}
int dfs(int s, int t, int f)
{
    if(s == t)
        return f;
    used[s] = true;
    for(int i = 0; i < v[s].size(); i++){
        Node &tmp = v[s][i];
        if(used[tmp.to] == false && tmp.cap > 0){
            int d = dfs(tmp.to, t, min(f, tmp.cap));
            if(d > 0){
                tmp.cap -= d;
                v[tmp.to][tmp.rev].cap += d;
                return d;
            }
        }
    }
    return 0;
}
int max_flow(int s, int t)
{
    int flow = 0;
    while(1){
        memset(used, false, sizeof(used));
        int f = dfs(s, t, inf);
        if(f == 0)
            return flow;
        flow += f;
    }
    return flow;
}
int main()
{
    int n, m;
    while(scanf("%d %d", &n, &m) != EOF){
        for(int i = 0; i <= m; i++)
            v[i].clear();
        int u1, v1, w;
        for(int i = 1; i <= n; i++){
            scanf("%d %d %d", &u1, &v1, &w);
            add_node(u1, v1, w);
        }
        printf("%d\n", max_flow(1, m));
    }
    return 0;
}</strong></span>


第三种方法:Dinic算法,可以看作是两种方法的结合体,它进行了一定的优化,对于某些横边多的图,运行速度方面得到了大幅提升

Dinic算法的基本思路:
       根据残量网络计算层次图。

       在层次图中使用DFS进行增广直到不存在增广路

       重复以上步骤直到无法增广

  • 层次图:分层图,以[从原点到某点的最短距离]分层的图,距离相等的为一层,(比如上图的分层为{1},{2,4},{3})
       观察前面的dfs算法,对于层次相同的边,会经过多次重复运算,很浪费时间,那么,可以考虑先对原图分好层产生新的层次图,即保存了每个点的层次,注意,很多人会把这里的边的最大容量跟以前算最短路时的那个权值混淆,其实这里每个点之间的距离都可以看作单位距离,然后对新图进行dfs,这时的dfs就非常有层次感,有筛选感了,同层次的点不可能在同一跳路径中,直接排除。那么运行速度就会快很多了。

<span style="font-size:12px;"><strong>#include <cstdio>
#include <string.h>
#include <queue>
using namespace std;
int const inf = 0x3f3f3f3f;
int const MAX = 205;
int n, m;
int c[MAX][MAX], dep[MAX];//dep[MAX]代表当前层数

int bfs(int s, int t)//重新建图,按层次建图
{
    queue<int> q;
    while(!q.empty())
        q.pop();
    memset(dep, -1, sizeof(dep));
    dep[s] = 0;
    q.push(s);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int v = 1; v <= m; v++){
            if(c[u][v] > 0 && dep[v] == -1){//如果可以到达且还没有访问,可以到达的条件是剩余容量大于0,没有访问的条件是当前层数还未知
                dep[v] = dep[u] + 1;
                q.push(v);
            }
        }
    }
    return dep[t] != -1;
}

int dfs(int u, int mi, int t)//查找路径上的最小流量
{
    if(u == t)
        return mi;
    int tmp;
    for(int v = 1; v <= m; v++){
        if(c[u][v] > 0 && dep[v] == dep[u] + 1  && (tmp = dfs(v, min(mi, c[u][v]), t))){
            c[u][v] -= tmp;
            c[v][u] += tmp;
            return tmp;
        }
    }
    return 0;
}

int dinic()
{
    int ans = 0, tmp;
    while(bfs(1, m)){
        while(1){
            tmp = dfs(1, inf, m);
            if(tmp == 0)
                break;
            ans += tmp;
        }
    }
    return ans;
}

int main()
{
    while(~scanf("%d %d", &n, &m)){
        memset(c, 0, sizeof(c));
        int u, v, w;
        while(n--){
            scanf("%d %d %d", &u, &v, &w);
            c[u][v] += w;
        }
        printf("%d\n", dinic());
    }
    return 0;
}</strong></span>

洗洗睡了。。明天继续研究其他几个网络流的算法








  • 60
    点赞
  • 221
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
在有关算法的书中,有一些叙述非常严谨,但不够全面;另一些涉及了大量的题材,但又缺乏严谨性。本书将严谨性和全面性融为一体,深入讨论各类算法,并着力使这些算法的设计和分析能为各个层次的读者接受。全书各章自成体系,可以作为独立的学习单元;算法以英语和伪代码的形式描述,具备初步程序设计经验的人就能看懂;说明和解释力求浅显易懂,不失深度和数学严谨性。 --------------------------------------------------------------- 目录 Introduction to Algorithms, Third Edition 出版者的话 译者序 前言 第一部分 基础知识 第1章 算法在计算中的作用  1.1 算法  1.2 作为一种技术的算法  思考题  本章注记 第2章 算法基础  2.1 插入排序  2.2 分析算法  2.3 设计算法   2.3.1 分治法   2.3.2 分析分治算法  思考题  本章注记 第3章 函数的增长  3.1 渐近记号  3.2 标准记号与常用函数  思考题  本章注记 第4章 分治策略  4.1 最大子数组问题  4.2 矩阵乘法的Strassen算法  4.3 用代入法求解递归式  4.4 用递归树方法求解递归式  4.5 用主方法求解递归式  4.6 证明主定理   4.6.1 对b的幂证明主定理   4.6.2 向下取整和向上取整  思考题  本章注记 第5章 概率分析和随机算法  5.1 雇用问题  5.2 指示器随机变量  5.3 随机算法  ?5.4 概率分析和指示器随机变量的进一步使用   5.4.1 生日悖论   5.4.2 球与箱子   5.4.3 特征序列   5.4.4 在线雇用问题  思考题  本章注记 第二部分 排序和顺序统计量 第6章 堆排序  6.1 堆  6.2 维护堆的性质  6.3 建堆  6.4 堆排序算法  6.5 优先队列  思考题  本章注记 第7章 快速排序  7.1 快速排序的描述  7.2 快速排序的性能  7.3 快速排序的随机化版本  7.4 快速排序分析   7.4.1 最坏情况分析   7.4.2 期望运行时间  思考题  本章注记 第8章 线性时间排序  8.1 排序算法的下界  8.2 计数排序  8.3 基数排序  8.4 桶排序  思考题  本章注记 第9章 中位数和顺序统计量  9.1 最小值和最大值  9.2 期望为线性时间的选择算法  9.3 最坏情况为线性时间的选择算法  思考题  本章注记 第三部分 数据结构 第10章 基本数据结构  10.1 栈和队列  10.2 链表  10.3 指针和对象的实现  10.4 有根树的表示  思考题  本章注记 第11章 散列表  11.1 直接寻址表  11.2 散列表  11.3 散列函数   11.3.1 除法散列法   11.3.2 乘法散列法   11.3.3 全域散列法  11.4 开放寻址法  11.5 完全散列  思考题  本章注记 第12章 二叉搜索树  12.1 什么是二叉搜索树  12.2 查询二叉搜索树  12.3 插入和删除  12.4 随机构建二叉搜索树  思考题  本章注记 第13章 红黑树  13.1 红黑树的性质  13.2 旋转  13.3 插入  13.4 删除  思考题  本章注记 第14章 数据结构的扩张  14.1 动态顺序统计  14.2 如何扩张数据结构  14.3 区间树  思考题  本章注记 第四部分 高级设计和分析技术 第15章 动态规划  15.1 钢条切割  15.2 矩阵链乘法  15.3 动态规划原理  15.4 最长公共子序列  15.5 最优二叉搜索树  思考题  本章注记 第16章 贪心算法  16.1 活动选择问题  16.2 贪心算法原理  16.3 赫夫曼编码  16.4 拟阵和贪心算法  16.5 用拟阵求解任务调度问题  思考题  本章注记 第17章 摊还分析  17.1 聚合分析  17.2 核算法  17.3 势能法  17.4 动态表   17.4.1 表扩张   17.4.2 表扩张和收缩  思考题  本章注记 第五部分 高级数据结构 第18章 B树  18.1 B树的定义  18.2 B树上的基本操作  18.3 从B树中删除关键字  思考题  本章注记 第19章 斐波那契堆  19.1 斐波那契堆结构  19.2 可合并堆操作  19.3 关键字减值和删除一个结点  19.4 最大度数的界  思考题  本章注记 第20章 van Emde Boas树  20.1 基本方法  20.2 递归结构   20.2.1 原型van Emde Boas结构   20.2.2 原型van Emde Boas结构上的操作  20.3 van Emde Boas树及其操作   20.3.1 van Emde Boas树   20.3.2 van Emde Boas树的操作  思考题  本章注记 第21章 用于不相交集合的数据结构  21.1 不相交集合的操作  21.2 不相交集合的链表表示  21.3 不相交集合森林  *21.4 带路径压缩的按秩合并的分析  思考题  本章注记 第六部分 图算法 第22章 基本的图算法  22.1 图的表示  22.2 广度优先搜索  22.3 深度优先搜索  22.4 拓扑排序  22.5 强连通分量  思考题  本章注记 第23章 最小生成树  23.1 最小生成树的形成  23.2 Kruskal算法和Prim算法  思考题  本章注记 第24章 单源最短路径  24.1 Bellman?Ford算法  24.2 有向无环图中的单源最短路径问题  24.3 Dijkstra算法  24.4 差分约束和最短路径  24.5 最短路径性质的证明  思考题  本章注记 第25章 所有结点对的最短路径问题  25.1 最短路径和矩阵乘法  25.2 Floyd?Warshall算法  25.3 用于稀疏图的Johnson算法  思考题  本章注记 第26章 最大流  26.1 网络  26.2 Ford\Fulkerson方法  26.3 最大二分匹配  26.4 推送重贴标签算法  26.5 前置重贴标签算法  思考题  本章注记 第七部分 算法问题选编 第27章 多线程算法  27.1 动态多线程基础  27.2 多线程矩阵乘法  27.3 多线程归并排序  思考题  本章注记 第28章 矩阵运算  28.1 求解线性方程组  28.2 矩阵求逆  28.3 对称正定矩阵和最小二乘逼近  思考题  本章注记 第29章 线性规划  29.1 标准型和松弛型  29.2 将问题表达为线性规划  29.3 单纯形算法  29.4 对偶性  29.5 初始基本可行解  思考题  本章注记 第30章 多项式与快速傅里叶变换  30.1 多项式的表示  30.2 DFT与FFT  30.3 高效FFT实现  思考题  本章注记 第31章 数论算法  31.1 基础数论概念  31.2 最大公约数  31.3 模运算  31.4 求解模线性方程  31.5 中国余数定理  31.6 元素的幂  31.7 RSA公钥加密系统  31.8 素数的测试  31.9 整数的因子分解  思考题  本章注记 第32章 字符串匹配  32.1 朴素字符串匹配算法  32.2 Rabin\Karp算法  32.3 利用有限自动机进行字符串匹配  32.4 Knuth?Morris?Pratt算法  思考题  本章注记 第33章 计算几何学  33.1 线段的性质  33.2 确定任意一对线段是否相交  33.3 寻找凸包  33.4 寻找最近点对  思考题  本章注记 第34章 NP完全性  34.1 多项式时间  34.2 多项式时间的验证  34.3 NP完全性与可归约性  34.4 NP完全性的证明  34.5 NP完全问题   34.5.1 团问题   34.5.2 顶点覆盖问题   34.5.3 哈密顿回路问题   34.5.4 旅行商问题   34.5.5 子集和问题  思考题  本章注记 第35章 近似算法  35.1 顶点覆盖问题  35.2 旅行商问题  35.2.1 满足三角不等式的旅行商问题  35.2.2 一般旅行商问题  35.3 集合覆盖问题  35.4 随机化和线性规划  35.5 子集和问题  思考题  本章注记 第八部分 附录:数学基础知识 附录A 求和  A.1 求和公式及其性质  A.2 确定求和时间的界  思考题  附录注记 附录B 集合等离散数学内容  B.1 集合  B.2 关系  B.3 函数  B.4 图  B.5 树   B.5.1 自由树   B.5.2 有根树和有序树   B.5.3 二叉树和位置树  思考题  附录注记 附录C 计数与概率  C.1 计数  C.2 概率 C.3 离散随机变量  C.4 几何分布与二项分布  *C.5 二项分布的尾部  思考题  附录注记 附录D 矩阵  D.1 矩阵与矩阵运算  D.2 矩阵基本性质  思考题  附录注记

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值