其他分享
首页 > 其他分享> > Day 5 上午

Day 5 上午

作者:互联网

内容提要

图论QwQ


 

学好图论的基础:
必须意识到图论 hendanteng
xuehuifangqi(雾


G = (V; E)
一般来说,图的存储难度主要在记录边的信息
无向图的存储中,只需要将一条无向边拆成两条即可
邻接矩阵:用一个二维数组 edg[N][N] 表示
edg[i][j] 就对应由 i 到 j 的边信息
edg[i][j] 可以记录 Bool,也可以记录边权
缺点:如果有重边有时候不好处理
空间复杂度 O(V^2)
点度等额外信息也是很好维护的

#include <bits/stdc++.h>

using namespace std;

const int N = 5005;

int ideg[N], odeg[N], n, m, edg[N][N];
bool visited[N];

void travel(int u, int distance)
{
    cout << u << " " << distance << endl; visited[u] = true;
    for (int v = 1; v <= n; v++)
        if (edg[u][v] != -1 && !visited[v])//是否已经访问过 
            travel(v, distance + edg[u][v]); //if there is an edge (u, v) and v has not been visited, then travel(v)
}
int main()
{
    cin >> n >> m;
    memset(edg, -1, sizeof edg);
    memset(visited, false, sizeof visited);
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, edg[u][v] = w, odeg[u]++, ideg[v]++;//出度和入度 
    for (int i = 1; i <= n; i++)
        cout << ideg[i] << " " << odeg[i] << endl;
    for (int i = 1; i <= n; i++)
        if (!visited[i]) travel(i, 0);
}

/*
Given a graph with N nodes and M unidirectional edges.
Each edge e_i starts from u_i to v_i and weights w_i
Output a travelsal from node 1 and output degree of each node.
*/

 

邻接表(链式前向星)
对每一个点 u 记录一个 List[u],包含所有从 u 出发的边
直接用数组实现 List[u]?读入边之前不知道 List[u] 长度
手写链表
用 STL 中的 vector 实现变长数组
只需要 O(V + E) 的空间就能实现图的存储

指针版本(一般不用)

#include <bits/stdc++.h>

using namespace std;

const int N = 5005;

struct edge {
    int u, v, w; edge *next;//next指针指向 
    edge(int _u, int _v, int _w, edge *_next):
        u(_u), v(_v), w(_w), next(_next) {}
};
edge *head[N]; //List[u] 最前面的节点是谁 
int ideg[N], odeg[N], n, m;
bool visited[N];

void add(int u, int v, int w)
{
    edge *e = new edge(u, v, w, head[u]);
    head[u] = e;
}
void travel(int u, int distance)
{
    cout << u << " " << distance << endl; visited[u] = true;
    for (edge *e = head[u]; e ; e = e -> next)
        if (!visited[e -> v])
            travel(e -> v, distance + e -> w); //if there is an edge (u, v) and v has not been visited, then travel(v)
}
int main()
{
    cin >> n >> m;
    memset(visited, false, sizeof visited);
    memset(head, 0, sizeof head);
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++;
    for (int i = 1; i <= n; i++)
        cout << ideg[i] << " " << odeg[i] << endl;
    for (int i = 1; i <= n; i++)
        if (!visited[i]) travel(i, 0);
}

/*
Given a graph with N nodes and M unidirectional edges.
Each edge e_i starts from u_i to v_i and weights w_i
Output a travelsal from node 1 and output degree of each node.
*/

 

数组模拟版本:

#include <bits/stdc++.h>

using namespace std;

const int N = 5005;

struct edge {
    int u, v, w, next;
}edg[N];
int head[N]; //List[u] stores all edges start from u
int ideg[N], odeg[N], n, m, cnt; //cnt: numbers of edges
bool visited[N];

void add(int u, int v, int w)
{
    int e = ++cnt;
    edg[e] = (edge){u, v, w, head[u]};
    head[u] = e;
}
void travel(int u, int distance)
{
    cout << u << " " << distance << endl; visited[u] = true;
    for (int e = head[u]; e ; e = edg[e].next)
        if (!visited[edg[e].v])
            travel(edg[e].v, distance + edg[e].w); //if there is an edge (u, v) and v has not been visited, then travel(v)
}
int main()
{
    cin >> n >> m; cnt = 0;
    memset(visited, false, sizeof visited);
    memset(head, 0, sizeof head);
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++;
    for (int i = 1; i <= n; i++)
        cout << ideg[i] << " " << odeg[i] << endl;
    for (int i = 1; i <= n; i++)
        if (!visited[i]) travel(i, 0);
}

/*
Given a graph with N nodes and M unidirectional edges.
Each edge e_i starts from u_i to v_i and weights w_i
Output a travelsal from node 1 and output degree of each node.
*/

 

用edg[u][i]表示从u出发第i条信息,但这样还是n^2
但这样很浪费,因为数组不能全部利用
传统的方法要在定义数组是就规定他的大小,但是我们读入之前并不知道从每个点出发的边数
我们可以用vector
一些 vector 的细节
vector 本质就是 c++ 模板库帮我们实现好的变长数组
向一个数组 a 的末尾加入一个元素 x a:push_back(x)
询问数组 a 的长度 a.size()
注意: vector 中元素下标从 0 开始

#include <bits/stdc++.h>

using namespace std;

const int N = 5005;

struct edge {
    int u, v, w;
};
vector<edge> edg[N]; //edge记录变长数组记录的是什么类型 
int ideg[N], odeg[N], n, m, cnt; //cnt: numbers of edges
bool visited[N];

void add(int u, int v, int w)
{
    edg[u].push_back((edge){u, v, w});//一个强制类型转换 
}
void travel(int u, int distance)
{
    cout << u << " " << distance << endl; visited[u] = true;
    for (int e = 0; e < edg[u].size(); e++)//遍历边 
        if (!visited[edg[u][e].v])//以u出发的第e条出边 
            travel(edg[u][e].v, distance + edg[u][e].w); //if there is an edge (u, v) and v has not been visited, then travel(v)
}
int main()
{
    cin >> n >> m; cnt = 0;
    memset(visited, false, sizeof visited);
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++;
    for (int i = 1; i <= n; i++)
        cout << ideg[i] << " " << odeg[i] << endl;
    for (int i = 1; i <= n; i++)
        if (!visited[i]) travel(i, 0);
}

/*
Given a graph with N nodes and M unidirectional edges.
Each edge e_i starts from u_i to v_i and weights w_i
Output a travelsal from node 1 and output degree of each node.
*/

生成树
给定一个连通无向图 G = (V; E)
E′ ⊂ E
G′ = (V; E′) 构成一棵树
G′ 就是 G 的一个生成树

生成树不是唯一的,并且数量是指数级别

问题描述
给定一个 n 个点 m 条边的带权无向图,求一个生成树,使得生成树中最大边权的最小
数据范围: n; m ≤ 10^6
Algorithms for Minimal Spanning Tree
Kruskal
Prim
Kosaraju


并查集
生成树的本质:选取若干条边使得任意两点连通
维护图中任意两点的连通性
查询任意两点连通性
添加一条边,使两个端点所在连通块合并

Kruskal
Intuitive ideas
将原图无向边按照边权从小到大排序
找到当前边权最小的边 e : (u; v)
如果 u 和 v 已经连通,则直接删除这条边(因为形成了环)
如果 u 和 v 已经未连通,将之加入生成树
重复上述过程
证明:
Rigorous proof
消圈算法:
如果原图中有一个环,然后把这个圈中边权最大的去掉,这样不断去掉最后得到的就是最小生成树
但是由于环不好找,所以不用他

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1000005;
struct edge {
    int u, v, w;
}edg[maxn];
int n, m, p[maxn], ans = 0;

bool cmp(edge a, edge b)
    {return a.w < b.w;}
int findp(int t) 
    {return p[t] ? p[t] = findp(p[t]) : t;}
bool merge(int u, int v)
{
    u = findp(u); v = findp(v);
    if (u == v) return false;
    p[u] = v; return true;
}
int main()
{
    cin >> n >> m;
    for (int i = 1, u, v, w; i <= m; i++)
        cin >> u >> v >> w, edg[i] = (edge){u, v, w};
    sort(edg + 1, edg + m + 1, cmp);
    
    for (int i = 1; i <= m; i++)
        if (merge(edg[i].u, edg[i].v))
            ans = max(ans, edg[i]. w);
    cout << ans << endl;
}

本蒟蒻的博客:传送门


路径
P = p0; ...; pn 为 u 到 v 的路径
p0 = u, pn = v
对于任意i∈2 [1; n]; 存在e : (pi−1; pi)∈E
P 的长度
length(P) = ∑e∈P length(e)

我们一般考虑简单路径,就是不重复经过一个点的路径
显然它也不一定是惟一的

最短路径问题
给定一个有向图 G,询问 u 到 v 之间最短路径长度
记 d(u,v) 表示 u 到 v 的最短路径长度
为方便起见,不妨规定 u 和 v 不连通时, d(u; v) = +1
Algorithms for Shortest Path Problem
floyd
Bellman-Ford
SPFA
Dijkstra

先介绍一下松弛操作
SSP 算法的本质思想就是不断进行松弛操作
d(u; v) ≤ d(u; w) + d(w; v)
我们算出来了一个d(u,v)但是他不一定是最终的d(u,v),这时如果我们找到了d(u,w) + d(w,v)<当前的d(u,v),就更新d(u,v)的值

floyd
初始时, d(u; v) 就等于 u 到 v 的边权
用邻接矩阵的形式记录 d
u 和 v 没边, d(u,v) = +∞
u 和 v 有重边,保留最小边权
三层循环枚举 k; i; j,执行松弛操作
d(i,j) = min{d(i,j); d(i,k) + d(k,j)}

代码:

#include <bits/stdc++.h>

using namespace std;

const int N = 505;
const int inf = 1 << 29;

int d[N][N], n, m;
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) d[i][j] = inf;
    for(int i = 1;i <= n; i++)
        d[i][i]=0; 
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, d[u][v] = min(d[u][v], w);

    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    //必须先枚举中间点 
}

 

