ACM图论知识总结

一. 欧拉图

1. 定义:

欧拉图是指通过图(无向图或有向图)中所有边且每边仅通过一次的通路,相应的回路称为欧拉回路 。

2. 性质:

  1. 欧拉图均为连通图;
  2. 无向连通图 G 是欧拉图,则其不含奇数度结点(所有结点度数均为偶数);
  3. 无向连通图 G 是欧拉通路,则其没有或有两个奇数度的结点,这两个节点为欧拉通路的起点与终点;
  4. 有向连通图 D 是欧拉图,则其每个结点的入度=出度;
  5. 有向连通图 D 是欧拉通路,则其除起点与终点外,其余每个结点的入度=出度;
  6. 上述两点两点均满足:起始点的入度 = 出度 - 1 ,结束点的出度 = 入度 - 1 或 两个点的入度 = 出度(主要针对含有欧拉图或欧拉回路的图)。

3. 代码:

//无向图欧拉通路、回路,每个边的度只能为1或2,度为1的点只能为0或2
#include
using namespace std;
int const maxn = 100;
vector<int>path;
int n,m,mmp[maxn][maxn],cnt[maxn];
string str;
void dfs(int o)
{
    for(int i=0; i<n; i++)
    {
        if(mmp[o][i])
        {
            mmp[o][i] = mmp[i][o] = 0;
            dfs(i);
        }
    }
    path.push_back(o);
}
int main(){
    cin>>n>>m;
    int a,b;
    for(int i=0; i<m; ++i){
        cin>>a>>b;
        mmp[a][b]=mmp[b][a]=1;
        cnt[a]++;cnt[b]++;
    }
    int flag=0,x,y,Count=0;
    for(int i=0; i<n; ++i)
        if(cnt[i]&1){//度为奇数
            Count++;//统计
            if(Count==1)
                x=i;//标记起点
            else
                y=i;//标记终点
        }
        else if(cnt[i]>0&&!flag)//若为欧拉回路,任意起点
            flag=i;
    if(Count==1||Count>2)
        cout<<"非欧拉图";
    else{
        if(Count==0)
            dfs(flag);
        else
            x<y?dfs(x):dfs(y);//模板例题要求从最小点输出
        reverse(path.begin(),path.end());
        for(int i=0; i<path.size(); ++i)
            cout<<wdnmd[path[i]];
    }
    return 0;
}

4. 例题:

洛谷P1341 无序字母对

二. 拓扑排序

1. 定义:

在一个有向图中,对所有的节点进行排序,没有一个节点指向它前面的节点。

2. 性质:

  1. 图中无环,否则无法进行拓扑排序。

3. 代码:

  1. 统计所有节点的入度,将入度为 0 的节点分离出来;
  2. 把分离出的节点指向的节点的入度减一;
  3. 重复上述操作,直到所有的节点都被分离出来;
  4. 如果最后不存在入度为 0 的节点,说明有环,不存在拓扑排序,即无解。
#include
using namespace std;
vector<int>edge[30];//邻接表
int in[30];//记录入度
set<int>mmp;//标记
int main()
{
    int s1,s2;
    while(cin>>s1>>s2)
    {
        mmp.insert(s1);
        mmp.insert(s2);//标记图中记录的点
        in[s2]++;//记录入度
        edge[s1].push_back(s2);//记录邻接表
    }
    priority_queue<int,vector<int>,greater<int> >ans;//大部分题目要求将拓扑序列按照字典序最小的情况输出,故需要用到优先队列来处理
    for(int i=0;i<30;i++)//入度为0的点入队
        if(in[i]==0&&mmp.count(i)!=0)
            ans.push(i);
    queue<int>Count;//记录出队顺序以及数量
    while(!ans.empty())
    {
        int flag=ans.top();
        ans.pop();//入度为0的点出队
        Count.push(flag);//按顺序入队
        for(int i=0;i<edge[flag].size();i++)//将该点所发出的边去掉
        {
            in[edge[flag][i]]--;
            if(in[edge[flag][i]]==0&&mmp.count(edge[flag][i])!=0)//如果有点的入度更新为0,则入优先队列
                ans.push(edge[flag][i]);
        }
    }
    if(Count.size()==mmp.size())//判断是否有环
        while(!Count.empty())
        {
            cout<<Count.front();
            Count.pop();
        }
    else cout<<"No Answer!";//有环的输出
    return 0;
}

