算法之图论

dfs

dfs最重要的东西就是添加路径,dfs下一层和回退

void dfs(路径)
{
	if(路径长度 == 目标长度)
	{
		把答案记录下来
		return;
	}
	for(选择元素 : 选择列表)
	{
		if(元素不符合要求) continue;
		把元素加进答案里面
		dfs(当前路径)
		回退(把元素从答案里面删除)
	} 
}

常见的两道题

排列数字

这道题在leetcode里面也有,是把电话号码排列组合。
这里就只做单纯的排列数字。给定一个数字,生成它的全排列
比如:

123
132
213
231
312
321

下面是代码。
有几个注意的地方

  1. 注意递归的高度,容易多1或者少1
  2. 路径里面的值是选择列表中的,但是路径的下标是人手控制的。在这几行代码几面,极容易把path[k]写成path[i]
#include <iostream>

using namespace std;

const int N = 10;

int path[N];
bool vis[N];

int n;

void dfs(int k)
{
    if(k == n)
    {
        for(int i = 0; i < n; i++) cout<<path[i]<<" ";
        cout<<endl;
        return;
    }
    
    for(int i = 1; i <= n; i++)
    {
        if(vis[i] == true) continue;
        path[k] = i;
        vis[i] = true;
        dfs(k+1);//填到下一空了
        vis[i] = false;
    }
}

int main()
{
    cin>>n;
    dfs(0);//从第一个空开始
}

八皇后

八皇后是经典的dfs问题。其实和上一题一样,选择路径—>dfs—>回退就可以了
这里写的是最朴素版的八皇后写法。每次放一个位置的时候,都去看一下它上方是否有皇后,如果有就不能放了。没有就放。

#include <iostream>
#include <cstring>
using namespace std;
const int N = 10;

char board[N][N];
int n;

bool IsValid(int row,int col)
{
    for(int i = row,j = col; i >=0&&j>=0; i--,j--)
        if(board[i][j] == 'Q')
            return false;
    
    for(int i = row,j = col; i >= 0 && j < n; i--,j++)
        if(board[i][j] == 'Q')
            return false;
    
    for(int i = row; i >= 0; i--)
        if(board[i][col] == 'Q')
            return false;
    
    return true;
}

void dfs(int k)//第k行
{
    if(k == n)
    {
        for(int i = 0; i < n; i++)
        {
            for(int j = 0; j < n; j++)
                cout<<board[i][j];
            cout<<endl;
        }
        cout<<endl;
        return;
    }
    
    for(int i = 0; i < n; i++)
    {
        if(IsValid(k,i) == false) continue;
        board[k][i] = 'Q';//加入路径
        dfs(k+1);//dfs行数,行数由人手控制,循环控制列
        board[k][i] = '.';//回退
    }
}

int main()
{
    memset(board,'.',sizeof board);//初始化
    cin>>n;
    dfs(0);
}

bfs

bfs最重要的两个东西就是queue和判重

bool vis[];
queue<typename> q;
q.push(第一个元素)

while(!q.empty())
{
	int sz = q.size();//每一层的大小
	for(int i = 0; i < sz; i++)
	{
		int t = q.front();
		q.pop();
		if(t == 目标) return 最小步数或者其他东西
		for(元素 : t的周围的元素)
		{
			if(符合某种条件 && vis[t] == false)
				q.push(元素);
				vis[元素] = true;
		}
	}
}

走迷宫的最短路径

这道经典题应该是bfs里面最简单的题了。它包含了bfs最经典的模块:queue和去重

#include <iostream>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 110;
int n,m;
int g[N][N];
bool vis[N][N];
int direction[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};

int bfs()
{
    int startx = 0,starty = 0;
    int endx = n-1,endy = m-1;
    queue<PII> q;
    q.push({0,0});
    int step = 0;
    vis[0][0] = true;
    while(!q.empty())
    {
        int sz = q.size();
        for(int i = 0; i < sz; i++)
        {
            PII t = q.front();
            q.pop();
            int curx = t.first, cury = t.second;
            if(t.first == endx && t.second == endy) return step;
            for(int i = 0; i < 4; i++)
            {
                int newx = curx + direction[i][0];
                int newy = cury + direction[i][1];
                if(newx < n && newx >= 0 && newy < m && newy >=0 && g[newx][newy] == 0 && vis[newx][newy] == false)
                {
                    q.push({newx,newy});
                    vis[newx][newy] = true;
                }
            }
        }
        step++;
    }
    return step;
}