本蒟蒻博客:传送门

 

三层循环枚举 k,i,j,执行松弛操作
d(i,j) = min{d(i,j), d(i,k) + d(k,j)}
任意两点之间的最短路最多经过 n − 1 条有向边
k = 1 的循环结束之后
如果 u --> v 的 最短路径 只经过 1,此时的 d(u,v) 就是真实值
k = 2 的循环结束之后
如果 u --> v 的 最短路径 只经过 1,2,此时的 d(u,v) 就是真实值
k = t 的循环结束之后
如果 u --> v 的 最短路径 只经过 1...t,此时的 d(u,v) 就是真实值
不断有真实值被算出,然后又不断合并

不断枚举松弛操作的中间点 k
算法时间复杂度 O(n^3)
优势:处理出 d 之后,任意两点 SP 能 O(1) 询问
注意:必须先枚举中间点!!!必须先枚举中间点!!!必须先枚举中间点!!!

负权环
如果存在一个环,其边权和为负数,则称为负权环
u --> v 存在一个负权环,d(u,v) = −∞
判断图是否含负权环
floyd 后检查是否存在 d(u,u) < 0,如果存在就说明有负权环


单源最短路
不需要知道图中任意两个点的 SP
只需要知道某一点 u 到其它点的 SP
u 称为源点,单源最短路就是求从 u 出发的最短路
为方便,下面记源点为 S