4. 例题:

2153: D.ly的排队问题

三. 最短路

1. 定义:

从图中的某个顶点出发到达另外一个顶点的所经过的边的权重和最小的一条路径,称为最短路径。

2. Dijkstra算法:

  1. 声明一个数组 d 来保存源点到各个顶点的最短距离;
  2. 声明一个保存已经找到了最短路径的顶点的集合 s ;
  3. 声明一个保存各节点起始节点的数组 path ;
  4. 初始化:原点 v0 的路径权重被赋为 0 ( d[ v0 ] = 0) 。将顶点 v0 能直接到达的边( v0 , v ),把 d[ v ] 设为该边权重 w ,把 v0 不能直接到达的顶点路径长度设为无穷大 inf 。初始时,集合 s 只有顶点 v0 ;
  5. 从 d 数组选择最小值,则该值就是源点 v0 到该值对应的顶点的最短路径,把该点加入到 s 中,此时完成一个顶点;
  6. 判断新加入的顶点是否可以更新到达其他点的路径长度,如果是则替换这些顶点在 d 中的值。
  7. 重复上述动作,直到 s 中包含了图的所有顶点。

2.1 代码:

#include
using namespace std;
int const inf = 1e6+5;
int const maxn = 1e3+5;
int edge[maxn][maxn];
int path[100005],d[100005];
bool s[100005];
int v0=1;
int n,m;
void init()
{
    for(int i=1; i<=n; ++i)
    {
        s[i]=false;
        d[i]=edge[v0][i];
        if(d[i]<inf)
            path[i]=v0;
        else
            path[i]=-1;
    }
    s[v0]=true;
    d[v0]=0;
}
void Dijkstra()
{
    int circle=n-1;
    while(circle--)
    {
        int Min=inf,v;
        for(int i=1; i<=n; ++i)
        {
            if(!s[i]&&d[i]<Min)
            {
                v=i;
                Min=d[i];
            }
        }
        s[v]=true;
        for(int i=1; i<=n; ++i)
        {
            if(!s[i]&&(d[v]+edge[v][i]<d[i]))
            {
                d[i]=d[v]+edge[v][i];
                path[i]=v;
            }
        }
    }
}
void output()
{
    for(int i=1; i<=n; ++i)
        if(i!=v0)
            cout<<i<<" "<<d[i]<<endl;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;++i)
        d[i]=inf;
    for(int i=0;i<maxn;++i)
        for(int u=0;u<maxn;++u)
            edge[i][u]=inf;
    while(m--)
    {
        int a,b,w;
        cin>>a>>b>>w;
        edge[a][b]=w;
        edge[b][a]=w;
    }
    init();
    Dijkstra();
    output();
    return 0;
}
2.2.1 优先队列优化(不能处理负边权)
#include 
using namespace std;
const int maxn=1000;
const int maxv=1000;
const int inf=0x3f3f3f3f;
struct mmp
{
    int to,cost;
};
int V;
vector<mmp>edge[maxn];//邻接表
int d[maxv];

