个人暑假对简单图论的学习笔记

简单图论笔记


松弛操作:如果满足 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;
}
  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值