int main()
{
    cin>>n>>m;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            cin>>g[i][j];
            
    cout<<bfs();
}

八数码

八数码这道题比走迷宫难了一点
要注意几点

  1. 八数码的队列存放的应该是每次交换之后的状态,可以像下面用string存,也可以用个二维数组存。用string的话坐标要进行转化一下。
  2. 在走迷宫里面,由于状态是坐标,我们没有改变过队头的值。但是这里我们需要去改变队头的值,除非拷贝一份.如果改变了队头就要还原回去,因为队头的值没有用完,只走过了一个方向。拷贝一份就不需要还原了。这里写的是还原的写法。
  3. 一维坐标转二维坐标 : x = pos/3; y = pos%3;
  4. 二维坐标转一维坐标:pos = x*n+y;
#include <iostream>
#include <queue>
#include <unordered_map>
#include <algorithm>
using namespace std;

unordered_map<string,bool> vis;

int direction[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
string start;
string e = "12345678x";
int bfs()
{
    int step = 0;
    queue<string> q;
    q.push(start);
    vis[start] = true;
    while(!q.empty())
    {
        int sz = q.size();
        for(int i = 0; i < sz; i++)
        {
            string s = q.front();
            q.pop();
            int pos;
            if(s == e) return step;
            for(int i = 0; i < 9; i++)
                if(s[i]=='x')
                    pos = i;
            int x = pos/3, y = pos % 3;
            for(int i = 0; i < 4; i++)
            {
                int newx = x + direction[i][0];
                int newy = y + direction[i][1];
                if(newx < 3 && newx >=0 && newy >= 0 && newy < 3)
                {
                    int newpos = newx*3 + newy;
                    swap(s[newpos],s[pos]);
                    if(vis[s] == false)
                    {
                        q.push(s);
                        vis[s] = true;
                    }
                    swap(s[newpos],s[pos]);
                }
            }
        }
        step++;
    }
    return -1;
}

int main()
{
    char k;
    while(cin>>k)
    {
        if(k==' ') continue;
        else start+=k;
    }
    int res = bfs();
    if(res == -1) cout<<"-1";
    else cout<<res;
}

树和图的遍历

树是特殊的图,遍历方式也和图一样。

树和图的dfs遍历

dfs基本操作。这里的dfs和上面的dfs稍稍有点不同。这里不需要回退。也没有结束标志

void dfs(int k)
{
	vis[k] = true;
	for(int i = h[k]; i != -1; i = ne[i])
	{
		int cur = e[i];
		if(vis[cur] == true) continue;
		dfs(cur)
	}
}

画个图就很好理解了。是一条路走到黑的遍历方式
在这里插入图片描述
这里说一个题外话:为什么无向图要存两条边。在dfs遍历无向图的时候,如果只存了一条边可能无法全部遍历完.因为确实无法走到那个点。举个例子:
在这里插入图片描述

树的重心

这道题很多细节。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
现在要你输出去除重心后,剩余的各个连通块节点数的最大值最小是多少?

思路:

  1. 由于dfs的性质是一条路走到黑,证明它走的都是相邻节点。可以通过一次完整的dfs求出一颗子树的大小。子树的大小刚好就是连通块的大小。
  2. 每次求出的连通块大小,都和之前的大小比一下,把最大的保留下来。
  3. 当所有子树的大小都被求出来了,就可以用总节点的个数-所有子树的个数+它自己得到它上面的连通块个数,然后和之前的最大的连通块大小进行比较,取大的。
  4. 经过上面三个步骤,我们可以得到删除一个点之后的最大连通块大小。在不断删除的过程中
    ,我们会得到很多这样的值,取最小的就是答案。

用图来解释就是下面这样:
在这里插入图片描述
这里有一个问题:其实我们存的是双向边,有没有疑惑为什么最上面的连通块要用(n-sum)这种操作来求,我们不是可以走到那个连通块然后dfs求出连通块的大小吗?
答:对于第一个节点确实是这样的。但是随着节点的向下,上面的节点在vis数组里面都被标记成true了。就算你有指向上面的边,也无法访问到

代码:

#include <iostream>
#include <cstring>
using namespace std;

const int N = 100010,M = 2*N;
int h[N],e[M],ne[M],idx;
bool vis[N];
int n,ans = 0x3f3f3f3f;//注意ans不能初始化为0,不然更新不了了

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a]  = idx++;
}