void Dijstra(int start)
{
    //通过指定greater>参数,堆按照first从小到大顺序取出值
    priority_queue< pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>> > q;
    fill(d,d+V,inf);//填充,第一个变量为首,第二个为尾,第三个为填充的值
    d[start]=0;//初始化为0
    q.push(pair<int,int>(0,start));//first是最短距离,second是顶点编号
    while(!q.empty())
    {
        pair<int,int> temp=q.top();
        q.pop();
        int v=temp.second;//v表示顶点编号
        if(d[v]<temp.first)
            continue;
        for(int i=0; i<edge[v].size(); i++)
        {
            mmp e=edge[v][i];
            if(d[e.to]>d[v]+e.cost)
            {
                d[e.to]=d[v]+e.cost;
                q.push(pair<int,int>(d[e.to],e.to));
            }
        }
    }
}
2.2.2 路径还原(待测试)
#include
using namespace std;
int prev[maxn];
int d[maxn];
int n;
int edge[maxn][maxn];
bool used[maxn]
void Dijkstra(int s)
{
    fill(d,d+v,inf);
    fill(used,used+n,false);
    fill(prev,prev+n,-1);
    d[s]=0;

    while(true)
    {
        int v=-1;
        for(int i=0; i<n; i++)
            if(!used[i]&& (v==-1 || d[i]<d[v]) )
                v=i;
        if(v==-1)
            break;
        used[v]=true;
        for(int u=0; u<n; u++)
        {
            if(d[u]>d[v]+edge[v][u])
                d[u]=d[v]+edge[v][u];
            prev[u]=v;
        }
    }
}
vector <int>get_path(int t)
{
    vector<int>path;
    for(; t!=-1; t=prev[t])
        path.push_back(t);
    reverse(path.begin(),path.end());
    return path;
}

2.3 例题:

HDU Today(hdu2112)

3. Floyd算法:

  1. 声明一个数组 d 来各个点到各个顶点的最短距离;
  2. 声明一个数组 path 来保存各节点到各节点的起始节点 ;
  3. 初始化:将各节点能直接到达的边( v0 , v1 ),把 d[ v0 ][ v1 ] 设为该边权重 w ,起始节点 path[ v0 ][ v1 ] 值设为 v0 ,把各节点不能直接到达的顶点路径长度设为无穷大 inf ,起始节点 path[ v0 ][ v1 ] 值设为 -1 ;
  4. 遍历所有节点,判断该顶点是否可以更新到达其他点的路径长度,如果是则替换这些顶点在 d 中的值。
  5. 重复上述动作,直到所有节点均被遍历。

3.1 代码:

#include
using namespace std;
int const inf = 0x3f3f3f3f;
int const maxn = 1e3+5;
int path[maxn][maxn];
int d[maxn][maxn];
int edge[maxn][maxn];
int m,n;
void init()
{
    for(int i=0; i<n; ++i)
        for(int u=0; u<n; ++u)
        {
            d[i][u]=edge[i][u];
            if(d[i][u]<inf && i!=u)
                path[i][u]=i;
            else
                path[i][u]=-1;
        }
}
void Floyd()
{
    for(int i=0; i<n; ++i)
        for(int u=0; u<n; ++u)
            for(int o=0; o<n; ++o)
                if(d[u][i]+d[i][o]<d[u][o])
                {
                    d[u][o]=d[u][i]+d[i][o];
                    path[u][o]=path[i][o];
                }
}
void output()
{
    for(int i=0; i<n; ++i)
    {
        for(int u=0; u<n; ++u)
            cout<<d[i][u]<<" ";
        cout<<endl;
    }
}
int main()
{
    for(int i=0; i<maxn; ++i)
        for(int u=0; u<maxn; ++u)
            edge[i][u]=inf;
    cin>>n>>m;
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        if(edge[a][b]>c)
        	edge[a][b]=edge[b][a]=c;
    }
    init();
    Floyd();
    output();
    return 0;
}

3.2 例题:

HDU Shortest Path(hdu3631)

4. spfa 算法:

  1. 算法用一个队列来进行维护,初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,且该点没有在队列中,则将其入队。直到队列为空时算法结束。
  2. 判断有无负环:如果某个点进入队列的次数超过V次则存在负环(SPFA无法处理带负环的图)