Bellman-Ford
将 d(S,u) 简写为 d(u),初始 d(S) = 0, d(u) = +∞
执行 n 次全局松弛操作
枚举图中每条边 e : (u,v,w)
松弛 d(v) = min{d(v),d(u) + w}

证明:
如果目前存在 d(u) 还不是真实值,一定有边能继续松弛
任意最短路经过不超过 n − 1 条边, n - 1 次松弛足矣

优势:算法很直观
算法时间复杂度 O(nm)
判负权环
再多进行一次全局松弛,如果有 d 被更新,一定有负权环

#include <bits/stdc++.h>

using namespace std;

const int N = 505;
const int inf = 1 << 29;

int d[N][N], n, m;
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) d[i][j] = inf;
    for(int i = 1;i <= n; i++)
        d[i][i]=0; 
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, d[u][v] = min(d[u][v], w);

    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    //必须先枚举中间点 
}

 

如果有一次松弛没有任何边被改变,就说明已经结束了
所以可以做一下小小的优化(常数级别)

 

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<iomanip>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<time.h>
#include<queue>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pr;
const double pi=acos(-1);
#define rep(i,a,n) for(int i=a;i<=n;i++)
#define per(i,n,a) for(int i=n;i>=a;i--)
#define Rep(i,u) for(int i=head[u];i;i=Next[i])
#define clr(a) memset(a,0,sizeof a)
#define pb push_back
#define mp make_pair
#define fi first
#define sc second
ld eps=1e-9;
ll pp=1000000007;
ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;}
ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;}
ll read(){
    ll ans=0;
    char last=' ',ch=getchar();
    while(ch<'0' || ch>'9')last=ch,ch=getchar();
    while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
    if(last=='-')ans=-ans;
    return ans;
}
//head