int dfs(int k)
{
    vis[k] = true;
    int sum = 1;//每一次删除得到的所有子树大小+自己的节点个数
    int res = 0;//每一次删除得到的最大连通块大小
    for(int i = h[k]; i != -1; i = ne[i])
    {
        int cur = e[i];
        if(vis[cur] == true) continue;
        int sz = dfs(cur);
        sum+=sz;
        res = max(res,sz);
    }
    res = max(res,n-sum);
    ans = min(ans,res);
    return sum;
}
int main()
{
    cin>>n;
    memset(h,-1,sizeof h);
    for(int i = 0 ; i < n; i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b)add(b,a);
    }
    dfs(1);
    cout<<ans;
}

树和图的bfs

这里的bfs和上面的bfs是一样的。但是这次我写了一个稍微有点不同的版本。
这次的最短路径用了一个dist数组来存储,没有像上面用step变量了。这是为了后面几个最短路算法的铺垫。
题目:求1号点到n号点的最短距离(有向图,稀疏图)

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int N = 100010;
int h[N],e[N],ne[N],idx;
int dist[N];
bool vis[N];

int n,m;

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

int bfs(int k)
{
    queue<int> q;
    q.push(1);
    vis[1] = true;
    while(!q.empty())
    {
        int sz = q.size();
        for(int i = 0; i < sz; i++)
        {
            int t = q.front();
            q.pop();
            if(t == n) return dist[n];
            for(int i = h[t]; i != -1; i = ne[i])
            {
                int cur = e[i];
                if(vis[cur]) continue;
                q.push(cur);
                dist[cur] = dist[t]+1;
                vis[cur] = true;
            }
        }
    }
    return -1;
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
    }
    int res = bfs(1);
    if(res == -1) cout<<"-1";
    else cout<<res;
}

拓扑序列

拓扑序列是针对有向无环图说的。只有有向无环图才有拓扑序列(具体不证明)
拓扑序列通俗的讲就是先输出指向的,后输出被指向的就叫拓扑序列
拓扑序列不是唯一的。
例如:若a->b,则a b就是这个图的拓扑序列。

a->b->c
a->c
则拓扑序列就是 a b c

就好像公司下班。出度就像阶级关系。管理者(有最多出度的点)肯定是最早走的,管理者走了员工(出度少的)才敢走

通过一个AOV(有向无环图)输出拓扑序列的思路如下:

  1. 先把所有入度为0的点push进队列
  2. 出队,把它相邻的所有元素的入度都-1
  3. 如果相邻的元素入度有为0的,就进队。
  4. 直到队列为空。如果拓扑序列里的元素个数和节点数相同则证明这是一个拓扑序列。
#include <iostream>
#include <queue>
#include <cstring>

using namespace std;

const int N = 100010;

int h[N],e[N],ne[N],idx,resSz;
int d[N];
int ans[N];
int n,m;

int topsort()
{
    queue<int> q;
    for(int i = 1; i <= n; i++)
        if(d[i] == 0)
            q.push(i);
    while(!q.empty())
    {
        int sz = q.size();
        for(int i = 0; i < sz; i++)
        {
            int t = q.front();
            q.pop();
            ans[resSz++] = t;
            if(resSz == n) return resSz;
            for(int i = h[t]; i != -1; i = ne[i])
            {
                int cur = e[i];
                d[cur]--;
                
                if(d[cur] == 0) 
                    q.push(cur);
            }
        }
    }
    return -1;
}

void add(int a,int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}


