简单图论笔记
文章目录
松弛操作:如果满足 d[u]+w(u,v)<d[v],就更新
d[v]
,使得**d[v]=d[u]+w(u,v)**这就是对
边uv
的一次放松操作;
其中,w(u,v)表示边的权重,d(u)表示顶点u到达源s的最短距离(目前已知)
最短路
dijkstra
朴素版
时间复杂度O(n^2)
结合贪心的算法,每一行取最小值,然后跳到最小值那一行继续
int dijkstra()
{
memset(d, 0x3f3f3f, sizeof(d));
d[1] = 0;
for (int i = 0; i < n; i++)
{
int t = -1;
for (int j = 1; j <= n; j++)
{
if (!st[j] && (t == -1 || d[t] > d[j]))
{
t = j;
}
}
st[t] = 1;
for (int j = 1; j <= n; j++)
{
d[j] = min(d[j], d[t] + g[t][j]);
}
}
if (d[n] == 0x3f3f3f)return -1;
return d[n];
}
堆优化版
时间复杂度O(mlogn)
int dijsktra(int nk)
{
memset(st, 0, sizeof(st));
memset(d, 0x3f3f3f3f, sizeof(d));
priority_queue<PII, vector<PII>, greater<PII> >q;
q.push(make_pair(0, 1));//花费...当前位置
d[1] = 0;
while (q.size())
{
PII t = q.top();
q.pop();
int x = t.first, y = t.second;
if (st[y])continue;
st[y] = true;
for (int i = 0; i < g[y].size(); i++)
{
int u = g[y][i].first;//下一个
int w = g[y][i].second;//花费
if (d[u] > d[y] + w)
{
d[u] = d[y] + w;
q.push(make_pair(d[u], u));
}
}
}
return d[n];
}
bellman_ford
bellman_ford模版
时间复杂度O(nm)
可以解决有边数限制的最短路问题,重点在松弛操作上,即对所有点都松弛k次(限制边数为k),若目标位置仍是初始值,则说明在有限边数内无法到达,否则则是有限边数内的最小值
void bellman_ford()
{
memset(dist, 0x3f3f, sizeof(dist));
dist[1] = 0;
for (int i = 0; i < k; i++)
{
memcpy(last, dist, sizeof (dist));
for (int j = 0; j < m; j++)
{
node ee = e[j];
dist[ee.v] = min(dist[ee.v], last[ee.u] + ee.w);
}
}
if (dist[n] >= 0x3f3f3f3f/2)cout << "impossible";
else cout << dist[n];
}
bellman_ford判断负环
由此引申,在执行m(总边数)次松弛后,即所有情况我都试过了若是仍存在更小值,说明存在负环,因此可以用来判断负环
bool bellman_ford()
{
memset(d, 0, sizeof(d));
d[p] = q;
for (int i = 0; i < n - 1; i++)//循环n-1次,可以修改这里调整循环次数
{
bool flag = 0;
for (int j = 0; j < 2 * m; j++)//对每个节点松弛n-1次
{
int a = cq[j].a, b = cq[j].b;
double r = cq[j].r, c = cq[j].c;
if (d[b] < (d[a] - c) * r)
{
d[b] = (d[a] - c) * r;
flag = 1;
}
}
if (!flag)break;
}//在上述操作后,理应每个节点都是最短路径,若还能找到更短,说明存在负环
for (int i = 0; i < 2 * m; i++) {
if (d[cq[i].b] < (d[cq[i].a] - cq[i].c) * cq[i].r)
return true;
}
return false;
}
spfa
spfa模版
时间复杂度在一般情况下O(m)
,而在稠密情况下会退化成O(nm)
,即和bellman_ford
一样
个人感觉像是优化版的bellman
,加入了队列优化
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
spfa判断负环
这里判断负环的方式是统计我走多少次能到目标位置,即以点来判断,有因为每次我只会走是路径和最小的路,所以两个节点路若为正数,那我肯定不会重新走,因此若不存在负环,那我走到目标点需要的操作次数不会超过总点数,若是负数,那我就会多走走让路径和下降,但是这样的话我的这两个就走了非常多次,以至于我的操作次数超过了总点数,因此若在循环过程中操作次数超过了总点数,则说明存在负环。
bool spfa()
{
memset(d, 0x3f3f, sizeof(d));
queue<ll>q;
d[1] = 0;
q.push(1);
st[1] = true;
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = 0; i < g[t].size(); i++)
{
int a = g[t][i].first,w = g[t][i].second;
if (d[a] > d[t] + w)
{
d[a] = d[t] + w;
cnt[a] = cnt[t] + 1;//路过次数,
if (cnt[a] >= n)return false;//很明显,我就n个点,咋地n-1次也可以走到头,所以超过n次就是有负环
if (!st[a])
{
q.push(a);
st[a] = true;
}
}
}
}
return true;
}
floyed
用于处理多元最短路,时间复杂度超级高,有O(n^3)
void floyd()
{
for (int i = 0; i < n; i++)//i是中间节点
{
for (int j = 0; j < n; j++)//起点
{
for (int k = 0; k < n; k++)//中点
{
if (g[j][i] + g[i][k] < g[j][k])
{
g[j][k] = g[j][i] + g[i][k];
}
}
}
}
}
最小生成树(MST)
很容易看出,最短路是求一个点a到一个点b的最短距离,而最小生成树则要求能到所有点,即使连通图边权和最小
prim
Prim 算法其在于点的方面,在稠密图中比Kruskal优
与dijkstra十分相似,核心代码都是松弛操作
for (int j = 1; j <= n; j++)
{
if (!st[j] && (t == -1 || d[j] < d[t]))
{
t = j;
}
}
int prim()
{
int ans = 0;
memset(d,0x3f, sizeof(d));
for (int i = 0; i < n; i++)
{
int t = -1;
for (int j = 1; j <= n; j++)
{
if (!st[j] && (t == -1 || d[j] < d[t]))
{
t = j;
}
}
if (i>0 && d[t] == 0x3f3f3f3f)
{
return -0x3f3f3f3f;
}
if (i > 0)ans += d[t];
st[t] = true;
for (int j = 1; j <= n; j++)
{
d[j] = min(d[j], g[t][j]);
}
}
return ans;
}
krustal
Kruskal算法是基于边的操作,思想也比Prim简单,使用了并查集的方式存mst
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5000 + 5;
const int M = 200000 + 5;
const int INF = 0x3fffffff;
struct edge
{
int u, v, w;
}e[M];
int f[M];
int n, m;
bool cmp(edge x, edge y)//根据权重排序
{
return x.w < y.w;
}
int find(int x)//查找并查集
{
if (f[x] == x)return f[x];
else
{
f[x] = find(f[x]);
return f[x];
}
}
int kruskal()
{
sort(e+1, e + m + 1, cmp);
int cnt = 0;
int ans = 0;
for (int i = 1; i <= m; i++)
{
int x = find(e[i].u);
int y = find(e[i].v);
if (x == y)continue;
ans += e[i].w;
f[x] = y;
cnt++;
}
if(cnt<n-1)return INF;
else return ans;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)f[i] = i;
for (int i = 1; i <= m; i++)
{
cin >> e[i].u >> e[i].v >> e[i].w;
}
int t=kruskal();
if(t==INF)cout <<"impossible";
else cout<<t<<endl;
return 0;
}
二分图
意义
当且仅当图中不含奇数环
即可将二分图划分至两边
染色体法
思路
思路很简单,即通过将所有点染为两种颜色,而且由于不含奇数环,所以不会出现矛盾,否则就不是二分图
给刚开始的点红色,与其相连的为蓝色,以此类推,若出现与某一个点相连的点颜色相同,则不是二分图
cpp实现
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!color[j])
{
if (!dfs(j, 3 - c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
匈牙利算法
解决二分图的最大匹配问题,时间复杂度最差O(n*m)
下面是一些定义
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E}中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(agumenting path)。
基础思路
设匹配双方为A,B
1.若A1的对象B1无匹配对象,则匹配A1B1
2.若A1的对象B1有对象了,则找与A1有关的其他B
3.若A1有关的对象被选完了,就找与A1有关的B成功匹配的对象,看看能不能让他换一个对象
cpp实现
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];
bool st[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d%d", &n1, &n2, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
printf("%d\n", res);
return 0;
}