struct edge{
    int u,v,w;
}edg[n];
int d[n],n,m,s;
int main()
{
    n=read(),m=read(),s=read()
    rep(i,1,n) d[i]=0x7fffffff;
    for(int u ,v,w,i=1;i<=m;i++)
    {
        cin>>u>>v>>w,edg[i]=(edge){u,v,w};
    }
    d[s]=0;
    rep(i,1,n)
    {
        rep(j,1,m)
        {
            int u=edg[j].u,v=edg[j].v,w=edg[j].w;
            if(d[v]>d[u]+w) d[v]=d[u]+w,tag=true;
        }
        if(!tag)break;
    }
}

 

SPFA
Intuitive ideas
Bellman-Ford 算法中,如果 d(u) 在上一次全局更新中没有被更新,
那么这一次全局更新中不必松弛 u 的出边(就是不用它更新其他边)
因为这个操作在上一次中已经做过了
在 Bellman-Ford 的基础上改进
维护队列 Queue,记录哪些点的出边有必要松弛
取出 Queue 最前端的点 u
枚举 u 的所有出边 e : (u,v,w)
尝试松弛 e,若 d(v) > d(u) + w,更新 d(v),同时如果 v 不 在 Queue中,将 v 加入 Queue 中

算法何时终止?
记录每个点加入 Queue 的次数
u 被加入 Queue 一次,意味着 d(u) 被更新了一次
u 最多被更新 n − 1 次,否则肯定有负权环
时间复杂度理论上是O(mn),但肯定不劣于 Bellman-ford,实际远不到 O(nm)

Detail
更新 d(v) 后,如何判断 v 是不是已经在 Queue 里
用一个 Bool 数组记录就行, in/out Queue 的时候标记
怎么记录一个点进入 Queue 的次数
用一个计数数组记录就行, in/out Queue 的时候标记
优势: SPFA 在解决单源最短路问题中很高效
越稀疏越快(网格图会炸)

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;
const int inf = 1 << 29;

struct edge{
    int u, v, w;
};
vector<edge> edg[N];
int d[N], n, m, S;

queue<int> Queue;
bool inQueue[N];
int cntQueue[N];

void add(int u, int v, int w)
{
    edg[u].push_back((edge){u, v, w});
}
int main()
{
    cin >> n >> m >> S;
    for (int i = 1; i <= n; i++) d[i] = inf;
    for (int u, v, w, i = 1; i <= m; i++)
        cin >> u >> v >> w, add(u, v, w);
        
    d[S] = 0; inQueue[S] = true; Queue.push(S);
    while (!Queue.empty())
    {
        int u = Queue.front(); Queue.pop(); inQueue[u] = false;
        for (int e = 0; e < edg[u].size(); e++)
        {
            int v = edg[u][e].v, w = edg[u][e].w;
            if (d[v] > d[u] + w)
            {
                d[v] = d[u] + w;
                if (!inQueue[v])
                {
                    Queue.push(v); ++cntQueue[v]; inQueue[v] = true;
                    if (cntQueue[v] >= n) {cout << "Negative Ring" << endl; return 0;}
                }
            }
        }
    }
    for (int i = 1; i <= n; i++)
        cout << d[i] << endl;
}

 

本蒟蒻博客:传送门

标签:edg,int,Queue,edge,上午,visited,include,Day
来源: https://www.cnblogs.com/lcezych/p/10801912.html