int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
        d[b]++;
    }
    int res = topsort();
    if(res == -1) cout<<"-1";
    else
    {
        for(int i = 0; i < n; i++) cout<<ans[i]<<" ";
    }
}

最短路

在这里插入图片描述

朴素dijkstra O(n^2)

朴素版dijkstra算法是用来解决正权边的最短路问题的。
由于时间复杂度是O(n^2),所以适合解决稠密图(点数远小于边数)的正权最短路问题
注意:用邻接矩阵来存边。
在邻接矩阵里面更新相邻点最短距离是从1遍历到n
邻接表里更新相邻点最短距离是遍历邻接表
思路:

  1. 先找出没有确定最短路径的点中 离起点最近的点**(从第一个节点遍历)**
  2. 把这个点的最短路径确定下来。(如果是第一个点最短距离就是0)
  3. 把这个点周围的相邻点的最短路径更新。
  4. 继续找下一个离起点最近的点。
  5. 如果结束的时候,目标点距离起点的距离是INF,证明它们之间没有路径相通。

操作是这么操作的,但是具体代码还是有很多细节的。
比如:
6. 每一次遍历这个点的周围的相邻点的距离,应该从1开始。
7. 每次找离起点最近的未确定的最短路的点也应该从0开始遍历
8. 在实际操作中,更新相邻点的最短路径是每一个点都遍历一遍,如果vis标记过了,证明最短路径已经确定了,就不更新,其他的点如果离起点的距离变短,就更新

#include <iostream>
#include <cstring>

using namespace std;

const int N = 510,M = 100010;

int g[N][N],dist[N];
bool vis[N];

int n,m;