4.1 代码:

#include 
using namespace std;
const int inf = 0x3fffffff;
const int maxn = 100;
int edge[maxn][maxn];
int d[maxn];
bool s[maxn];
int num[maxn];//记录每个结点入队的次数
int n,m;
bool spfa(int v0)
{
    queue<int> q;
    memset(s,false,sizeof(s));
    memset(num,0,sizeof(num));
    for(int i=0; i<n; i++)
        d[i] = inf;
    d[v0] = 0;
    q.push(v0);
    s[v0] = true;
    num[v0]++;
    while(!q.empty())
    {
        int p = q.front();
        q.pop();
        for(int i=0; i<n; i++)
        {
            if(d[p]+edge[p][i]<d[i])
            {
                d[i] = d[p]+edge[p][i];
                if(!s[i])
                {
                    q.push(i);
                    num[i]++;
                    if(num[i]>n)//存在负环
                        return false;
                    s[i]=true;
                }
            }
        }
        s[p] = false;
    }
    return true;
}
int main()
{
    int a,b,c;
    cin>>n>>m;
    for(int i=0;i<maxn;++i)
        for(int u=0;u<maxn;++u)
            edge[i][u]=inf;
    for(int i=0; i<m; ++i)
    {
        cin>>a>>b>>c;
        edge[a][b]=edge[b][a]=c;
    }
    if(spfa(0))
        for(int i=0; i<n; ++i)
            cout<<d[i]<<endl;
    else
        cout<<"存在负环"<<endl;
    return 0;
}

4.2 例题:

四. 最小生成树

1. 定义:

G=(V,E)为连通无向图,V为结点的集合,E为结点可能连接的边,对每条边(u ,v)都赋予权重w(u ,v)。找到一个无环子集T, 既能将所有结点连接起来,又具有最小权重。T是由G生成的树,这种问题叫做最小生成树问题。

2. 性质:

  1. G为无向连通图;

3. kruskal算法:

将V的每个结点视为单独节点,定义一个根节点,从根节点开始将E中的边按权重从小到大依次处理。

首先判断边的两个结点是否存在孤立点,若存在,则合并两节点(合成一颗树),并更新根节点;若不存在,则不予理会。

下图为算法过程:

3.1 代码:

#include
using namespace std;
typedef long long ll;
int n, m;
ll ans;
struct Edge
{
    int start, to;
    int val;
} edge[500005];
int pre[100005];
int find(int x)
{
    if(pre[x] == x)
    {
        return x;
    }
    else
    {
        pre[x]=find(pre[x]);
        return pre[x];
    }
}
bool cmp(Edge a, Edge b)
{
    return a.val < b.val;
}
int kruskal()
{
    for(int i = 1; i <= m; i++)//遍历边
    {
        int total=1;
        int u=find(edge[i].start);
        int v=find(edge[i].to);
        if(u == v)
            continue;
        ans += edge[i].val;
        pre[u] = v;
        total++;
        if(total == n)
            break;
    }
    return ans;
}
int main()
{
    cin >> n >> m;
    int root,res=0;
    cin >> root;
    for(int i = 1; i <= n; i++)
        pre[i] = i;
    for(int i = 1; i <= m; i++)
        cin >> edge[i].start >> edge[i].to >> edge[i].val;
    sort(edge + 1, edge + m + 1, cmp);
    cout << kruskal() << endl;
    return 0;
}

4. Prim算法:

给定连通图G和任意根节点r,最小生成树从结点r开始,一直扩大直到覆盖V中所有结点为止,即不断寻找轻量级边以实现最小权重和。

下图为算法过程:

4.1 代码:

#include
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int maxn = 100;
int n, m;
int edge[maxn][maxn];//地图
int d[maxn];//存储距离
int s[maxn];//标记
void init(int v0)
{
    for(int i=1; i<=n; ++i)
    {
        s[i]=false;
        d[i]=edge[v0][i];
    }
    s[v0]=true;
    d[v0]=0;
}
int prim(int v0) //传进起始点
{
    int sum = 0;  //权值总和
    for(int i = 1; i <= n - 1; i ++)
    {
        //  n - 1次迭代
        int minn = inf;
        int v;
        for(int u = 1; u <= n; u ++)     //找到距离最小的点
            if(!s[u] && d[u] < minn)
            {
                minn = d[u];
                v = u;
            }
        s[v] = 1;
        sum += minn;        //加上边权值
        for(int u = 1; u <= n; u ++) //松弛操作
        {
            if(!s[u] && d[u] > edge[v][u])
                d[u] = edge[v][u];
        }
    }
    return sum;
}
int main()
{
    for(int i=0; i<maxn; ++i)
        d[i]=inf;
    for(int i=0; i<maxn; ++i)
        for(int u=0; u<maxn; ++u)
            edge[i][u]=inf;
    cin >> n >> m;
    int v0,res=0;
    cin >> v0;
    int a,b,c;
    for(int i = 1; i <= m; i++)
    {
        cin >> a >> b >> c;
        edge[a][b]=c;
    }
    init(v0);
    cout << prim(v0) << endl;
    return 0;
}

5. 例题:

CCF-CSP201812-4数据中心(最小生成树)

五. 网络流

推荐博客

1. 最大流代码:

#include 
using namespace std;
const int maxn=1000;
const int maxm=100000;
const int inf=0x3f3f3f3f;
struct Edge
{
    int to,c,next;
} edge[maxm];
int p[maxn];//记录每个点的下标
int cnm=0;//总点数
void addedge(int a,int b,int c)
{
    edge[cnm].to=b;
    edge[cnm].next=p[a];
    edge[cnm].c=c;
    p[a]=cnm++;
}
int S,T;                 //源点和汇点
int d[maxn];       //d表示当前点的层数(level)
bool bfs()
{
    memset(d,-1,sizeof(d));
    queue<int>q;
    q.push(S);
    d[S]=0;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=p[u]; i!=-1; i=edge[i].next)
        {
            int v=edge[i].to;
            if(edge[i].c>0 && d[v]==-1)
            {
                q.push(v);
                d[v]=d[u]+1;
            }
        }
    }
    return (d[T]!=-1);
}
int dfs(int u,int f)
{
    if(u==T)
        return f;
    int res=0;
    for(int i=p[u]; i!=-1; i=edge[i].next)
    {
        int v=edge[i].to;
        if(edge[i].c>0 && d[u]+1 == d[v])
        {
            int tmp=dfs(v,min(f,edge[i].c));      //递归计算顶点v,用c(u,v)更新当前流量上限
            f-=tmp;
            edge[i].c-=tmp;
            res+=tmp;
            edge[i^1].c+=tmp;                          //修改反向弧流量
            if(f==0)                                      //流量达到上限,不必继续搜索
                break;
        }
    }
    if(res==0)                    //当前没有经过顶点u的流,不必再搜索顶点u
        d[u]=-1;
    return res;
}
int maxflow()                 //计算最大流
{
    int res=0;
    while(bfs())
    {
        res+=dfs(S,inf);           //初始流量上限为inf
    }
    return res;
}
int main()
{
    memset(p,-1,sizeof(p));
    cnm=0;
    int m,n;
    cin>>n>>m;//n个点m条边
    for(int i=0; i<m; i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        addedge(a,b,c);
        addedge(b,a,0);
    }
/*
    for(int i=1;i<=n;i++)                       //输出图,用于检测图的读入是否正确
       for(int u=p[i];u!=-1;u=edge[u].next)
       {
           cout<"<
    S=1;
    T=n;
    cout<<maxflow()<<endl;
    return 0;
}

你可能感兴趣的:(算法,图论,ACM,算法)