int dijkstra()
{
    dist[1] = 0;
    //第一次起点的最短路径当作是未确定的
    for(int i = 0; i < n; i++)
    {
        int cur = -1;
        for(int j = 1; j <= n; j++)
        {
            if(vis[j] == false && (cur == -1 || dist[j] < dist[cur]))
                cur = j;
        }
        vis[cur] = true;
        for(int j = 1; j <= n; j++)
        {
            if(vis[j] == true) continue;
            if(dist[j] > dist[cur] + g[cur][j])
            {
                dist[j] = dist[cur] + g[cur][j];
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    cin>>n>>m;
    memset(g,0x3f,sizeof g);
    for(int i = 0; i < n; i++) g[i][i] = 0;
    memset(dist,0x3f,sizeof dist);
    
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b] = min(g[a][b],c);
    }
    
    int res = dijkstra();
    cout<<res;
}

堆优化dijkstra O(mlogn)

堆优化的dijkstra时间复杂度是O(mlogn),因此适合解决正权稀疏图的最短路问题。
注:稀疏图用邻接表存储
堆优化的部分是朴素dijkstra里面找距离起点最近且没有确定最短距离的点。
朴素dijkstra由于是从1遍历到n,所以在找点的时间复杂度是O(n)
堆找最小值是时间复杂度是O(1).

思路:

  1. 先把起点放进堆里面(注意:由于是当前点的最短距离,因此要用pair来存这些信息,且pair的排序是先排first,再排second。因此要把距离放在first)
  2. 弹出堆顶的元素,这一步相当于找到了距离起点最近且没有确定最短距离的点。
  3. 把和这个点相邻的所有点的最短距离更新一下
  4. 如果堆为空,但dist[n]等于INF,证明起点和终点不连通

总体思路和朴素版差不多。写起来甚至还清晰一点。这里由于是用邻接表来存放边的。因此在更新相邻点的时候有一点不相同。
在邻接矩阵里面更新相邻点最短距离是从1遍历到n,这里是直接遍历邻接表就好了。

这里重点讲一句代码存在的必要性:

if(vis[vertex] == true) continue;

有这句话的本质原因是:会产生数据冗余。
同一个节点被不同的节点指向,且更新后的距离都变小了,因此这个节点被push了两次
当这个节点第一次到堆顶的时候,它的dist其实已经确定了。
当这个节点第二次到堆顶的时候,它不属于未确定最短距离的点。按照算法思路,应该不管它,直接跳过。
如果不写这一句代码,其实并不会wa。只不过它又进去for循环里面循环了。时间复杂度增加。而它进去for循环里面是没有意义的,因为它本来就不是那些要更新周围节点的节点。
(简单来说就是,不写不会WA,但是数据过大时会TLE)

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;
typedef pair<int,int> PII;
const int N = 150010;

int h[N],e[N],ne[N],idx,w[N];
int dist[N];
bool vis[N];

int n,m;

void add(int a,int b,int c)
{
    w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

int dijkstra()
{
    priority_queue<PII,vector<PII>,greater<PII>> heap;
    heap.push({0,1});
    dist[1] = 0;
    while(!heap.empty())
    {
        PII t = heap.top();
        heap.pop();
        int distance = t.first,vertex = t.second;
        //有这句话的本质原因是:会产生数据冗余。同一个节点被不同的节点指向,且更新后的距离都变小了,因此这个节点被push了两次。
        if(vis[vertex]==true) continue;//这句话不加答案不会错,但是会走多一次for循环导致TLE
        //这个点如果最短距离已经被确定了,那么就不需要更新它邻接的节点的最短距离了。(算法思路就是这样的)
        vis[vertex] = true;
        for(int i = h[vertex]; i != -1; i = ne[i])
        {
            int cur = e[i];
            if(vis[cur]==false && dist[cur] > distance + w[i])//加上一个vis[cur] == false的判断可以减少一些已经确定了最短距离的点被push进堆
            //但是有可能一个节点被连续更新了两次,因此同一个节点被push了两遍,产生冗余。
                dist[cur] = distance + w[i];
                heap.push({dist[cur],cur});
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    memset(dist,0x3f,sizeof dist);
    while(m--)
    {
        int a,b,w;
        cin>>a>>b>>w;
        add(a,b,w);
    }
    int res = dijkstra();
    cout<<res;
}

Bellman-ford O(nm)

Bellman-ford如果不在乎时间复杂度的话,可以解决dijkstra的问题,也可以解决负权图的问题。但是如果想解决正权图问题,要么用spfa,要么用dijkstra,不会去使用Bellman-ford。只有一种问题必须要用Bellman-ford-----有边数限制的最短路问题

Bellman-ford也可以判断是否存在负权环,但是一般都用spfa判断(数边数组)。这里就不说如何判负环了.其实也很简单,松弛n-1次之后,再让每条边松弛一次。如果存在边被松弛了,证明有负环。

**bellman-ford算法思路用一句话总结就是:对m条边进行n-1次松弛。因此是两重循环。
松弛操作就是让节点a的最短距离+w(a,b) 赋值给b。更新b的最短距离。**因此时间复杂度是O(nm)

思路:

  1. 循环边数限制次(如果没有边数限制就循环n-1次)
  2. 松弛每一条边。即dist[b] = dist[a] + w
  3. 如果有n个节点,松弛了n-1次,如果没有负权环,一定可以走到最后一个点。
  4. 如果松弛了n-1次,dist[n]还是INF,就证明无法走到那个点。
#include <iostream>
#include <cstring>
using namespace std;

const int N = 510,M = 100010;

int dist[N],backup[N];

struct Edge
{
    int a,b,w;
}edge[M];

int n,m,k;

void bellman_ford()
{
    dist[1] = 0;
    for(int i = 0; i < k ; i++)
    {
        memcpy(backup,dist,sizeof backup);
        for(int j = 0; j < m; j++)
        {
            int a = edge[j].a , b = edge[j].b , c = edge[j].w;
            dist[b] = min(dist[b],backup[a]+c);
        }
    }
    if(dist[n] > 0x3f3f3f3f/2) cout<<"impossible";
    else cout<<dist[n];
}

int main()
{
    cin>>n>>m>>k;
    memset(dist,0x3f,sizeof dist);
    for(int i = 0; i < m; i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        edge[i] = {a,b,c};
    }
    bellman_ford();
}

思路不难,但是细节还是挺多的。
注意点:

  1. 第一重循环代表对每一条边松弛n次,第二重循环代表对每一条边进行松弛操作。
  2. 所谓松弛操作,就是让a节点的最短距离加上a到b的权值,然后赋值给b节点。这个就是目前b节点的最短距离。
  3. 为什么这里要backup数组?这里要严格控制每次循环的时候,只更新一个节点的所有出度。这样循环k次后刚好就是走了k条边的最短距离
  4. 对于普通的求最短距离,bellman-ford算法并不需要backup数组,而实际操作中也并不需要松弛n-1次要会得到答案,因为存在很多串联反应。往往循环一次就可以把很多点的最短距离给确定了。

spfa求最短路 O(m)

spfa是队列优化的bellman-ford。

Bellman-ford每次都松弛都是遍历所有边进行松弛。其实很多都是没有必要的遍历。比如快遍历了n-1次了,此时松弛还是从第一条边开始遍历,很多情况下前面的最小距离已经不会再改变了。

spfa在这基础上进行了优化。spfa用bfs的框架。每次入度的节点的最短距离减小,则出度的节点的最短距离大概率也会减小(不减小的情况可能是出度的节点又指回前面的节点了)。这时候如果队列里面没有出度这个距离减小的节点,就入队。

思路:

  1. 先把第一个节点入队。
  2. pop队头的节点,然后遍历队头节点的邻接表,得到和它相邻的节点。
  3. 判断这些节点的最短距离是否会因为前面节点的最短距离的改变而改变。
  4. 会的话,就修改它们的dist。并且入队。但是直接入队的话有可能会造成数据冗余。比如在很多点都指向同一个点的情况,同一个点的最短距离被不断修改,队中就会有很多份这个节点。因此我们可以判断一下,如果队里面没有这个节点才push
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

const int N = 100010;

int h[N],ne[N],e[N],idx,w[N];
int dist[N];
bool vis[N];
int n,m;

void add(int a,int b,int c)
{
    w[idx] = c;
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}
int spfa()
{
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    while(!q.empty())
    {
        int t = q.front();
        q.pop();
        vis[t] = false;
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int cur = e[i];
            if(dist[cur] > dist[t] + w[i])
            {
                dist[cur] = dist[t] + w[i];
                if(vis[cur] == false)
                {
                    q.push(cur);
                    vis[cur] = true;
                }
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) cout<<"impossible";
    else cout<<dist[n];
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    memset(dist,0x3f,sizeof dist);
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    spfa();
}

再讲一下这里的vis数组

  1. 这里的vis数组是看这个点是否被更新过,更新过的就是true,没有更新过的就是false
  2. 这里的vis数组还可以看成队列里是否有这个节点,有就是true,没有就是false(只有要更新的点才会进队列,换言之,队列里的点都是要更新的)

spfa求负环 O(mn)

spfa判断负环和spfa求最短路有一点不同。由于图可能是分裂的,从1开始可能走不到负环。因此要一开始就把所有点都入进队列然后每次松弛的时候把边数+1.如果边数比n-1大,证明有负环。其余写法和spfa求最短路一样

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int N = 2010,M = 10010;
int h[N],e[M],ne[M],idx,cnt[N],w[M];
int dist[N];
bool vis[N];
int n,m;

void add(int a,int b,int c)
{
    w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

bool spfa()
{
    dist[1] = 0;
    queue<int> q;
    for(int i = 1; i <= n; i++) q.push(i);
    while(!q.empty())
    {
        int t = q.front();
        q.pop();
        vis[t] = false;
        if(cnt[t] >= n) return true;//松弛次数大于等于n,证明有负环
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int cur = e[i];
            if(dist[cur] > dist[t] + w[i])
            {
                dist[cur] = dist[t] + w[i];
                cnt[cur] = cnt[t] + 1;//松弛次数+1
                if(vis[cur] == false)
                {
                    q.push(cur);
                    vis[cur] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    memset(dist,0x3f,sizeof dist);
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    int res = spfa();
    if(res == false) cout<<"No";
    else cout<<"Yes";
}

floyd O(n^3)

floyd算法算的是多起点多终点的最短距离。
用邻接矩阵存储边,一开始存的是权值,后面存的是i->j的最短路径
这个是基于dp的算法,直接背三个循环就好了.

#include <iostream>
#include <cstring>
using namespace std;

const int N = 210;

int d[N][N];
int n,m,k;

void floyd()
{
    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]);//是一个向上的圈,这么记就好ij = i,k + k,j
            }
        }
    }
}

int main()
{
    cin.tie(0);
    cin>>n>>m>>k;
    memset(d,0x3f,sizeof d);
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            if(i == j) d[i][j] = 0;
        }
    }
    
    for(int i = 0; i < m; i++)
    {
        int x,y,z;
        cin>>x>>y>>z;
        d[x][y] = min(d[x][y],z);//有重边
    }
    floyd();
    for(int i = 0; i < k ; i++)
    {
        int x,y;
        cin>>x>>y;
        if(d[x][y] > 0x3f3f3f3f/2) cout<<"impossible"<<endl;
        else cout<<d[x][y]<<endl;
    }
}

最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
用人话讲就是:把图中所有点连起来,权值最小的叫最小生成树

prim O(n^2)

朴素版prim算法,适合稠密图。

思路:

  1. 找到集合外离集合最近的那个点
  2. 把它放进集合里面
  3. 更新它周围的节点到集合的距离
  4. 循环节点个数次

思路还是那么简单,但是细节还是那么多。

  1. 无向图用邻接矩阵存储的时候,要a,b和b,a一起存。
  2. dist里面存的是到集合里面的距离,因此在找集合外离集合最近的一点是dist[j] < dist[cur]
  3. 如果找到了离集合最近的那个点的距离是INF,证明无法形成最小生成树
  4. 每一次找到离集合最近的点都更新一下那条边的权值。(位置不能搞错了,放在循环里面就变成遍历每一条边了)
#include <iostream>
#include <cstring>

using namespace std;

const int N = 510;
int g[N][N],dist[N];
bool vis[N];

int n,m;

void prim()
{
    int res = 0;
    dist[1] = 0;
    for(int i = 0; i < n; i++)
    {
        int cur = -1;
        for(int j = 1; j <= n; j++)
        {
            if(!vis[j] && (cur == -1 || dist[j] < dist[cur]))
                cur = j;
        }
        if(dist[cur] == 0x3f3f3f3f)
        {
            cout<<"impossible";
            return;
        }
        res += dist[cur];
        vis[cur] = true;
        for(int j = 1; j <= n; j++)
        {
            if(!vis[j] && dist[j] > g[cur][j])
            {
                dist[j] = g[cur][j];
            }
        }
    }
    cout<<res;
}

int main()
{
    cin>>n>>m;
    memset(dist,0x3f,sizeof dist);
    memset(g,0x3f,sizeof g);
    while(m--)
    {
        int a,b,w;
        cin>>a>>b>>w;
        g[b][a] = g[a][b] = min(g[a][b],w);
    }
        
    prim();
}

kruskal O(mlogm)

适用于稀疏图

思路:

  1. 将所有边按权重从小到大排序

  2. 枚举每一条边a,b,权重是c

  3. 如果a,b不连通(find(a) != find(b)),就把a,b这条边加入集合里面来(这个操作用并查集的合并操作p[find(a)] = find(b))

  4. 每次加入边之后,集合内部边数加1.

  5. 循环结束后,如果集合内边数小于n-1,那么就证明没有最小生成树

kruskal应该是细节比较少的算法了,很优美。记住思路就能写出来

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 100010,M = 2*N;

int p[N];
struct Edge
{
    int a,b,w;
}edge[M];

bool cmp(const Edge& e1 , const Edge& e2)
{
    return e1.w < e2.w;
}

int n,m;

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void kruskal()
{
    int res = 0, cnt = 0;
    for(int i = 0; i < m; i++)
    {
        int a = edge[i].a , b = edge[i].b , c = edge[i].w;
        if(find(a) != find(b))
        {
            p[find(a)] = find(b);
            cnt++;
            res += c;
        }
    }
    if(cnt < n-1) cout<<"impossible";
    else cout<<res;
}

int main()
{
    cin>>n>>m;
    for(int i = 1; i <= n; i++) p[i] = i;
    for(int i = 0; i < m; i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        edge[i] = {a,b,c};
    }
    sort(edge,edge+m,cmp);
    kruskal();
}

二分图

简单来说,如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。准确地说:把一个图的顶点划分为两个不相交子集 ,使得每一条边都分别连接两个集合中的顶点。如果存在这样的划分,则此图为一个二分图

这就是一个二分图
在这里插入图片描述

两个重要定理

  1. 有奇数环就一定不是二分图

  2. 如果不含有奇数环,就一定是二分图。

染色法

由于同一条边的两个点不能在同一集合,因此产生了染色法。可以判断这是否是一个二分图。
先染色一个节点为黑色,同一条边的节点染成白色(相反的颜色),如果染色发生冲突了,证明不是二分图。如果全部染色完,就是二分图。

染色法可以用dfs来染色

思路:

  1. 遍历每一个点,找到一个没有染色的点,就去染色。
  2. 再把这个点的相邻点全部染色。(一个点的颜色确定了,其他点也确定了)
#include <iostream>
#include <cstring>

using namespace std;
//注意是无向图,边数乘2
const int N = 1e5+10,M = 2e5+10;

int h[N],ne[M],e[M],idx;
int color[N];
int n,m;

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

bool dfs(int k,int c)
{
    color[k] = c;
    for(int i = h[k]; i != -1; i = ne[i])
    {
        int cur = e[i];
        if(color[cur] == 0)
        {
        	//两种写法都是对的
            if(dfs(cur,3-c) == false) return false;
            //dfs(cur,3-c);
        }
        else
        {
            if(color[cur] == color[k]) return false;
        }
    }
    return true;
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b),add(b,a);
    }
    for(int i = 1; i <= n; i++)
    {
        if(color[i] == 0)
        {
            if(dfs(i,1) == false)
            {
                cout<<"No";
                return 0;
            }
        }
    }
    cout<<"Yes";
}

匈牙利算法(相亲算法)

匈牙利算法可以二分图里面最大匹配数。
匹配指两个不同集合里的点之间的边最多只有一条。
最大匹配数是指匹配能有多少种。
在这里插入图片描述
思路:

  1. 从左半边第一个节点开始遍历,为每一个节点找右边匹配的节点
  2. 首先先标记希望匹配的右边节点,如果希望匹配的右边节点没有被匹配,或者曾经被匹配过但是曾经匹配过的对象可以换另外一个匹配对象(符合更换的条件),就进行匹配操作(match[j] = x)
  3. 匹配成功就结束,换成下一个左边节点去匹配(最大匹配数++)。匹配失败也换成下一个左边节点去匹配
#include <iostream>
#include <cstring>
using namespace std;

const int N = 100010;

int h[N],e[N],ne[N],idx;
bool vis[N];
int match[N];


int n,m,k;

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

bool find(int k)
{
    for(int i = h[k]; i != -1; i = ne[i])
    {
        int cur = e[i];
        if(vis[cur] == false)
        {
            vis[cur] = true;
            if(match[cur] == 0 || find(match[cur]))
            {
                match[cur] = k;
                return true;
            }
        }
    }
    return false;
}

int main()
{
    cin>>n>>m>>k;
    memset(h,-1,sizeof h);
    int res = 0;
    while(k--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
    }
    for(int i = 1; i <= n; i ++)
    {
        memset(vis,false,sizeof vis);
        if(find(i) == true)
        {
            res++;
        }
    }
    cout<<res;
}

一点细节:

  1. 每次左边到右边找到了匹配的节点,要更新一下vis数组。因为匈牙利算法不管前面的节点是否已经匹配成功,它都要重新试一遍。
  2. 匈牙利算法找最大匹配数的时候,虽然是无向图,但是只能存一条边。因为是从左边向右查找,是单方向的(这点背住就好)
  3. vis数组代表的意思是当前我预定的节点,预定过的节点就代表匹配过了,不需要再匹配了。匈牙利算法虽然坚持不懈,但还是有底线的,不会匹配过且失败了还去匹配(不要脸了)
  4. 上面的过程理解为谈恋爱的过程就很好理解了。左边是男生,右边是女生。男生追女生不管它是否有男友,都去试一下。如果成功了就匹配,如果失败了就换下一个(他不会再去追求已经失败的女生!!!)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值